-
- Selected Genes
-
+
- Autosuggest
+ Autosuggest genes
) : null}
@@ -291,7 +278,7 @@ class GeneExpression extends React.Component {
onClick={this.handleBulkAddClick.bind(this)}
loading={userDefinedGenesLoading}
>
- Add
+ Add genes
@@ -318,15 +305,6 @@ class GeneExpression extends React.Component {
: null}
-
- Differentially Expressed Genes
-
-
{differential.diffExp
? _.map(differential.diffExp, (value, index) => {
const name = world.varAnnotations.at(value[0], varIndexName);
diff --git a/client/src/components/graph/graph.js b/client/src/components/graph/graph.js
index c49063f9..63f89b1a 100644
--- a/client/src/components/graph/graph.js
+++ b/client/src/components/graph/graph.js
@@ -3,82 +3,28 @@ import React from "react";
import * as d3 from "d3";
import { connect } from "react-redux";
import mat4 from "gl-mat4";
-import vec3 from "gl-vec3";
import _regl from "regl";
import memoize from "memoize-one";
-import {
- Button,
- AnchorButton,
- Tooltip,
- Popover,
- Menu,
- MenuItem,
- Position,
- NumericInput,
- Icon,
- RadioGroup,
- Radio
-} from "@blueprintjs/core";
import * as globals from "../../globals";
import setupSVGandBrushElements from "./setupSVGandBrush";
-import actions from "../../actions";
import _camera from "../../util/camera";
import _drawPoints from "./drawPointsRegl";
import scaleLinear from "../../util/scaleLinear";
-import { World } from "../../util/stateManager";
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
@connect(state => ({
world: state.world,
- universe: state.universe,
crossfilter: state.crossfilter,
- clipPercentileMin: Math.round(100 * (state.world?.clipQuantiles?.min ?? 0)),
- clipPercentileMax: Math.round(100 * (state.world?.clipQuantiles?.max ?? 1)),
responsive: state.responsive,
colorRGB: state.colors.rgb,
- opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
- resettingInterface: state.controls.resettingInterface,
- 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, // eslint-disable-line camelcase
- undoDisabled: state["@@undoable/past"].length === 0,
- redoDisabled: state["@@undoable/future"].length === 0,
selectionTool: state.graphSelection.tool,
currentSelection: state.graphSelection.selection,
- layoutChoice: state.layoutChoice
+ layoutChoice: state.layoutChoice,
+ graphInteractionMode: state.controls.graphInteractionMode
}))
class Graph extends React.Component {
- static isValidDigitKeyEvent(e) {
- /*
- Return true if this event is necessary to enter a percent number input.
- Return false if not.
-
- Returns true for events with keys: backspace, control, alt, meta, [0-9],
- or events that don't have a key.
- */
- if (e.key === null) return true;
- if (e.ctrlKey || e.altKey || e.metaKey) return true;
-
- // concept borrowed from blueprint's numericInputUtils:
- // keys that print a single character when pressed have a `key` name of
- // length 1. every other key has a longer `key` name (e.g. "Backspace",
- // "ArrowUp", "Shift"). since none of those keys can print a character
- // to the field--and since they may have important native behaviors
- // beyond printing a character--we don't want to disable their effects.
- const isSingleCharKey = e.key.length === 1;
- if (!isSingleCharKey) return true;
-
- const key = e.key.charCodeAt(0) - 48; /* "0" */
- return key >= 0 && key <= 9;
- }
-
computePointPositions = memoize((X, Y, scaleX, scaleY) => {
/*
compute webgl coordinate buffer for each point
@@ -115,7 +61,6 @@ class Graph extends React.Component {
super(props);
this.count = 0;
this.graphPaddingTop = 0;
- this.graphPaddingBottom = 45;
this.graphPaddingRight = globals.leftSidebarWidth;
this.renderCache = {
X: null,
@@ -127,9 +72,7 @@ class Graph extends React.Component {
this.state = {
svg: null,
tool: null,
- container: null,
- mode: "select",
- pendingClipPercentiles: null
+ container: null
};
}
@@ -146,10 +89,17 @@ class Graph extends React.Component {
const sizeBuffer = regl.buffer();
// preallocate coordinate system transformation between data and gl
- const fractionToUse = 0.98; // fraction of dimension to use
+ const fractionToUse = 0.93; // fraction of dimension to use
+ const shiftForMenuBar = 0.05;
const transform = {
glScaleX: scaleLinear([0, 1], [-1 * fractionToUse, 1 * fractionToUse]),
- glScaleY: scaleLinear([0, 1], [1 * fractionToUse, -1 * fractionToUse])
+ glScaleY: scaleLinear(
+ [0, 1],
+ [
+ (1 + shiftForMenuBar) * fractionToUse,
+ (-1 + shiftForMenuBar) * fractionToUse
+ ]
+ )
};
/* first time, but this duplicates above function, should be possile to avoid this */
@@ -179,7 +129,7 @@ class Graph extends React.Component {
});
}
- componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate(prevProps) {
const { renderCache } = this;
const {
world,
@@ -188,14 +138,30 @@ class Graph extends React.Component {
responsive,
selectionTool,
currentSelection,
- layoutChoice
+ layoutChoice,
+ graphInteractionMode
} = this.props;
- const { reglRender, mode, regl, svg } = this.state;
+ const { reglRender, regl, svg } = this.state;
let stateChanges = {};
- if (reglRender && this.reglRenderState === "rendering" && mode !== "zoom") {
- reglRender.cancel();
- this.reglRenderState = "paused";
+ if (reglRender) {
+ if (
+ // If it IS RENDERING and it is NOT IN ZOOM mode, stop rendering.
+ this.reglRenderState === "rendering" &&
+ graphInteractionMode !== "zoom"
+ ) {
+ reglRender.cancel();
+ this.reglRenderState = "paused";
+ }
+
+ if (
+ // If it is NOT RENDERING and it IS IN ZOOM mode, start rendering
+ this.reglRenderState !== "rendering" &&
+ graphInteractionMode === "zoom"
+ ) {
+ this.restartReglLoop();
+ this.reglRenderState = "rendering";
+ }
}
if (regl && world) {
@@ -293,7 +259,7 @@ class Graph extends React.Component {
*/
if (
currentSelection !== prevProps.currentSelection ||
- mode !== prevState.mode ||
+ graphInteractionMode !== prevProps.graphInteractionMode ||
stateChanges.svg
) {
const { tool, container } = this.state;
@@ -308,165 +274,6 @@ class Graph extends React.Component {
}
}
- isResetDisabled = () => {
- /*
- Reset should be disabled when all of the following are true:
- * nothing is selected in the crossfilter
- * world EQ universe
- * nothing is colored by
- * there are no userDefinedGenes or diffexpGenes displayed
- * scatterplot is not displayed
- * nothing in cellset1 or cellset2
- * clip percentiles are [0,100]
- */
- const {
- crossfilter,
- world,
- universe,
- userDefinedGenes,
- diffexpGenes,
- colorAccessor,
- scatterplotXXaccessor,
- scatterplotYYaccessor,
- celllist1,
- celllist2,
- clipPercentileMin,
- clipPercentileMax
- } = this.props;
-
- if (!crossfilter || !world || !universe) {
- return false;
- }
- const nothingSelected = crossfilter.countSelected() === crossfilter.size();
- const nothingColoredBy = !colorAccessor;
- const noGenes = userDefinedGenes.length === 0 && diffexpGenes.length === 0;
- const scatterNotDpl = !scatterplotXXaccessor || !scatterplotYYaccessor;
- const nothingInCellsets = !celllist1 && !celllist2;
-
- return (
- nothingSelected &&
- World.worldEqUniverse(world, universe) &&
- nothingColoredBy &&
- noGenes &&
- scatterNotDpl &&
- nothingInCellsets &&
- clipPercentileMax === 100 &&
- clipPercentileMin === 0
- );
- };
-
- resetInterface = () => {
- const { dispatch } = this.props;
- dispatch({
- type: "interface reset started"
- });
- dispatch(actions.resetInterface());
- };
-
- isClipDisabled = () => {
- /*
- return true if clip button should be disabled.
- */
- const { pendingClipPercentiles } = this.state;
- const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
- const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
-
- const { world } = this.props;
- const currentClipMin = 100 * world?.clipQuantiles?.min;
- const currentClipMax = 100 * world?.clipQuantiles?.max;
-
- // if you change this test, be careful with logic around
- // comparisons between undefined / NaN handling.
- const isDisabled =
- !(clipPercentileMin < clipPercentileMax) ||
- (clipPercentileMin === currentClipMin &&
- clipPercentileMax === currentClipMax);
-
- return isDisabled;
- };
-
- handleClipOnKeyPress = e => {
- /*
- allow only numbers, plus other critical keys which
- may be required to make a number
- */
- if (!Graph.isValidDigitKeyEvent(e)) {
- e.preventDefault();
- }
- };
-
- handleClipPercentileMinValueChange = v => {
- /*
- Ignore anything that isn't a legit number
- */
- if (!Number.isFinite(v)) return;
-
- const { pendingClipPercentiles } = this.state;
- const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
-
- /*
- clamp to [0, currentClipPercentileMax]
- */
- if (v <= 0) v = 0;
- if (v > 100) v = 100;
- const clipPercentileMin = Math.round(v); // paranoia
- this.setState({
- pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
- });
- };
-
- handleClipPercentileMaxValueChange = v => {
- /*
- Ignore anything that isn't a legit number
- */
- if (!Number.isFinite(v)) return;
-
- const { pendingClipPercentiles } = this.state;
- const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
-
- /*
- clamp to [0, 100]
- */
- if (v < 0) v = 0;
- if (v > 100) v = 100;
- const clipPercentileMax = Math.round(v); // paranoia
-
- this.setState({
- pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
- });
- };
-
- handleClipCommit = () => {
- const { dispatch } = this.props;
- const { pendingClipPercentiles } = this.state;
- const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles;
- const min = clipPercentileMin / 100;
- const max = clipPercentileMax / 100;
- dispatch({
- type: "set clip quantiles",
- clipQuantiles: { min, max }
- });
- };
-
- handleClipOpening = () => {
- const { clipPercentileMin, clipPercentileMax } = this.props;
- this.setState({
- pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
- });
- };
-
- handleClipClosing = () => {
- this.setState({ pendingClipPercentiles: null });
- };
-
- handleLayoutChoiceChange = e => {
- const { dispatch } = this.props;
- dispatch({
- type: "set layout choice",
- layoutChoice: e.currentTarget.value
- });
- };
-
brushToolUpdate(tool, container) {
/*
this is called from componentDidUpdate(), so be very careful using
@@ -509,7 +316,7 @@ class Graph extends React.Component {
}
}
- lassoToolUpdate(tool, container) {
+ lassoToolUpdate(tool) {
/*
this is called from componentDidUpdate(), so be very careful using
anything from this.state, which may be updated asynchronously.
@@ -636,7 +443,7 @@ class Graph extends React.Component {
const scale = aspect < 1 ? 1 / aspect : 1;
// compute inverse view matrix
- let inverse = mat4.invert([], camera.view());
+ const inverse = mat4.invert([], camera.view());
// variable names are choosen to reflect inverse of those used
// in mapScreenToPoint().
@@ -769,367 +576,21 @@ class Graph extends React.Component {
}
render() {
- const {
- dispatch,
- responsive,
- crossfilter,
- resettingInterface,
- libraryVersions,
- undoDisabled,
- redoDisabled,
- selectionTool,
- clipPercentileMin,
- clipPercentileMax,
- layoutChoice
- } = this.props;
- const { mode, pendingClipPercentiles } = this.state;
-
- const clipMin =
- pendingClipPercentiles?.clipPercentileMin ?? clipPercentileMin;
- const clipMax =
- pendingClipPercentiles?.clipPercentileMax ?? clipPercentileMax;
- const activeClipClass =
- clipPercentileMin > 0 || clipPercentileMax < 100
- ? " bp3-intent-warning"
- : "";
-
- // constants used to create selection tool button
- let selectionTooltip;
- let selectionButtonClass;
- if (selectionTool === "brush") {
- selectionTooltip = "Brush selection";
- selectionButtonClass = "bp3-icon-select";
- } else {
- selectionTooltip = "Lasso selection";
- selectionButtonClass = "bp3-icon-polygon-filter";
- }
+ const { responsive, graphInteractionMode } = this.props;
return (
-
-
-
- {
- dispatch(actions.regraph());
- dispatch({ type: "increment graph render counter" });
- }}
- >
- subset to current selection
-
-
-
-
- reset
-
-
-
-
-
-
-
-
-
-
- {
- dispatch({ type: "@@undoable/undo" });
- }}
- style={{
- cursor: "pointer"
- }}
- />
-
-
- {
- dispatch({ type: "@@undoable/redo" });
- }}
- style={{
- cursor: "pointer"
- }}
- />
-
-
-
-
-
- }
- position={Position.BOTTOM_RIGHT}
- content={
-
-
- {layoutChoice.available.map(name => (
-
- ))}
-
-
- }
- />
-
-
-
-
- }
- position={Position.BOTTOM_RIGHT}
- onOpening={this.handleClipOpening}
- onClosing={this.handleClipClosing}
- content={
-
-
Clip all continuous values to percentile range
-
-
-
-
- }
- />
-
-
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
-
-
-
-
-
-
-
-
- }
- position={Position.BOTTOM_RIGHT}
- >
-
-
-
-
-
diff --git a/client/src/components/leftSidebar/index.js b/client/src/components/leftSidebar/index.js
new file mode 100644
index 00000000..200f1f7a
--- /dev/null
+++ b/client/src/components/leftSidebar/index.js
@@ -0,0 +1,61 @@
+// jshint esversion: 6
+import React from "react";
+import { connect } from "react-redux";
+import Categorical from "../categorical/categorical";
+import Continuous from "../continuous/continuous";
+import GeneExpression from "../geneExpression";
+import * as globals from "../../globals";
+import DynamicScatterplot from "../scatterplot/scatterplot";
+import TopLeftLogoAndTitle from "./topLeftLogoAndTitle";
+
+@connect(state => ({
+ responsive: state.responsive,
+ scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
+ scatterplotYYaccessor: state.controls.scatterplotYYaccessor
+}))
+class LeftSideBar extends React.Component {
+ render() {
+ const {
+ responsive,
+ scatterplotXXaccessor,
+ scatterplotYYaccessor
+ } = this.props;
+
+ /*
+ this magic number should be made less fragile,
+ if cellxgene logo or tabs change, this must as well
+ */
+ const logoRelatedPadding = 50;
+
+ return (
+
+
+
+
+
+
+
+ {scatterplotXXaccessor && scatterplotYYaccessor ? (
+
+ ) : null}
+
+ );
+ }
+}
+
+export default LeftSideBar;
diff --git a/client/src/components/leftSidebar/topLeftLogoAndTitle.js b/client/src/components/leftSidebar/topLeftLogoAndTitle.js
new file mode 100644
index 00000000..380a10a3
--- /dev/null
+++ b/client/src/components/leftSidebar/topLeftLogoAndTitle.js
@@ -0,0 +1,71 @@
+// jshint esversion: 6
+import React from "react";
+import { connect } from "react-redux";
+import * as globals from "../../globals";
+import Logo from "../framework/logo";
+
+@connect(state => ({
+ responsive: state.responsive,
+ datasetTitle: state.config?.displayNames?.dataset ?? "",
+ scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
+ scatterplotYYaccessor: state.controls.scatterplotYYaccessor
+}))
+class LeftSideBar extends React.Component {
+ render() {
+ const { datasetTitle } = this.props;
+
+ const paddingToAvoidScrollBar = 15;
+
+ return (
+
+
+
+ cell
+ ×
+ gene
+
+
+ {datasetTitle}
+
+
+ );
+ }
+}
+
+export default LeftSideBar;
diff --git a/client/src/components/leftsidebar.js b/client/src/components/leftsidebar.js
deleted file mode 100644
index 5d259316..00000000
--- a/client/src/components/leftsidebar.js
+++ /dev/null
@@ -1,105 +0,0 @@
-// jshint esversion: 6
-import React from "react";
-import { connect } from "react-redux";
-import Categorical from "./categorical/categorical";
-import Continuous from "./continuous/continuous";
-import GeneExpression from "./geneExpression";
-import * as globals from "../globals";
-import DynamicScatterplot from "./scatterplot/scatterplot";
-import Logo from "./framework/logo.js";
-
-@connect(state => ({
- responsive: state.responsive,
- datasetTitle: state.config?.displayNames?.dataset,
- scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
- scatterplotYYaccessor: state.controls.scatterplotYYaccessor
-}))
-class LeftSideBar extends React.Component {
- render() {
- const {
- responsive,
- datasetTitle,
- scatterplotXXaccessor,
- scatterplotYYaccessor
- } = this.props;
-
- /*
- this magic number should be made less fragile,
- if cellxgene logo or tabs change, this must as well
- */
- const metadataSectionPadding = 0;
-
- return (
-
-
-
-
- cell
- ×
- gene
-
-
- {datasetTitle}
-
-
-
-
-
-
-
- {scatterplotXXaccessor && scatterplotYYaccessor ? (
-
- ) : null}
-
- );
- }
-}
-
-export default LeftSideBar;
diff --git a/client/src/components/geneExpression/cellSetButtons.js b/client/src/components/menubar/cellSetButtons.js
similarity index 93%
rename from client/src/components/geneExpression/cellSetButtons.js
rename to client/src/components/menubar/cellSetButtons.js
index 841423c8..5822c748 100644
--- a/client/src/components/geneExpression/cellSetButtons.js
+++ b/client/src/components/menubar/cellSetButtons.js
@@ -32,7 +32,7 @@ class CellSetButton extends React.Component {
render() {
const { differential, eitherCellSetOneOrTwo } = this.props;
const cellListName = `celllist${eitherCellSetOneOrTwo}`;
- let cells_selected = differential[cellListName]
+ const cellsSelected = differential[cellListName]
? differential[cellListName].length
: 0;
return (
@@ -41,7 +41,6 @@ class CellSetButton extends React.Component {
position="top"
>
- {cells_selected}
+ {cellsSelected}
{" cells"}
diff --git a/client/src/components/menubar/clip.js b/client/src/components/menubar/clip.js
new file mode 100644
index 00000000..b9fb8cf8
--- /dev/null
+++ b/client/src/components/menubar/clip.js
@@ -0,0 +1,130 @@
+// jshint esversion: 6
+import React from "react";
+import {
+ Position,
+ Button,
+ Popover,
+ NumericInput,
+ Icon
+} from "@blueprintjs/core";
+
+function Clip(props) {
+ const {
+ pendingClipPercentiles,
+ clipPercentileMin,
+ clipPercentileMax,
+ handleClipOpening,
+ handleClipClosing,
+ handleClipCommit,
+ isClipDisabled,
+ handleClipOnKeyPress,
+ handleClipPercentileMaxValueChange,
+ handleClipPercentileMinValueChange
+ } = props;
+
+ const clipMin =
+ pendingClipPercentiles?.clipPercentileMin ?? clipPercentileMin;
+ const clipMax =
+ pendingClipPercentiles?.clipPercentileMax ?? clipPercentileMax;
+ const activeClipClass =
+ clipPercentileMin > 0 || clipPercentileMax < 100
+ ? " bp3-intent-warning"
+ : "";
+
+ return (
+
+
+ }
+ position={Position.BOTTOM_RIGHT}
+ onOpening={handleClipOpening}
+ onClosing={handleClipClosing}
+ content={
+
+
Clip all continuous values to percentile range
+
+
+
+
+ }
+ />
+
-
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+ );
+}
+
+export default Clip;
diff --git a/client/src/components/menubar/index.js b/client/src/components/menubar/index.js
new file mode 100644
index 00000000..7c807f10
--- /dev/null
+++ b/client/src/components/menubar/index.js
@@ -0,0 +1,464 @@
+// jshint esversion: 6
+import React from "react";
+import { connect } from "react-redux";
+import {
+ Button,
+ AnchorButton,
+ Tooltip,
+ Popover,
+ Position,
+ RadioGroup,
+ Radio
+} from "@blueprintjs/core";
+import { World } from "../../util/stateManager";
+import actions from "../../actions";
+import CellSetButton from "./cellSetButtons";
+import InformationMenu from "./infoMenu";
+import UndoRedoReset from "./undoRedoReset";
+import Clip from "./clip";
+
+@connect(state => ({
+ universe: state.universe,
+ world: state.world,
+ loading: state.controls.loading,
+ crossfilter: state.crossfilter,
+ differential: state.differential,
+ resettingInterface: state.controls.resettingInterface,
+ layoutChoice: state.layoutChoice,
+ graphInteractionMode: state.controls.graphInteractionMode,
+ clipPercentileMin: Math.round(100 * (state.world?.clipQuantiles?.min ?? 0)),
+ clipPercentileMax: Math.round(100 * (state.world?.clipQuantiles?.max ?? 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, // eslint-disable-line camelcase
+ undoDisabled: state["@@undoable/past"].length === 0,
+ redoDisabled: state["@@undoable/future"].length === 0
+}))
+class MenuBar extends React.Component {
+ static isValidDigitKeyEvent(e) {
+ /*
+ Return true if this event is necessary to enter a percent number input.
+ Return false if not.
+
+ Returns true for events with keys: backspace, control, alt, meta, [0-9],
+ or events that don't have a key.
+ */
+ if (e.key === null) return true;
+ if (e.ctrlKey || e.altKey || e.metaKey) return true;
+
+ // concept borrowed from blueprint's numericInputUtils:
+ // keys that print a single character when pressed have a `key` name of
+ // length 1. every other key has a longer `key` name (e.g. "Backspace",
+ // "ArrowUp", "Shift"). since none of those keys can print a character
+ // to the field--and since they may have important native behaviors
+ // beyond printing a character--we don't want to disable their effects.
+ const isSingleCharKey = e.key.length === 1;
+ if (!isSingleCharKey) return true;
+
+ const key = e.key.charCodeAt(0) - 48; /* "0" */
+ return key >= 0 && key <= 9;
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ pendingClipPercentiles: null
+ };
+ }
+
+ isClipDisabled = () => {
+ /*
+ return true if clip button should be disabled.
+ */
+ const { pendingClipPercentiles } = this.state;
+ const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
+ const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
+
+ const { world } = this.props;
+ const currentClipMin = 100 * world?.clipQuantiles?.min;
+ const currentClipMax = 100 * world?.clipQuantiles?.max;
+
+ // if you change this test, be careful with logic around
+ // comparisons between undefined / NaN handling.
+ const isDisabled =
+ !(clipPercentileMin < clipPercentileMax) ||
+ (clipPercentileMin === currentClipMin &&
+ clipPercentileMax === currentClipMax);
+
+ return isDisabled;
+ };
+
+ isResetDisabled = () => {
+ /*
+ Reset should be disabled when all of the following are true:
+ * nothing is selected in the crossfilter
+ * world EQ universe
+ * nothing is colored by
+ * there are no userDefinedGenes or diffexpGenes displayed
+ * scatterplot is not displayed
+ * nothing in cellset1 or cellset2
+ * clip percentiles are [0,100]
+ */
+ const {
+ crossfilter,
+ world,
+ universe,
+ userDefinedGenes,
+ diffexpGenes,
+ colorAccessor,
+ scatterplotXXaccessor,
+ scatterplotYYaccessor,
+ celllist1,
+ celllist2,
+ clipPercentileMin,
+ clipPercentileMax
+ } = this.props;
+
+ if (!crossfilter || !world || !universe) {
+ return false;
+ }
+ const nothingSelected = crossfilter.countSelected() === crossfilter.size();
+ const nothingColoredBy = !colorAccessor;
+ const noGenes = userDefinedGenes.length === 0 && diffexpGenes.length === 0;
+ const scatterNotDpl = !scatterplotXXaccessor || !scatterplotYYaccessor;
+ const nothingInCellsets = !celllist1 && !celllist2;
+
+ return (
+ nothingSelected &&
+ World.worldEqUniverse(world, universe) &&
+ nothingColoredBy &&
+ noGenes &&
+ scatterNotDpl &&
+ nothingInCellsets &&
+ clipPercentileMax === 100 &&
+ clipPercentileMin === 0
+ );
+ };
+
+ resetInterface = () => {
+ const { dispatch } = this.props;
+ dispatch({
+ type: "interface reset started"
+ });
+ dispatch(actions.resetInterface());
+ };
+
+ handleClipOnKeyPress = e => {
+ /*
+ allow only numbers, plus other critical keys which
+ may be required to make a number
+ */
+ if (!MenuBar.isValidDigitKeyEvent(e)) {
+ e.preventDefault();
+ }
+ };
+
+ handleClipPercentileMinValueChange = v => {
+ /*
+ Ignore anything that isn't a legit number
+ */
+ if (!Number.isFinite(v)) return;
+
+ const { pendingClipPercentiles } = this.state;
+ const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
+
+ /*
+ clamp to [0, currentClipPercentileMax]
+ */
+ if (v <= 0) v = 0;
+ if (v > 100) v = 100;
+ const clipPercentileMin = Math.round(v); // paranoia
+ this.setState({
+ pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
+ });
+ };
+
+ handleClipPercentileMaxValueChange = v => {
+ /*
+ Ignore anything that isn't a legit number
+ */
+ if (!Number.isFinite(v)) return;
+
+ const { pendingClipPercentiles } = this.state;
+ const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
+
+ /*
+ clamp to [0, 100]
+ */
+ if (v < 0) v = 0;
+ if (v > 100) v = 100;
+ const clipPercentileMax = Math.round(v); // paranoia
+
+ this.setState({
+ pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
+ });
+ };
+
+ handleClipCommit = () => {
+ const { dispatch } = this.props;
+ const { pendingClipPercentiles } = this.state;
+ const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles;
+ const min = clipPercentileMin / 100;
+ const max = clipPercentileMax / 100;
+ dispatch({
+ type: "set clip quantiles",
+ clipQuantiles: { min, max }
+ });
+ };
+
+ handleClipOpening = () => {
+ const { clipPercentileMin, clipPercentileMax } = this.props;
+ this.setState({
+ pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
+ });
+ };
+
+ handleClipClosing = () => {
+ this.setState({ pendingClipPercentiles: null });
+ };
+
+ handleLayoutChoiceChange = e => {
+ const { dispatch } = this.props;
+ dispatch({
+ type: "set layout choice",
+ layoutChoice: e.currentTarget.value
+ });
+ };
+
+ computeDiffExp = () => {
+ const { dispatch, differential } = this.props;
+ if (differential.celllist1 && differential.celllist2) {
+ dispatch(
+ actions.requestDifferentialExpression(
+ differential.celllist1,
+ differential.celllist2
+ )
+ );
+ }
+ };
+
+ clearDifferentialExpression = () => {
+ const { dispatch, differential } = this.props;
+ dispatch({
+ type: "clear differential expression",
+ diffExp: differential.diffExp
+ });
+ dispatch({
+ type: "clear scatterplot"
+ });
+ };
+
+ render() {
+ const {
+ dispatch,
+ differential,
+ crossfilter,
+ resettingInterface,
+ libraryVersions,
+ undoDisabled,
+ redoDisabled,
+ selectionTool,
+ clipPercentileMin,
+ clipPercentileMax,
+ layoutChoice,
+ graphInteractionMode
+ } = this.props;
+ const { pendingClipPercentiles } = this.state;
+
+ const haveBothCellSets =
+ !!differential.celllist1 && !!differential.celllist2;
+
+ // constants used to create selection tool button
+ let selectionTooltip;
+ let selectionButtonClass;
+ if (selectionTool === "brush") {
+ selectionTooltip = "Brush selection";
+ selectionButtonClass = "bp3-icon-select";
+ } else {
+ selectionTooltip = "Lasso selection";
+ selectionButtonClass = "bp3-icon-polygon-filter";
+ }
+
+ return (
+
+
+
+
+ {!differential.diffExp ? (
+
+
+ Compute Differential Expression
+
+
+ ) : null}
+
+ {differential.diffExp ? (
+
+
+
+ ) : null}
+
+
+ {
+ dispatch(actions.regraph());
+ dispatch({ type: "increment graph render counter" });
+ }}
+ >
+ subset to current selection
+
+
+
+
+
+
+
+
+
+
+ }
+ position={Position.BOTTOM_RIGHT}
+ content={
+
+
+ {layoutChoice.available.map(name => (
+
+ ))}
+
+
+ }
+ />
+
+
+
+
+
+ );
+ }
+}
+
+export default MenuBar;
diff --git a/client/src/components/menubar/infoMenu.js b/client/src/components/menubar/infoMenu.js
new file mode 100644
index 00000000..736e2968
--- /dev/null
+++ b/client/src/components/menubar/infoMenu.js
@@ -0,0 +1,61 @@
+// jshint esversion: 6
+import React from "react";
+import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
+
+function InformationMenu(props) {
+ const { libraryVersions } = props;
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ position={Position.BOTTOM_RIGHT}
+ >
+
+
+
+ );
+}
+
+export default InformationMenu;
diff --git a/client/src/components/menubar/undoRedoReset.js b/client/src/components/menubar/undoRedoReset.js
new file mode 100644
index 00000000..7baafcf7
--- /dev/null
+++ b/client/src/components/menubar/undoRedoReset.js
@@ -0,0 +1,64 @@
+// jshint esversion: 6
+import React from "react";
+import { AnchorButton, Tooltip } from "@blueprintjs/core";
+
+function InformationMenu(props) {
+ const {
+ resettingInterface,
+ undoDisabled,
+ redoDisabled,
+ resetInterface,
+ isResetDisabled,
+ dispatch
+ } = props;
+ return (
+
+
+ {
+ dispatch({ type: "@@undoable/undo" });
+ }}
+ style={{
+ cursor: "pointer"
+ }}
+ />
+
+
+ {
+ dispatch({ type: "@@undoable/redo" });
+ }}
+ style={{
+ cursor: "pointer"
+ }}
+ />
+
+
+
+ reset
+
+
+
+ );
+}
+
+export default InformationMenu;
diff --git a/client/src/components/scatterplot/scatterplot.js b/client/src/components/scatterplot/scatterplot.js
index 1d14488d..bb331649 100644
--- a/client/src/components/scatterplot/scatterplot.js
+++ b/client/src/components/scatterplot/scatterplot.js
@@ -3,7 +3,6 @@
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
import React from "react";
-import _ from "lodash";
import { connect } from "react-redux";
import { Button, ButtonGroup } from "@blueprintjs/core";
import _regl from "regl";
diff --git a/client/src/globals.js b/client/src/globals.js
index 4fc893dd..1ff89b05 100644
--- a/client/src/globals.js
+++ b/client/src/globals.js
@@ -73,114 +73,3 @@ let _API = {
if (window.CELLXGENE && window.CELLXGENE.API) _API = window.CELLXGENE.API;
export const API = _API;
-
-export const ordinalColors = [
- "#0ac115",
- "#c10ab6",
- "#c1710a",
- "#0a5ac1",
- "#c1150a",
- "#0ab6c1",
- "#5ac10a",
- "#710ac1",
- "#0ac171",
- "#c10a5a",
- "#b6c10a",
- "#150ac1",
- "#b2ffb7",
- "#ffb2fa",
- "#ffddb2",
- "#b2d4ff",
- "#ffb7b2",
- "#b2faff",
- "#d4ffb2",
- "#ddb2ff",
- "#b2ffdd",
- "#ffb2d4",
- "#faffb2",
- "#b7b2ff",
- "#27a908",
- "#8b08a9",
- "#a93a08",
- "#0877a9",
- "#a90827",
- "#08a98b",
- "#77a908",
- "#3a08a9",
- "#08a93a",
- "#a90877",
- "#a98b08",
- "#0827a9",
- "#00ff0f",
- "#ff00ef",
- "#ff8e00",
- "#0070ff",
- "#ff0f00",
- "#00efff",
- "#70ff00",
- "#8e00ff",
- "#00ff8e",
- "#ff0070",
- "#efff00",
- "#0f00ff",
- "#006606",
- "#66005f",
- "#663900",
- "#002c66",
- "#660600",
- "#005f66",
- "#2c6600",
- "#390066",
- "#006639",
- "#66002c",
- "#5f6600",
- "#060066",
- "#83ff65",
- "#e165ff",
- "#ff9565",
- "#65cfff",
- "#ff6583",
- "#65ffe1",
- "#cfff65",
- "#9565ff",
- "#65ff95",
- "#ff65cf",
- "#ffe165",
- "#6583ff",
- "#009909",
- "#99008f",
- "#995500",
- "#004399",
- "#990900",
- "#008f99",
- "#439900",
- "#550099",
- "#009955",
- "#990043",
- "#8f9900",
- "#090099",
- "#d9fecc",
- "#f1ccfe",
- "#fed7cc",
- "#ccf3fe",
- "#feccd9",
- "#ccfef1",
- "#f3fecc",
- "#d7ccfe",
- "#ccfed7",
- "#feccf3",
- "#fef1cc",
- "#ccd9fe",
- "#47ea51",
- "#ea47e0",
- "#eaa247",
- "#478fea",
- "#ea5147",
- "#47e0ea",
- "#8fea47",
- "#a247ea",
- "#47eaa2",
- "#ea478f",
- "#e0ea47",
- "#5147ea"
-];
diff --git a/client/src/reducers/controls.js b/client/src/reducers/controls.js
index 35d3cfb6..e9f77956 100644
--- a/client/src/reducers/controls.js
+++ b/client/src/reducers/controls.js
@@ -16,7 +16,7 @@ const Controls = (
diffexpGenes: [],
resettingInterface: false,
-
+ graphInteractionMode: "select",
opacityForDeselectedCells: 0.2,
scatterplotXXaccessor: null, // just easier to read
scatterplotYYaccessor: null,
@@ -138,6 +138,11 @@ const Controls = (
/*******************************
User Events
*******************************/
+ case "change graph interaction mode":
+ return {
+ ...state,
+ graphInteractionMode: action.data
+ };
case "change opacity deselected cells in 2d graph background":
return {
...state,
diff --git a/client/src/reducers/undoableConfig.js b/client/src/reducers/undoableConfig.js
index 59943b9b..a94d9c8e 100644
--- a/client/src/reducers/undoableConfig.js
+++ b/client/src/reducers/undoableConfig.js
@@ -74,7 +74,8 @@ const saveOnActions = new Set([
"set World to current selection",
"set clip quantiles",
- "set layout choice"
+ "set layout choice",
+ "change graph interaction mode"
]);
/**