mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-04 05:48:11 +08:00
menubar (#804)
* menubar 1 * zoom switching * centering, pixel perfect canvas * remove dead args and code * clipping * remove log * if * connect props * lint * undo * logo left, componetize * graph back to full height * shadow to top * do not prematurely call event handlers during render * change test to deal with async histogram creation * left section padding * lint * adjust graph to account for top bar, * lasso tests * refine histogram tests * remove testing (onlys)
This commit is contained in:
committed by
Charlotte Weaver
parent
eac514e04d
commit
6aeefb0fe6
@@ -27,7 +27,7 @@ export const datasets = {
|
|||||||
lasso: [
|
lasso: [
|
||||||
{
|
{
|
||||||
"coordinates-as-percent": { x1: 0.05, y1: 0.25, x2: 0.15, y2: 0.35 },
|
"coordinates-as-percent": { x1: 0.05, y1: 0.25, x2: 0.15, y2: 0.35 },
|
||||||
count: "101"
|
count: "71"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
categorical: [
|
categorical: [
|
||||||
@@ -92,7 +92,7 @@ export const datasets = {
|
|||||||
},
|
},
|
||||||
lasso: {
|
lasso: {
|
||||||
"coordinates-as-percent": { x1: 0.45, y1: 0.05, x2: 0.65, y2: 0.15 },
|
"coordinates-as-percent": { x1: 0.45, y1: 0.05, x2: 0.65, y2: 0.15 },
|
||||||
count: "46"
|
count: "36"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scatter: {
|
scatter: {
|
||||||
|
|||||||
@@ -154,10 +154,13 @@ describe("gene entry", () => {
|
|||||||
await utils.clickOn("section-bulk-add");
|
await utils.clickOn("section-bulk-add");
|
||||||
await utils.typeInto("input-bulk-add", testGenes.join(","));
|
await utils.typeInto("input-bulk-add", testGenes.join(","));
|
||||||
await page.keyboard.press("Enter");
|
await page.keyboard.press("Enter");
|
||||||
const userGeneHist = await cxgActions.getAllHistograms(
|
|
||||||
"histogram-user-gene"
|
const allHistograms = await cxgActions.getAllHistograms(
|
||||||
|
"histogram-user-gene",
|
||||||
|
testGenes
|
||||||
);
|
);
|
||||||
expect(userGeneHist).toEqual(expect.arrayContaining(testGenes));
|
expect(allHistograms).toEqual(expect.arrayContaining(testGenes));
|
||||||
|
expect(allHistograms.length).toEqual(testGenes.length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,10 +179,14 @@ describe("diffexp", () => {
|
|||||||
}
|
}
|
||||||
await cxgActions.cellSet(2);
|
await cxgActions.cellSet(2);
|
||||||
await utils.clickOn("diffexp-button");
|
await utils.clickOn("diffexp-button");
|
||||||
const diffExpHists = await cxgActions.getAllHistograms("histogram-diffexp");
|
const allHistograms = await cxgActions.getAllHistograms(
|
||||||
expect(diffExpHists).toEqual(
|
"histogram-diffexp",
|
||||||
|
data.diffexp["gene-results"]
|
||||||
|
);
|
||||||
|
expect(allHistograms).toEqual(
|
||||||
expect.arrayContaining(data.diffexp["gene-results"])
|
expect.arrayContaining(data.diffexp["gene-results"])
|
||||||
);
|
);
|
||||||
|
expect(allHistograms.length).toEqual(data.diffexp["gene-results"].length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,26 @@ export const puppeteerUtils = puppeteerPage => ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async waitForAllByIds(testids, props = {}) {
|
||||||
|
await Promise.all(
|
||||||
|
testids.map(testid =>
|
||||||
|
puppeteerPage.waitForSelector(`[data-testid='${testid}']`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAllByClass(testclass, props = {}) {
|
||||||
|
const elements = await puppeteerPage.$$eval(
|
||||||
|
`[data-testclass=${testclass}]`,
|
||||||
|
els => {
|
||||||
|
return els.map(el => {
|
||||||
|
return el.dataset.testid;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return elements;
|
||||||
|
},
|
||||||
|
|
||||||
async typeInto(testid, text) {
|
async typeInto(testid, text) {
|
||||||
// only works for text without special characters
|
// only works for text without special characters
|
||||||
await this.waitByID(testid);
|
await this.waitByID(testid);
|
||||||
@@ -32,8 +52,8 @@ export const puppeteerUtils = puppeteerPage => ({
|
|||||||
await puppeteerPage.waitFor(200);
|
await puppeteerPage.waitFor(200);
|
||||||
// select all
|
// select all
|
||||||
|
|
||||||
await puppeteerPage.click(selector, {clickCount: 3})
|
await puppeteerPage.click(selector, { clickCount: 3 });
|
||||||
await puppeteerPage.keyboard.type("Backspace")
|
await puppeteerPage.keyboard.type("Backspace");
|
||||||
await puppeteerPage.type(selector, text);
|
await puppeteerPage.type(selector, text);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -77,20 +97,16 @@ export const cellxgeneActions = puppeteerPage => ({
|
|||||||
await puppeteerPage.mouse.up();
|
await puppeteerPage.mouse.up();
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAllHistograms(testclass) {
|
async getAllHistograms(testclass, testids) {
|
||||||
await puppeteerUtils(puppeteerPage).waitByClass(testclass);
|
const histTestIds = testids.map(tid => `histogram-${tid}`);
|
||||||
const histograms = await puppeteerPage.$$eval(
|
// these load asynchronously, so we need to wait for each histogram individually
|
||||||
`[data-testclass=${testclass}]`,
|
await puppeteerUtils(puppeteerPage).waitForAllByIds(histTestIds);
|
||||||
els => {
|
const allHistograms = await puppeteerUtils(puppeteerPage).getAllByClass(
|
||||||
return els.map(el => {
|
testclass
|
||||||
return el.dataset.testid.substring(
|
);
|
||||||
"histogram_".length,
|
return allHistograms.map(hist =>
|
||||||
el.dataset.testid.length
|
hist.substr("histogram_".length, hist.length)
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return histograms;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAllCategoriesAndCounts(category) {
|
async getAllCategoriesAndCounts(category) {
|
||||||
@@ -180,11 +196,16 @@ export const cellxgeneActions = puppeteerPage => ({
|
|||||||
await page.waitFor(200);
|
await page.waitFor(200);
|
||||||
},
|
},
|
||||||
|
|
||||||
async clip(min = 0, max = 100) {
|
async clip(min = 0, max = 100) {
|
||||||
await puppeteerUtils(puppeteerPage).clickOn("visualization-settings");
|
await puppeteerUtils(puppeteerPage).clickOn("visualization-settings");
|
||||||
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto("clip-min-input", min);
|
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
|
||||||
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto("clip-max-input", max);
|
"clip-min-input",
|
||||||
|
min
|
||||||
|
);
|
||||||
|
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
|
||||||
|
"clip-max-input",
|
||||||
|
max
|
||||||
|
);
|
||||||
await puppeteerUtils(puppeteerPage).clickOn("clip-commit");
|
await puppeteerUtils(puppeteerPage).clickOn("clip-commit");
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import Helmet from "react-helmet";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
import Container from "./framework/container";
|
import Container from "./framework/container";
|
||||||
import LeftSideBar from "./leftsidebar";
|
import LeftSideBar from "./leftSidebar";
|
||||||
import Legend from "./continuousLegend";
|
import Legend from "./continuousLegend";
|
||||||
import Graph from "./graph/graph";
|
import Graph from "./graph/graph";
|
||||||
|
import MenuBar from "./menubar";
|
||||||
|
|
||||||
import actions from "../actions";
|
import actions from "../actions";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
@@ -71,18 +73,23 @@ class App extends React.Component {
|
|||||||
loading cellxgene
|
loading cellxgene
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
{error ? (
|
||||||
{loading ? null : <LeftSideBar />}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: 15,
|
position: "fixed",
|
||||||
width: 1440 - 410 /* but responsive */,
|
fontWeight: 500,
|
||||||
marginLeft: 350 /* but responsive */
|
top: window.innerHeight / 2,
|
||||||
|
left: window.innerWidth / 2 - 50
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? null : <Graph key={graphRenderCounter} />}
|
error loading
|
||||||
<Legend />
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div>
|
||||||
|
{loading ? null : <LeftSideBar />}
|
||||||
|
{loading ? null : <MenuBar />}
|
||||||
|
{loading ? null : <Graph key={graphRenderCounter} />}
|
||||||
|
<Legend />
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ class Categories extends React.Component {
|
|||||||
padding: globals.leftSidebarSectionPadding
|
padding: globals.leftSidebarSectionPadding
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p
|
|
||||||
style={Object.assign({}, globals.leftSidebarSectionHeading, {
|
|
||||||
marginTop: 4
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Categorical Metadata
|
|
||||||
</p>
|
|
||||||
{_.map(categoricalSelection, (catState, catName) => (
|
{_.map(categoricalSelection, (catState, catName) => (
|
||||||
<Category key={catName} metadataField={catName} />
|
<Category key={catName} metadataField={catName} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Continuous extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* initial value for iterator to simulate index, ranges is an object */
|
/* initial value for iterator to simulate index, ranges is an object */
|
||||||
let zebra = -1;
|
let zebra = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import { interpolateViridis, interpolateCool } from "d3-scale-chromatic";
|
import { interpolateCool } from "d3-scale-chromatic";
|
||||||
|
|
||||||
// create continuous color legend
|
// create continuous color legend
|
||||||
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
|
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as globals from "../../globals";
|
|||||||
const Logo = props => {
|
const Logo = props => {
|
||||||
const { size } = props;
|
const { size } = props;
|
||||||
return (
|
return (
|
||||||
<svg width={size} height={size} viewBox={`0 0 48 48`} fill="none">
|
<svg width={size} height={size} viewBox="0 0 48 48" fill="none">
|
||||||
<rect width="48" height="48" fill="white" />
|
<rect width="48" height="48" fill="white" />
|
||||||
<rect width="48" height="48" fill={globals.logoColor} />
|
<rect width="48" height="48" fill={globals.logoColor} />
|
||||||
<rect x="19" y="19" width="22" height="22" fill="white" />
|
<rect x="19" y="19" width="22" height="22" fill="white" />
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { Button, AnchorButton, Tooltip } from "@blueprintjs/core";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import actions from "../../actions";
|
|
||||||
import CellSetButton from "./cellSetButtons";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
differential: state.differential,
|
|
||||||
world: state.world,
|
|
||||||
crossfilter: state.crossfilter
|
|
||||||
}))
|
|
||||||
class Expression extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { differential } = this.props;
|
|
||||||
if (!differential) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const haveBothCellSets =
|
|
||||||
!!differential.celllist1 && !!differential.celllist2;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginRight: 10,
|
|
||||||
marginBottom: 10,
|
|
||||||
paddingLeft: globals.leftSidebarSectionPadding
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
|
|
||||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
|
|
||||||
{!differential.diffExp ? (
|
|
||||||
<Tooltip
|
|
||||||
content="Add two cells selections, see the top 15 differentially expressed genes between them"
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
style={{ marginTop: 10 }}
|
|
||||||
disabled={!haveBothCellSets}
|
|
||||||
intent="primary"
|
|
||||||
data-testid="diffexp-button"
|
|
||||||
loading={differential.loading}
|
|
||||||
fill
|
|
||||||
type="button"
|
|
||||||
onClick={this.computeDiffExp.bind(this)}
|
|
||||||
>
|
|
||||||
Compute Differential Expression
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
{differential.diffExp ? (
|
|
||||||
<Tooltip
|
|
||||||
content="Remove differentially expressed gene list and clear cell selections"
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
fill
|
|
||||||
style={{ marginTop: 10 }}
|
|
||||||
intent="warning"
|
|
||||||
onClick={this.clearDifferentialExpression.bind(this)}
|
|
||||||
>
|
|
||||||
Clear Differential Expression
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Expression;
|
|
||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
postUserErrorToast,
|
postUserErrorToast,
|
||||||
keepAroundErrorToast
|
keepAroundErrorToast
|
||||||
} from "../framework/toasters";
|
} from "../framework/toasters";
|
||||||
import ExpressionButtons from "./expressionButtons";
|
|
||||||
|
|
||||||
const renderGene = (fuzzySortResult, { handleClick, modifiers, query }) => {
|
const renderGene = (fuzzySortResult, { handleClick, modifiers, query }) => {
|
||||||
if (!modifiers.matchesPredicate) {
|
if (!modifiers.matchesPredicate) {
|
||||||
@@ -180,19 +179,7 @@ class GeneExpression extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div>
|
||||||
style={{
|
|
||||||
marginTop: 30
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={Object.assign({}, globals.leftSidebarSectionHeading, {
|
|
||||||
paddingLeft: globals.leftSidebarSectionPadding,
|
|
||||||
margin: 0
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Selected Genes
|
|
||||||
</p>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: globals.leftSidebarSectionPadding
|
padding: globals.leftSidebarSectionPadding
|
||||||
@@ -208,7 +195,7 @@ class GeneExpression extends React.Component {
|
|||||||
this.setState({ tab: "autosuggest" });
|
this.setState({ tab: "autosuggest" });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Autosuggest
|
Autosuggest genes
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
active={tab === "bulkadd"}
|
active={tab === "bulkadd"}
|
||||||
@@ -260,7 +247,7 @@ class GeneExpression extends React.Component {
|
|||||||
data-testid={"add-gene"}
|
data-testid={"add-gene"}
|
||||||
loading={userDefinedGenesLoading}
|
loading={userDefinedGenesLoading}
|
||||||
>
|
>
|
||||||
Add
|
Add gene
|
||||||
</Button>
|
</Button>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -291,7 +278,7 @@ class GeneExpression extends React.Component {
|
|||||||
onClick={this.handleBulkAddClick.bind(this)}
|
onClick={this.handleBulkAddClick.bind(this)}
|
||||||
loading={userDefinedGenesLoading}
|
loading={userDefinedGenesLoading}
|
||||||
>
|
>
|
||||||
Add
|
Add genes
|
||||||
</Button>
|
</Button>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -318,15 +305,6 @@ class GeneExpression extends React.Component {
|
|||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p
|
|
||||||
style={Object.assign({}, globals.leftSidebarSectionHeading, {
|
|
||||||
marginTop: 40,
|
|
||||||
paddingLeft: globals.leftSidebarSectionPadding
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Differentially Expressed Genes
|
|
||||||
</p>
|
|
||||||
<ExpressionButtons />
|
|
||||||
{differential.diffExp
|
{differential.diffExp
|
||||||
? _.map(differential.diffExp, (value, index) => {
|
? _.map(differential.diffExp, (value, index) => {
|
||||||
const name = world.varAnnotations.at(value[0], varIndexName);
|
const name = world.varAnnotations.at(value[0], varIndexName);
|
||||||
|
|||||||
@@ -3,82 +3,28 @@ import React from "react";
|
|||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import mat4 from "gl-mat4";
|
import mat4 from "gl-mat4";
|
||||||
import vec3 from "gl-vec3";
|
|
||||||
import _regl from "regl";
|
import _regl from "regl";
|
||||||
import memoize from "memoize-one";
|
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 * as globals from "../../globals";
|
||||||
import setupSVGandBrushElements from "./setupSVGandBrush";
|
import setupSVGandBrushElements from "./setupSVGandBrush";
|
||||||
import actions from "../../actions";
|
|
||||||
import _camera from "../../util/camera";
|
import _camera from "../../util/camera";
|
||||||
import _drawPoints from "./drawPointsRegl";
|
import _drawPoints from "./drawPointsRegl";
|
||||||
import scaleLinear from "../../util/scaleLinear";
|
import scaleLinear from "../../util/scaleLinear";
|
||||||
import { World } from "../../util/stateManager";
|
|
||||||
|
|
||||||
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
|
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
world: state.world,
|
world: state.world,
|
||||||
universe: state.universe,
|
|
||||||
crossfilter: state.crossfilter,
|
crossfilter: state.crossfilter,
|
||||||
clipPercentileMin: Math.round(100 * (state.world?.clipQuantiles?.min ?? 0)),
|
|
||||||
clipPercentileMax: Math.round(100 * (state.world?.clipQuantiles?.max ?? 1)),
|
|
||||||
responsive: state.responsive,
|
responsive: state.responsive,
|
||||||
colorRGB: state.colors.rgb,
|
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,
|
selectionTool: state.graphSelection.tool,
|
||||||
currentSelection: state.graphSelection.selection,
|
currentSelection: state.graphSelection.selection,
|
||||||
layoutChoice: state.layoutChoice
|
layoutChoice: state.layoutChoice,
|
||||||
|
graphInteractionMode: state.controls.graphInteractionMode
|
||||||
}))
|
}))
|
||||||
class Graph extends React.Component {
|
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) => {
|
computePointPositions = memoize((X, Y, scaleX, scaleY) => {
|
||||||
/*
|
/*
|
||||||
compute webgl coordinate buffer for each point
|
compute webgl coordinate buffer for each point
|
||||||
@@ -115,7 +61,6 @@ class Graph extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
this.graphPaddingTop = 0;
|
this.graphPaddingTop = 0;
|
||||||
this.graphPaddingBottom = 45;
|
|
||||||
this.graphPaddingRight = globals.leftSidebarWidth;
|
this.graphPaddingRight = globals.leftSidebarWidth;
|
||||||
this.renderCache = {
|
this.renderCache = {
|
||||||
X: null,
|
X: null,
|
||||||
@@ -127,9 +72,7 @@ class Graph extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
svg: null,
|
svg: null,
|
||||||
tool: null,
|
tool: null,
|
||||||
container: null,
|
container: null
|
||||||
mode: "select",
|
|
||||||
pendingClipPercentiles: null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,10 +89,17 @@ class Graph extends React.Component {
|
|||||||
const sizeBuffer = regl.buffer();
|
const sizeBuffer = regl.buffer();
|
||||||
|
|
||||||
// preallocate coordinate system transformation between data and gl
|
// 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 = {
|
const transform = {
|
||||||
glScaleX: scaleLinear([0, 1], [-1 * fractionToUse, 1 * fractionToUse]),
|
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 */
|
/* 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 { renderCache } = this;
|
||||||
const {
|
const {
|
||||||
world,
|
world,
|
||||||
@@ -188,14 +138,30 @@ class Graph extends React.Component {
|
|||||||
responsive,
|
responsive,
|
||||||
selectionTool,
|
selectionTool,
|
||||||
currentSelection,
|
currentSelection,
|
||||||
layoutChoice
|
layoutChoice,
|
||||||
|
graphInteractionMode
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { reglRender, mode, regl, svg } = this.state;
|
const { reglRender, regl, svg } = this.state;
|
||||||
let stateChanges = {};
|
let stateChanges = {};
|
||||||
|
|
||||||
if (reglRender && this.reglRenderState === "rendering" && mode !== "zoom") {
|
if (reglRender) {
|
||||||
reglRender.cancel();
|
if (
|
||||||
this.reglRenderState = "paused";
|
// 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) {
|
if (regl && world) {
|
||||||
@@ -293,7 +259,7 @@ class Graph extends React.Component {
|
|||||||
*/
|
*/
|
||||||
if (
|
if (
|
||||||
currentSelection !== prevProps.currentSelection ||
|
currentSelection !== prevProps.currentSelection ||
|
||||||
mode !== prevState.mode ||
|
graphInteractionMode !== prevProps.graphInteractionMode ||
|
||||||
stateChanges.svg
|
stateChanges.svg
|
||||||
) {
|
) {
|
||||||
const { tool, container } = this.state;
|
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) {
|
brushToolUpdate(tool, container) {
|
||||||
/*
|
/*
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
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
|
this is called from componentDidUpdate(), so be very careful using
|
||||||
anything from this.state, which may be updated asynchronously.
|
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;
|
const scale = aspect < 1 ? 1 / aspect : 1;
|
||||||
|
|
||||||
// compute inverse view matrix
|
// 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
|
// variable names are choosen to reflect inverse of those used
|
||||||
// in mapScreenToPoint().
|
// in mapScreenToPoint().
|
||||||
@@ -769,367 +576,21 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { responsive, graphInteractionMode } = this.props;
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="graphWrapper">
|
<div id="graphWrapper">
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
right: 0,
|
|
||||||
top: 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 10,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "baseline"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
content="Show only metadata and cells which are currently selected"
|
|
||||||
position="left"
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
data-testid="subset-button"
|
|
||||||
disabled={
|
|
||||||
crossfilter &&
|
|
||||||
(crossfilter.countSelected() === 0 ||
|
|
||||||
crossfilter.countSelected() === crossfilter.size())
|
|
||||||
}
|
|
||||||
style={{ marginRight: 10 }}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(actions.regraph());
|
|
||||||
dispatch({ type: "increment graph render counter" });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
subset to current selection
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
content="Reset cellxgene, clearing all selections"
|
|
||||||
position="left"
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
disabled={this.isResetDisabled()}
|
|
||||||
type="button"
|
|
||||||
loading={resettingInterface}
|
|
||||||
intent="warning"
|
|
||||||
style={{ marginRight: 10 }}
|
|
||||||
onClick={this.resetInterface}
|
|
||||||
data-testid="reset"
|
|
||||||
data-testclass={`resetting-${resettingInterface}`}
|
|
||||||
>
|
|
||||||
reset
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
<div className="bp3-button-group">
|
|
||||||
<Tooltip content={selectionTooltip} position="left">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="mode-lasso"
|
|
||||||
className={`bp3-button ${selectionButtonClass}`}
|
|
||||||
active={mode === "select"}
|
|
||||||
onClick={() => {
|
|
||||||
this.setState({ mode: "select" });
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="Pan and zoom" position="left">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="mode-pan-zoom"
|
|
||||||
className="bp3-button bp3-icon-zoom-in"
|
|
||||||
active={mode === "zoom"}
|
|
||||||
onClick={() => {
|
|
||||||
this.restartReglLoop();
|
|
||||||
this.setState({ mode: "zoom" });
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bp3-button-group"
|
|
||||||
style={{
|
|
||||||
marginLeft: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip content="Undo" position="left">
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
className="bp3-button bp3-icon-undo"
|
|
||||||
disabled={undoDisabled}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({ type: "@@undoable/undo" });
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="Redo" position="left">
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
className="bp3-button bp3-icon-redo"
|
|
||||||
disabled={redoDisabled}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({ type: "@@undoable/redo" });
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="bp3-button-group"
|
|
||||||
style={{
|
|
||||||
marginLeft: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
target={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="layout-choice"
|
|
||||||
className="bp3-button bp3-icon-heatmap"
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
position={Position.BOTTOM_RIGHT}
|
|
||||||
content={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
flexDirection: "column",
|
|
||||||
padding: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RadioGroup
|
|
||||||
label="Layout Choice"
|
|
||||||
onChange={this.handleLayoutChoiceChange}
|
|
||||||
selectedValue={layoutChoice.current}
|
|
||||||
>
|
|
||||||
{layoutChoice.available.map(name => (
|
|
||||||
<Radio label={name} value={name} key={name} />
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="bp3-button-group"
|
|
||||||
style={{
|
|
||||||
marginLeft: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
target={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="visualization-settings"
|
|
||||||
className={`bp3-button bp3-icon-timeline-bar-chart ${activeClipClass}`}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
position={Position.BOTTOM_RIGHT}
|
|
||||||
onOpening={this.handleClipOpening}
|
|
||||||
onClosing={this.handleClipClosing}
|
|
||||||
content={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
flexDirection: "column",
|
|
||||||
padding: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>Clip all continuous values to percentile range</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingTop: 5,
|
|
||||||
paddingBottom: 5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NumericInput
|
|
||||||
style={{ width: 50 }}
|
|
||||||
data-testid={"clip-min-input"}
|
|
||||||
onValueChange={this.handleClipPercentileMinValueChange}
|
|
||||||
onKeyPress={this.handleClipOnKeyPress}
|
|
||||||
value={clipMin}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
fill={false}
|
|
||||||
minorStepSize={null}
|
|
||||||
rightElement={
|
|
||||||
<div style={{ padding: "4px 2px" }}>
|
|
||||||
<Icon
|
|
||||||
icon="percentage"
|
|
||||||
intent="primary"
|
|
||||||
iconSize={14}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span style={{ marginRight: 5, marginLeft: 5 }}> - </span>
|
|
||||||
<NumericInput
|
|
||||||
style={{ width: 50 }}
|
|
||||||
data-testid={"clip-max-input"}
|
|
||||||
onValueChange={this.handleClipPercentileMaxValueChange}
|
|
||||||
onKeyPress={this.handleClipOnKeyPress}
|
|
||||||
value={clipMax}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
fill={false}
|
|
||||||
minorStepSize={null}
|
|
||||||
rightElement={
|
|
||||||
<div style={{ padding: "4px 2px" }}>
|
|
||||||
<Icon
|
|
||||||
icon="percentage"
|
|
||||||
intent="primary"
|
|
||||||
iconSize={14}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="clip-commit"
|
|
||||||
className="bp3-button"
|
|
||||||
disabled={this.isClipDisabled()}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
marginRight: 5,
|
|
||||||
marginLeft: 5
|
|
||||||
}}
|
|
||||||
onClick={this.handleClipCommit}
|
|
||||||
>
|
|
||||||
Clip
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginLeft: 10 }} className="bp3-button-group">
|
|
||||||
<Popover
|
|
||||||
content={
|
|
||||||
<Menu>
|
|
||||||
<MenuItem
|
|
||||||
href="https://chanzuckerberg.github.io/cellxgene/faq.html"
|
|
||||||
target="_blank"
|
|
||||||
icon="help"
|
|
||||||
text="FAQ"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
href="https://join-cellxgene-users.herokuapp.com/"
|
|
||||||
target="_blank"
|
|
||||||
icon="chat"
|
|
||||||
text="Chat"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
|
||||||
target="_blank"
|
|
||||||
icon="book"
|
|
||||||
text="Docs"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
href="https://github.com/chanzuckerberg/cellxgene"
|
|
||||||
target="_blank"
|
|
||||||
icon="git-branch"
|
|
||||||
text="Github"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
target="_blank"
|
|
||||||
text={`cellxgene v${
|
|
||||||
libraryVersions && libraryVersions.cellxgene
|
|
||||||
? libraryVersions.cellxgene
|
|
||||||
: null
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<MenuItem text="MIT License" />
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
position={Position.BOTTOM_RIGHT}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bp3-button bp3-icon-info-sign"
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
zIndex: -9999,
|
zIndex: -9999,
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
top: 0,
|
top: this.graphPaddingTop,
|
||||||
right: 0
|
right: 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: mode === "select" ? "inherit" : "none"
|
display: graphInteractionMode === "select" ? "inherit" : "none"
|
||||||
}}
|
}}
|
||||||
id="graphAttachPoint"
|
id="graphAttachPoint"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
backgroundColor: "white",
|
||||||
|
/* x y blur spread color */
|
||||||
|
boxShadow: "-3px 0px 6px 2px rgba(153,153,153,0.4)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TopLeftLogoAndTitle />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: responsive.height - logoRelatedPadding,
|
||||||
|
marginTop: logoRelatedPadding,
|
||||||
|
width: globals.leftSidebarWidth,
|
||||||
|
overflowY: "auto",
|
||||||
|
overflowX: "hidden"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Categorical />
|
||||||
|
<GeneExpression />
|
||||||
|
<Continuous />
|
||||||
|
</div>
|
||||||
|
{scatterplotXXaccessor && scatterplotYYaccessor ? (
|
||||||
|
<DynamicScatterplot />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeftSideBar;
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingLeft: 8,
|
||||||
|
paddingTop: 8,
|
||||||
|
width: globals.leftSidebarWidth - paddingToAvoidScrollBar,
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: "white",
|
||||||
|
zIndex: 8888
|
||||||
|
/* x y blur spread color */
|
||||||
|
// boxShadow: "-5px -1px 4px 2px rgba(225,225,225,0.4)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logo size={30} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 28,
|
||||||
|
position: "relative",
|
||||||
|
top: -6,
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginLeft: 5,
|
||||||
|
color: globals.logoColor,
|
||||||
|
userSelect: "none"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
cell<span
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
top: 1,
|
||||||
|
fontWeight: 300,
|
||||||
|
fontSize: 24
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</span>gene
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
data-testid="header"
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
position: "relative",
|
||||||
|
marginLeft: 7,
|
||||||
|
top: -8
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{datasetTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeftSideBar;
|
||||||
@@ -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 (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
backgroundColor: "white",
|
|
||||||
/* x y blur spread color */
|
|
||||||
boxShadow: "1px 0px 6px 2px rgba(153,153,153,0.4)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: globals.cellxgeneTitleTopPadding,
|
|
||||||
left: globals.leftSidebarWidth + globals.cellxgeneTitleLeftPadding,
|
|
||||||
margin: 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Logo size={32} />
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 28,
|
|
||||||
position: "relative",
|
|
||||||
top: -4,
|
|
||||||
fontWeight: "bold",
|
|
||||||
marginLeft: 5,
|
|
||||||
color: globals.logoColor,
|
|
||||||
userSelect: "none"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
cell<span
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
top: 1,
|
|
||||||
fontWeight: 300,
|
|
||||||
fontSize: 24
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</span>gene
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-testid="header"
|
|
||||||
style={{
|
|
||||||
fontSize: 16,
|
|
||||||
display: "block",
|
|
||||||
position: "relative",
|
|
||||||
marginTop: 10,
|
|
||||||
top: -4
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{datasetTitle}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: responsive.height - metadataSectionPadding,
|
|
||||||
width: globals.leftSidebarWidth,
|
|
||||||
overflowY: "auto",
|
|
||||||
overflowX: "hidden"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Categorical />
|
|
||||||
<GeneExpression />
|
|
||||||
<Continuous />
|
|
||||||
</div>
|
|
||||||
{scatterplotXXaccessor && scatterplotYYaccessor ? (
|
|
||||||
<DynamicScatterplot />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeftSideBar;
|
|
||||||
+2
-3
@@ -32,7 +32,7 @@ class CellSetButton extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { differential, eitherCellSetOneOrTwo } = this.props;
|
const { differential, eitherCellSetOneOrTwo } = this.props;
|
||||||
const cellListName = `celllist${eitherCellSetOneOrTwo}`;
|
const cellListName = `celllist${eitherCellSetOneOrTwo}`;
|
||||||
let cells_selected = differential[cellListName]
|
const cellsSelected = differential[cellListName]
|
||||||
? differential[cellListName].length
|
? differential[cellListName].length
|
||||||
: 0;
|
: 0;
|
||||||
return (
|
return (
|
||||||
@@ -41,7 +41,6 @@ class CellSetButton extends React.Component {
|
|||||||
position="top"
|
position="top"
|
||||||
>
|
>
|
||||||
<AnchorButton
|
<AnchorButton
|
||||||
style={{ marginRight: 10 }}
|
|
||||||
type="button"
|
type="button"
|
||||||
disabled={differential.diffExp}
|
disabled={differential.diffExp}
|
||||||
onClick={this.set.bind(this)}
|
onClick={this.set.bind(this)}
|
||||||
@@ -50,7 +49,7 @@ class CellSetButton extends React.Component {
|
|||||||
{eitherCellSetOneOrTwo}
|
{eitherCellSetOneOrTwo}
|
||||||
{": "}
|
{": "}
|
||||||
<span data-testid={`cellset-count-${eitherCellSetOneOrTwo}`}>
|
<span data-testid={`cellset-count-${eitherCellSetOneOrTwo}`}>
|
||||||
{cells_selected}
|
{cellsSelected}
|
||||||
</span>
|
</span>
|
||||||
{" cells"}
|
{" cells"}
|
||||||
</AnchorButton>
|
</AnchorButton>
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className="bp3-button-group"
|
||||||
|
style={{
|
||||||
|
marginLeft: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
target={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
data-testid="visualization-settings"
|
||||||
|
className={`bp3-button bp3-icon-timeline-bar-chart ${activeClipClass}`}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={Position.BOTTOM_RIGHT}
|
||||||
|
onOpening={handleClipOpening}
|
||||||
|
onClosing={handleClipClosing}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>Clip all continuous values to percentile range</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NumericInput
|
||||||
|
style={{ width: 50 }}
|
||||||
|
data-testid="clip-min-input"
|
||||||
|
onValueChange={handleClipPercentileMinValueChange}
|
||||||
|
onKeyPress={handleClipOnKeyPress}
|
||||||
|
value={clipMin}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
fill={false}
|
||||||
|
minorStepSize={null}
|
||||||
|
rightElement={
|
||||||
|
<div style={{ padding: "4px 2px" }}>
|
||||||
|
<Icon icon="percentage" intent="primary" iconSize={14} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span style={{ marginRight: 5, marginLeft: 5 }}> - </span>
|
||||||
|
<NumericInput
|
||||||
|
style={{ width: 50 }}
|
||||||
|
data-testid="clip-max-input"
|
||||||
|
onValueChange={handleClipPercentileMaxValueChange}
|
||||||
|
onKeyPress={handleClipOnKeyPress}
|
||||||
|
value={clipMax}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
fill={false}
|
||||||
|
minorStepSize={null}
|
||||||
|
rightElement={
|
||||||
|
<div style={{ padding: "4px 2px" }}>
|
||||||
|
<Icon icon="percentage" intent="primary" iconSize={14} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
data-testid="clip-commit"
|
||||||
|
className="bp3-button"
|
||||||
|
disabled={isClipDisabled()}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
marginRight: 5,
|
||||||
|
marginLeft: 5
|
||||||
|
}}
|
||||||
|
onClick={handleClipCommit}
|
||||||
|
>
|
||||||
|
Clip
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Clip;
|
||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
right: 8,
|
||||||
|
top: 8
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="bp3-button-group" style={{ marginRight: 10 }}>
|
||||||
|
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
|
||||||
|
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
|
||||||
|
{!differential.diffExp ? (
|
||||||
|
<Tooltip
|
||||||
|
content="Add two cells selections, see the top 15 differentially expressed genes between them"
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<AnchorButton
|
||||||
|
disabled={!haveBothCellSets}
|
||||||
|
intent="primary"
|
||||||
|
data-testid="diffexp-button"
|
||||||
|
loading={differential.loading}
|
||||||
|
icon="left-join"
|
||||||
|
fill
|
||||||
|
type="button"
|
||||||
|
onClick={this.computeDiffExp}
|
||||||
|
>
|
||||||
|
Compute Differential Expression
|
||||||
|
</AnchorButton>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{differential.diffExp ? (
|
||||||
|
<Tooltip
|
||||||
|
content="Remove differentially expressed gene list and clear cell selections"
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
fill
|
||||||
|
intent="warning"
|
||||||
|
onClick={this.clearDifferentialExpression}
|
||||||
|
>
|
||||||
|
Clear Differential Expression
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
content="Show only metadata and cells which are currently selected"
|
||||||
|
position="left"
|
||||||
|
>
|
||||||
|
<AnchorButton
|
||||||
|
type="button"
|
||||||
|
data-testid="subset-button"
|
||||||
|
disabled={
|
||||||
|
crossfilter &&
|
||||||
|
(crossfilter.countSelected() === 0 ||
|
||||||
|
crossfilter.countSelected() === crossfilter.size())
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(actions.regraph());
|
||||||
|
dispatch({ type: "increment graph render counter" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
subset to current selection
|
||||||
|
</AnchorButton>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="bp3-button-group">
|
||||||
|
<Tooltip content={selectionTooltip} position="left">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
data-testid="mode-lasso"
|
||||||
|
className={`bp3-button ${selectionButtonClass}`}
|
||||||
|
active={graphInteractionMode === "select"}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({
|
||||||
|
type: "change graph interaction mode",
|
||||||
|
data: "select"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Pan and zoom" position="left">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
data-testid="mode-pan-zoom"
|
||||||
|
className="bp3-button bp3-icon-zoom-in"
|
||||||
|
active={graphInteractionMode === "zoom"}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({
|
||||||
|
type: "change graph interaction mode",
|
||||||
|
data: "zoom"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="bp3-button-group"
|
||||||
|
style={{
|
||||||
|
marginLeft: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
target={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
data-testid="layout-choice"
|
||||||
|
className="bp3-button bp3-icon-heatmap"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={Position.BOTTOM_RIGHT}
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: 10
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
label="Layout Choice"
|
||||||
|
onChange={this.handleLayoutChoiceChange}
|
||||||
|
selectedValue={layoutChoice.current}
|
||||||
|
>
|
||||||
|
{layoutChoice.available.map(name => (
|
||||||
|
<Radio label={name} value={name} key={name} />
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Clip
|
||||||
|
pendingClipPercentiles={pendingClipPercentiles}
|
||||||
|
clipPercentileMin={clipPercentileMin}
|
||||||
|
clipPercentileMax={clipPercentileMax}
|
||||||
|
handleClipOpening={this.handleClipOpening}
|
||||||
|
handleClipClosing={this.handleClipClosing}
|
||||||
|
handleClipCommit={this.handleClipCommit}
|
||||||
|
isClipDisabled={this.isClipDisabled}
|
||||||
|
handleClipOnKeyPress={this.handleClipOnKeyPress}
|
||||||
|
handleClipPercentileMaxValueChange={
|
||||||
|
this.handleClipPercentileMaxValueChange
|
||||||
|
}
|
||||||
|
handleClipPercentileMinValueChange={
|
||||||
|
this.handleClipPercentileMinValueChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<UndoRedoReset
|
||||||
|
dispatch={dispatch}
|
||||||
|
isResetDisabled={this.isResetDisabled}
|
||||||
|
resetInterface={this.resetInterface}
|
||||||
|
resettingInterface={resettingInterface}
|
||||||
|
undoDisabled={undoDisabled}
|
||||||
|
redoDisabled={redoDisabled}
|
||||||
|
/>
|
||||||
|
<InformationMenu libraryVersions={libraryVersions} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MenuBar;
|
||||||
@@ -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 (
|
||||||
|
<div style={{ marginLeft: 10 }} className="bp3-button-group">
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
href="https://chanzuckerberg.github.io/cellxgene/faq.html"
|
||||||
|
target="_blank"
|
||||||
|
icon="help"
|
||||||
|
text="FAQ"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
href="https://join-cellxgene-users.herokuapp.com/"
|
||||||
|
target="_blank"
|
||||||
|
icon="chat"
|
||||||
|
text="Chat"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
href="https://chanzuckerberg.github.io/cellxgene/"
|
||||||
|
target="_blank"
|
||||||
|
icon="book"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
href="https://github.com/chanzuckerberg/cellxgene"
|
||||||
|
target="_blank"
|
||||||
|
icon="git-branch"
|
||||||
|
text="Github"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
target="_blank"
|
||||||
|
text={`cellxgene v${
|
||||||
|
libraryVersions && libraryVersions.cellxgene
|
||||||
|
? libraryVersions.cellxgene
|
||||||
|
: null
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<MenuItem text="MIT License" />
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
position={Position.BOTTOM_RIGHT}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="bp3-button bp3-icon-info-sign"
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InformationMenu;
|
||||||
@@ -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 (
|
||||||
|
<div style={{ marginLeft: 10 }} className="bp3-button-group">
|
||||||
|
<Tooltip content="Undo" position="left">
|
||||||
|
<AnchorButton
|
||||||
|
type="button"
|
||||||
|
className="bp3-button bp3-icon-undo"
|
||||||
|
disabled={undoDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: "@@undoable/undo" });
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Redo" position="left">
|
||||||
|
<AnchorButton
|
||||||
|
type="button"
|
||||||
|
className="bp3-button bp3-icon-redo"
|
||||||
|
disabled={redoDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch({ type: "@@undoable/redo" });
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content="Reset cellxgene, clearing all selections"
|
||||||
|
position="left"
|
||||||
|
>
|
||||||
|
<AnchorButton
|
||||||
|
disabled={isResetDisabled()}
|
||||||
|
style={{ marginLeft: 10 }}
|
||||||
|
type="button"
|
||||||
|
loading={resettingInterface}
|
||||||
|
intent="none"
|
||||||
|
icon="refresh"
|
||||||
|
onClick={resetInterface}
|
||||||
|
data-testid="reset"
|
||||||
|
data-testclass={`resetting-${resettingInterface}`}
|
||||||
|
>
|
||||||
|
reset
|
||||||
|
</AnchorButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InformationMenu;
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
|
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Button, ButtonGroup } from "@blueprintjs/core";
|
import { Button, ButtonGroup } from "@blueprintjs/core";
|
||||||
import _regl from "regl";
|
import _regl from "regl";
|
||||||
|
|||||||
@@ -73,114 +73,3 @@ let _API = {
|
|||||||
|
|
||||||
if (window.CELLXGENE && window.CELLXGENE.API) _API = window.CELLXGENE.API;
|
if (window.CELLXGENE && window.CELLXGENE.API) _API = window.CELLXGENE.API;
|
||||||
export const API = _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"
|
|
||||||
];
|
|
||||||
|
|||||||
Vendored
+6
-1
@@ -16,7 +16,7 @@ const Controls = (
|
|||||||
diffexpGenes: [],
|
diffexpGenes: [],
|
||||||
|
|
||||||
resettingInterface: false,
|
resettingInterface: false,
|
||||||
|
graphInteractionMode: "select",
|
||||||
opacityForDeselectedCells: 0.2,
|
opacityForDeselectedCells: 0.2,
|
||||||
scatterplotXXaccessor: null, // just easier to read
|
scatterplotXXaccessor: null, // just easier to read
|
||||||
scatterplotYYaccessor: null,
|
scatterplotYYaccessor: null,
|
||||||
@@ -138,6 +138,11 @@ const Controls = (
|
|||||||
/*******************************
|
/*******************************
|
||||||
User Events
|
User Events
|
||||||
*******************************/
|
*******************************/
|
||||||
|
case "change graph interaction mode":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
graphInteractionMode: action.data
|
||||||
|
};
|
||||||
case "change opacity deselected cells in 2d graph background":
|
case "change opacity deselected cells in 2d graph background":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ const saveOnActions = new Set([
|
|||||||
"set World to current selection",
|
"set World to current selection",
|
||||||
"set clip quantiles",
|
"set clip quantiles",
|
||||||
|
|
||||||
"set layout choice"
|
"set layout choice",
|
||||||
|
"change graph interaction mode"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user