diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 40ba928c..f886988e 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -284,4 +284,5 @@ export default { saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction, needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations, layoutChoiceAction: selnActions.layoutChoiceAction, + setCellSetFromSelection: selnActions.setCellSetFromSelection, }; diff --git a/client/src/actions/selection.js b/client/src/actions/selection.js index 1ab8e42b..8afdad8f 100644 --- a/client/src/actions/selection.js +++ b/client/src/actions/selection.js @@ -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, + }); +}; diff --git a/client/src/components/graph/graph.js b/client/src/components/graph/graph.js index d7f3e582..b60b7a42 100644 --- a/client/src/components/graph/graph.js +++ b/client/src/components/graph/graph.js @@ -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; } ); diff --git a/client/src/components/menubar/cellSetButtons.js b/client/src/components/menubar/cellSetButtons.js index 26dba3f0..ae4bb95c 100644 --- a/client/src/components/menubar/cellSetButtons.js +++ b/client/src/components/menubar/cellSetButtons.js @@ -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)); } } diff --git a/client/src/components/menubar/clip.js b/client/src/components/menubar/clip.js index 729d0dbc..5b866bd0 100644 --- a/client/src/components/menubar/clip.js +++ b/client/src/components/menubar/clip.js @@ -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) { /> ); -} +}); export default Clip; diff --git a/client/src/components/menubar/diffexpButtons.js b/client/src/components/menubar/diffexpButtons.js index 92d0c0a2..4212014c 100644 --- a/client/src/components/menubar/diffexpButtons.js +++ b/client/src/components/menubar/diffexpButtons.js @@ -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 ( - {/* eslint-disable react/jsx-props-no-spreading --- disable until eslint-config-airbnb v18.1.1*/} - - - {/* eslint-enable react/jsx-props-no-spreading --- end disable*/} - + + {!differential.diffExp ? ( ({ - 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 { { - dispatch(actions.subsetAction()); - }} - handleSubsetReset={() => { - dispatch(actions.resetSubsetAction()); - }} + subsetPossible={subsetPossible} + subsetResetPossible={subsetResetPossible} + handleSubset={this.handleSubset} + handleSubsetReset={this.handleSubsetReset} /> {disableDiffexp ? null : } diff --git a/client/src/components/menubar/infoMenu.js b/client/src/components/menubar/infoMenu.js index c0eed627..ee6a6496 100644 --- a/client/src/components/menubar/infoMenu.js +++ b/client/src/components/menubar/infoMenu.js @@ -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 (
@@ -72,6 +72,6 @@ function InformationMenu(props) {
); -} +}); export default InformationMenu; diff --git a/client/src/components/menubar/subset.js b/client/src/components/menubar/subset.js index 270ef205..03025cb3 100644 --- a/client/src/components/menubar/subset.js +++ b/client/src/components/menubar/subset.js @@ -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) { ); -} +}); export default Subset; diff --git a/client/src/components/menubar/undoRedo.js b/client/src/components/menubar/undoRedo.js index dba4d048..601dba32 100644 --- a/client/src/components/menubar/undoRedo.js +++ b/client/src/components/menubar/undoRedo.js @@ -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 (
@@ -46,6 +45,6 @@ function InformationMenu(props) {
); -} +}); -export default InformationMenu; +export default UndoRedo; diff --git a/client/src/components/scatterplot/scatterplot.js b/client/src/components/scatterplot/scatterplot.js index d084faa0..43e5b0f1 100644 --- a/client/src/components/scatterplot/scatterplot.js +++ b/client/src/components/scatterplot/scatterplot.js @@ -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; } );