mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-25 19:38:12 +08:00
render perf refinements (#1632)
* render performance improvements * improve render perf * remove logging * lint * improve memoziation
This commit is contained in:
@@ -284,4 +284,5 @@ export default {
|
||||
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
|
||||
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
|
||||
layoutChoiceAction: selnActions.layoutChoiceAction,
|
||||
setCellSetFromSelection: selnActions.setCellSetFromSelection,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user