render perf refinements (#1632)

* render performance improvements

* improve render perf

* remove logging

* lint

* improve memoziation
This commit is contained in:
Bruce Martin
2020-07-16 09:53:46 -07:00
committed by GitHub
parent 17c1145ee4
commit 2265bfc921
11 changed files with 163 additions and 114 deletions
+1
View File
@@ -284,4 +284,5 @@ export default {
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
layoutChoiceAction: selnActions.layoutChoiceAction,
setCellSetFromSelection: selnActions.setCellSetFromSelection,
};
+13
View File
@@ -209,3 +209,16 @@ export const layoutChoiceAction = (newLayoutChoice) => async (
obsCrossfilter,
});
};
/*
Differential expression set selection
*/
export const setCellSetFromSelection = (cellSetId) => (dispatch, getState) => {
const { obsCrossfilter } = getState();
const selected = obsCrossfilter.allSelectedLabels();
dispatch({
type: `store current cell selection as differential set ${cellSetId}`,
data: selected.length > 0 ? selected : null,
});
};
+40 -12
View File
@@ -139,6 +139,32 @@ class Graph extends React.Component {
}
);
computeHighlightFlags = memoize(
(nObs, pointDilationData, pointDilationLabel) => {
const flags = new Float32Array(nObs);
if (pointDilationData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (pointDilationData[i] === pointDilationLabel) {
flags[i] = flagHighlight;
}
}
}
return flags;
}
);
computeColorByFlags = memoize((nObs, colorByData) => {
const flags = new Float32Array(nObs);
if (colorByData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (!Number.isFinite(colorByData[i])) {
flags[i] = flagNaN;
}
}
}
return flags;
});
computePointFlags = memoize(
(crossfilter, colorByData, pointDilationData, pointDilationLabel) => {
/*
@@ -154,23 +180,25 @@ class Graph extends React.Component {
continuous metadata, as they rely on different tests, and some of the flags
(eg, isNaN) are meaningless in the face of categorical metadata.
*/
const flags = this.computeSelectedFlags(
const nObs = crossfilter.size();
const flags = new Float32Array(nObs);
const selectedFlags = this.computeSelectedFlags(
crossfilter,
flagSelected,
0
).slice();
);
const highlightFlags = this.computeHighlightFlags(
nObs,
pointDilationData,
pointDilationLabel
);
const colorByFlags = this.computeColorByFlags(nObs, colorByData);
if (colorByData || pointDilationData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (pointDilationData) {
flags[i] +=
pointDilationData[i] === pointDilationLabel ? flagHighlight : 0;
}
if (colorByData) {
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
}
}
for (let i = 0; i < nObs; i += 1) {
flags[i] = selectedFlags[i] + highlightFlags[i] + colorByFlags[i];
}
return flags;
}
);
@@ -3,25 +3,18 @@ import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core";
import { connect } from "react-redux";
import { tooltipHoverOpenDelay } from "../../globals";
import actions from "../../actions";
@connect()
@connect((state) => ({
differential: state.differential,
}))
class CellSetButton extends React.PureComponent {
set() {
const {
differential,
crossfilter,
dispatch,
eitherCellSetOneOrTwo,
} = this.props;
const { differential, dispatch, eitherCellSetOneOrTwo } = this.props;
let set = crossfilter.allSelectedLabels();
if (set.length === 0) set = null;
if (!differential.diffExp) {
/* diffexp needs to be cleared before we store a new set */
dispatch({
type: `store current cell selection as differential set ${eitherCellSetOneOrTwo}`,
data: set,
});
// disallow this action if the user has active differential expression results
dispatch(actions.setCellSetFromSelection(eitherCellSetOneOrTwo));
}
}
+2 -3
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import {
Position,
@@ -11,7 +10,7 @@ import {
import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css";
function Clip(props) {
const Clip = React.memo((props) => {
const {
pendingClipPercentiles,
clipPercentileMin,
@@ -129,6 +128,6 @@ function Clip(props) {
/>
</div>
);
}
});
export default Clip;
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import { connect } from "react-redux";
import { Button, ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
@@ -8,15 +7,13 @@ import actions from "../../actions";
import CellSetButton from "./cellSetButtons";
@connect((state) => ({
config: state.config,
crossfilter: state.obsCrossfilter,
differential: state.differential,
celllist1: state.differential?.celllist1,
celllist2: state.differential?.celllist2,
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
diffexpCellcountMax: state.config?.limits?.["diffexp_cellcount_max"],
}))
class DiffexpButtons extends React.Component {
class DiffexpButtons extends React.PureComponent {
computeDiffExp = () => {
const { dispatch, differential } = this.props;
if (differential.celllist1 && differential.celllist2) {
@@ -42,13 +39,7 @@ class DiffexpButtons extends React.Component {
render() {
/* diffexp-related buttons may be disabled */
const {
differential,
diffexpMayBeSlow,
diffexpCellcountMax,
crossfilter,
dispatch,
} = this.props;
const { differential, diffexpMayBeSlow, diffexpCellcountMax } = this.props;
const haveBothCellSets =
!!differential.celllist1 && !!differential.celllist2;
@@ -72,17 +63,8 @@ class DiffexpButtons extends React.Component {
return (
<ButtonGroup className={styles.menubarButton}>
{/* eslint-disable react/jsx-props-no-spreading --- disable until eslint-config-airbnb v18.1.1*/}
<CellSetButton
{...{ differential, crossfilter, dispatch }}
eitherCellSetOneOrTwo={1}
/>
<CellSetButton
{...{ differential, crossfilter, dispatch }}
eitherCellSetOneOrTwo={2}
/>
{/* eslint-enable react/jsx-props-no-spreading --- end disable*/}
<CellSetButton eitherCellSetOneOrTwo={1} />
<CellSetButton eitherCellSetOneOrTwo={2} />
{!differential.diffExp ? (
<Tooltip
content={warnMaxSizeExceeded ? tipMessageWarn : tipMessage}
+50 -43
View File
@@ -12,32 +12,44 @@ import Subset from "./subset";
import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons";
@connect((state) => ({
annoMatrix: state.annoMatrix,
crossfilter: state.obsCrossfilter,
differential: state.differential,
graphInteractionMode: state.controls.graphInteractionMode,
clipPercentileMin: Math.round(100 * (state.annoMatrix?.clipRange?.[0] ?? 0)),
clipPercentileMax: Math.round(100 * (state.annoMatrix?.clipRange?.[1] ?? 1)),
userDefinedGenes: state.controls.userDefinedGenes,
diffexpGenes: state.controls.diffexpGenes,
colorAccessor: state.colors.colorAccessor,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
celllist1: state.differential.celllist1,
celllist2: state.differential.celllist2,
libraryVersions: state.config?.["library_versions"],
undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"],
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
categoricalSelection: state.categoricalSelection,
}))
class MenuBar extends React.Component {
@connect((state) => {
const { annoMatrix } = state;
const crossfilter = state.obsCrossfilter;
const selectedCount = crossfilter.countSelected();
const subsetPossible =
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all are selected
const subsetResetPossible =
annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs;
return {
subsetPossible,
subsetResetPossible,
differential: state.differential,
graphInteractionMode: state.controls.graphInteractionMode,
clipPercentileMin: Math.round(100 * (annoMatrix?.clipRange?.[0] ?? 0)),
clipPercentileMax: Math.round(100 * (annoMatrix?.clipRange?.[1] ?? 1)),
userDefinedGenes: state.controls.userDefinedGenes,
diffexpGenes: state.controls.diffexpGenes,
colorAccessor: state.colors.colorAccessor,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
celllist1: state.differential.celllist1,
celllist2: state.differential.celllist2,
libraryVersions: state.config?.["library_versions"],
undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"],
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
diffexpMayBeSlow:
state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
categoricalSelection: state.categoricalSelection,
};
})
class MenuBar extends React.PureComponent {
static isValidDigitKeyEvent(e) {
/*
Return true if this event is necessary to enter a percent number input.
@@ -171,17 +183,14 @@ class MenuBar extends React.Component {
});
};
subsetPossible = () => {
const { crossfilter } = this.props;
const count = crossfilter.countSelected();
return (
count !== 0 && count !== crossfilter.size() // ie, not all are selected
);
handleSubset = () => {
const { dispatch } = this.props;
dispatch(actions.subsetAction());
};
subsetResetPossible = () => {
const { annoMatrix } = this.props;
return annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs;
handleSubsetReset = () => {
const { dispatch } = this.props;
dispatch(actions.resetSubsetAction());
};
render() {
@@ -201,6 +210,8 @@ class MenuBar extends React.Component {
tosURL,
categoricalSelection,
colorAccessor,
subsetPossible,
subsetResetPossible,
} = this.props;
const { pendingClipPercentiles } = this.state;
@@ -309,14 +320,10 @@ class MenuBar extends React.Component {
</Tooltip>
</ButtonGroup>
<Subset
subsetPossible={this.subsetPossible()}
subsetResetPossible={this.subsetResetPossible()}
handleSubset={() => {
dispatch(actions.subsetAction());
}}
handleSubsetReset={() => {
dispatch(actions.resetSubsetAction());
}}
subsetPossible={subsetPossible}
subsetResetPossible={subsetResetPossible}
handleSubset={this.handleSubset}
handleSubsetReset={this.handleSubsetReset}
/>
{disableDiffexp ? null : <DiffexpButtons />}
</div>
+2 -2
View File
@@ -3,7 +3,7 @@ import React from "react";
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
import styles from "./menubar.css";
function InformationMenu(props) {
const InformationMenu = React.memo((props) => {
const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
return (
<div className={`bp3-button-group ${styles.menubarButton}`}>
@@ -72,6 +72,6 @@ function InformationMenu(props) {
</Popover>
</div>
);
}
});
export default InformationMenu;
+2 -2
View File
@@ -3,7 +3,7 @@ import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core";
import styles from "./menubar.css";
import * as globals from "../../globals";
function Subset(props) {
const Subset = React.memo((props) => {
const {
subsetPossible,
subsetResetPossible,
@@ -41,6 +41,6 @@ function Subset(props) {
</Tooltip>
</ButtonGroup>
);
}
});
export default Subset;
+3 -4
View File
@@ -1,10 +1,9 @@
// jshint esversion: 6
import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core";
import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css";
function InformationMenu(props) {
const UndoRedo = React.memo((props) => {
const { undoDisabled, redoDisabled, dispatch } = props;
return (
<div className={`bp3-button-group ${styles.menubarButton}`}>
@@ -46,6 +45,6 @@ function InformationMenu(props) {
</Tooltip>
</div>
);
}
});
export default InformationMenu;
export default UndoRedo;
@@ -117,6 +117,32 @@ class Scatterplot extends React.PureComponent {
}
);
computeHighlightFlags = memoize(
(nObs, pointDilationData, pointDilationLabel) => {
const flags = new Float32Array(nObs);
if (pointDilationData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (pointDilationData[i] === pointDilationLabel) {
flags[i] = flagHighlight;
}
}
}
return flags;
}
);
computeColorByFlags = memoize((nObs, colorByData) => {
const flags = new Float32Array(nObs);
if (colorByData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (!Number.isFinite(colorByData[i])) {
flags[i] = flagNaN;
}
}
}
return flags;
});
computePointFlags = memoize(
(crossfilter, colorByData, pointDilationData, pointDilationLabel) => {
/*
@@ -132,24 +158,25 @@ class Scatterplot extends React.PureComponent {
continuous metadata, as they rely on different tests, and some of the flags
(eg, isNaN) are meaningless in the face of categorical metadata.
*/
const nObs = crossfilter.size();
const flags = this.computeSelectedFlags(
const selectedFlags = this.computeSelectedFlags(
crossfilter,
flagSelected,
0
).slice();
);
const highlightFlags = this.computeHighlightFlags(
nObs,
pointDilationData,
pointDilationLabel
);
const colorByFlags = this.computeColorByFlags(nObs, colorByData);
if (colorByData || pointDilationData) {
for (let i = 0, len = flags.length; i < len; i += 1) {
if (pointDilationData) {
flags[i] +=
pointDilationData[i] === pointDilationLabel ? flagHighlight : 0;
}
if (colorByData) {
flags[i] += Number.isFinite(colorByData[i]) ? 0 : flagNaN;
}
}
const flags = new Float32Array(nObs);
for (let i = 0; i < nObs; i += 1) {
flags[i] = selectedFlags[i] + highlightFlags[i] + colorByFlags[i];
}
return flags;
}
);