Undo selection appends genes from differential expression to user gene list (#1183)

* Undo selection appends diffExp genes to user gene list

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1171

Need: When a user performs a differential expression from within
a sub-selection (world) of the data and then resets the selection to all
cells (universe), the differential expression results are no longer
valid.

Approach:
* When the selection is reset, move the top (maxUserDefinedGenes
- len(userDefinedGenes) from the differential expression results to the
list of user defined genes
* Raise maxUserDefinedGenes to 25 to give users more room and
accommodate the extra genes transferred in from differential expression

Other commits:
* Choose different button icons
* Add diff exp genes to user defined genes on subset too
* Respond to feedback from @liaprins-czi and @bkmartinjr
This commit is contained in:
Matt Weiden
2020-03-02 12:07:12 -08:00
committed by GitHub
parent 87efcc0e99
commit 05323ae643
15 changed files with 231 additions and 246 deletions
+47 -59
View File
@@ -20,9 +20,7 @@ beforeEach(async () => {
}); });
afterAll(() => { afterAll(() => {
if (!DEBUG) { browser.close();
browser.close();
}
}); });
describe("did launch", () => { describe("did launch", () => {
@@ -111,16 +109,10 @@ describe("cell selection", () => {
describe("gene entry", () => { describe("gene entry", () => {
test("search for single gene", async () => { test("search for single gene", async () => {
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this await cxgActions.addGeneToSearch(data.genes.search);
await utils.typeInto("gene-search", data.genes.search);
await page.keyboard.press("Enter");
await page.waitForSelector(
`[data-testid='histogram-${data.genes.search}']`
);
}); });
test("bulk add genes", async () => { test("bulk add genes", async () => {
await cxgActions.reset();
const testGenes = data.genes.bulkadd; const testGenes = data.genes.bulkadd;
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(","));
@@ -135,21 +127,9 @@ describe("gene entry", () => {
}); });
}); });
describe("diffexp", () => { describe("differential expression", () => {
test("selects cells, saves them and performs diffexp", async () => { test("selects cells, saves them and performs diffexp", async () => {
for (const select of data.diffexp.cellset1) { await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await cxgActions.cellSet(1);
for (const select of data.diffexp.cellset2) {
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await cxgActions.cellSet(2);
await utils.clickOn("diffexp-button");
const allHistograms = await cxgActions.getAllHistograms( const allHistograms = await cxgActions.getAllHistograms(
"histogram-diffexp", "histogram-diffexp",
data.diffexp["gene-results"] data.diffexp["gene-results"]
@@ -161,7 +141,7 @@ describe("diffexp", () => {
}); });
}); });
describe("subset/reset", () => { describe("subset", () => {
test("subset - cell count matches", async () => { test("subset - cell count matches", async () => {
for (const select of data.subset.cellset1) { for (const select of data.subset.cellset1) {
if (select.kind === "categorical") { if (select.kind === "categorical") {
@@ -180,39 +160,6 @@ describe("subset/reset", () => {
} }
}); });
test("reset after subset", async () => {
for (const select of data.subset.cellset1) {
if (select.kind === "categorical") {
await cxgActions.selectCategory(select.metadata, select.values, true);
}
}
await utils.clickOn("subset-button");
for (const label in data.subset.categorical) {
const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.subset.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.subset.categorical[label])
);
}
await cxgActions.reset();
for (const label in data.categorical) {
await utils.waitByID(`category-${label}`);
const categoryName = await utils.getOneElementInnerText(
`[data-testid="category-${label}"]`
);
expect(categoryName).toMatch(label);
const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.categorical[label])
);
}
});
test("lasso after subset", async () => { test("lasso after subset", async () => {
for (const select of data.subset.cellset1) { for (const select of data.subset.cellset1) {
if (select.kind === "categorical") { if (select.kind === "categorical") {
@@ -233,11 +180,52 @@ describe("subset/reset", () => {
const cellCount = await cxgActions.cellSet(1); const cellCount = await cxgActions.cellSet(1);
expect(cellCount).toBe(data.subset.lasso.count); expect(cellCount).toBe(data.subset.lasso.count);
}); });
test("undo selection appends the top diff exp genes to user defined genes", async () => {
const userDefinedGenes = ["ACD", "AAR2", "AATF", "ARSG"];
const diffExpGenes = data.diffexp["gene-results"];
for (const userDefinedGene of userDefinedGenes) {
await cxgActions.addGeneToSearch(userDefinedGene);
}
const userDefinedHistograms = await cxgActions.getAllHistograms("histogram-user-gene", userDefinedGenes);
expect(userDefinedHistograms).toStrictEqual(userDefinedGenes);
await cxgActions.subset({x1: 0.15, y1: 0.10, x2: 0.98, y2: 0.98});
await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
const diffExpHistograms = await cxgActions.getAllHistograms("histogram-diffexp", diffExpGenes);
expect(diffExpHistograms).toStrictEqual(diffExpGenes);
await utils.clickOn("reset-subset-button");
const expected = [].concat(userDefinedGenes, diffExpGenes);
const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms(
"histogram-user-gene",
expected
);
expect(userDefinedHistogramsAfterSubset).toStrictEqual(expected);
});
test("subset selection appends the top diff exp genes to user defined genes", async () => {
const userDefinedGenes = ["ACD", "AAR2", "AATF", "ARSG"];
const diffExpGenes = data.diffexp["gene-results"];
for (const userDefinedGene of userDefinedGenes) {
await cxgActions.addGeneToSearch(userDefinedGene);
}
const userDefinedHistograms = await cxgActions.getAllHistograms("histogram-user-gene", userDefinedGenes);
expect(userDefinedHistograms).toStrictEqual(userDefinedGenes);
await cxgActions.subset({x1: 0.15, y1: 0.10, x2: 0.98, y2: 0.98});
await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
const diffExpHistograms = await cxgActions.getAllHistograms("histogram-diffexp", diffExpGenes);
expect(diffExpHistograms).toStrictEqual(diffExpGenes);
await cxgActions.subset({x1: 0.16, y1: 0.11, x2: 0.97, y2: 0.97});
const expected = [].concat(userDefinedGenes, diffExpGenes);
const userDefinedHistogramsAfterSubset = await cxgActions.getAllHistograms(
"histogram-user-gene",
expected
);
expect(userDefinedHistogramsAfterSubset).toStrictEqual(expected);
});
}); });
describe("scatter plot", () => { describe("scatter plot", () => {
test("scatter plot appears", async () => { test("scatter plot appears", async () => {
await cxgActions.reset();
const testGenes = data.scatter.genes; const testGenes = data.scatter.genes;
await utils.clickOn("section-bulk-add"); await utils.clickOn("section-bulk-add");
await utils.typeInto("input-bulk-add", Object.values(testGenes).join(",")); await utils.typeInto("input-bulk-add", Object.values(testGenes).join(","));
+1 -17
View File
@@ -22,22 +22,6 @@ describe.each([
{withSubset: false, tag: "whole"} {withSubset: false, tag: "whole"}
])("annotations", (config) => { ])("annotations", (config) => {
async function subset() {
const lassoSelection = await cxgActions.calcDragCoordinates(
"layout-graph",
{x1: 0.10, y1: 0.10, x2: 0.80, y2: 0.80}
);
await cxgActions.drag(
"layout-graph",
lassoSelection.start,
lassoSelection.end,
true
);
await utils.clickOn("subset-button");
const coordinate = await cxgActions.calcCoordinate("layout-graph", 0.9, 0.9);
await cxgActions.clickOnCoordinate("layout-graph", coordinate);
}
const perTestCategoryName = "per-test-category"; const perTestCategoryName = "per-test-category";
const perTestLabelName = "per-test-label"; const perTestLabelName = "per-test-label";
@@ -48,7 +32,7 @@ describe.each([
// setup the test fixtures // setup the test fixtures
await cxgActions.createCategory(perTestCategoryName); await cxgActions.createCategory(perTestCategoryName);
await cxgActions.createLabel(perTestCategoryName, perTestLabelName); await cxgActions.createLabel(perTestCategoryName, perTestLabelName);
if (config.withSubset) await subset(); if (config.withSubset) await cxgActions.subset({x1: 0.10, y1: 0.10, x2: 0.80, y2: 0.80});
await utils.waitByClass("autosave-complete"); await utils.waitByClass("autosave-complete");
}); });
+46 -17
View File
@@ -1,5 +1,6 @@
import {DEBUG, DEV} from "./config"; import {DEBUG, DEV} from "./config";
import puppeteer from "puppeteer"; import puppeteer from "puppeteer";
import { strict as assert } from "assert";
export const puppeteerUtils = puppeteerPage => ({ export const puppeteerUtils = puppeteerPage => ({
async waitByID(testid, props = {}) { async waitByID(testid, props = {}) {
@@ -16,7 +17,7 @@ export const puppeteerUtils = puppeteerPage => ({
); );
}, },
async waitForAllByIds(testids, props = {}) { async waitForAllByIds(testids) {
await Promise.all( await Promise.all(
testids.map(testid => testids.map(testid =>
puppeteerPage.waitForSelector(`[data-testid='${testid}']`) puppeteerPage.waitForSelector(`[data-testid='${testid}']`)
@@ -24,19 +25,16 @@ export const puppeteerUtils = puppeteerPage => ({
); );
}, },
async getAllByClass(testclass, props = {}) { async getAllByClass(testclass) {
const elements = await puppeteerPage.$$eval( const elements = await puppeteerPage.$$eval(
`[data-testclass=${testclass}]`, `[data-testclass=${testclass}]`,
els => { eles => eles.map(ele => ele.dataset.testid)
return els.map(el => {
return el.dataset.testid;
});
}
); );
return elements; return elements;
}, },
async typeInto(testid, text) { async typeInto(testid, text) {
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
// only works for text without special characters // only works for text without special characters
await this.waitByID(testid); await this.waitByID(testid);
const selector = `[data-testid='${testid}']`; const selector = `[data-testid='${testid}']`;
@@ -80,6 +78,7 @@ export const puppeteerUtils = puppeteerPage => ({
}); });
export const cellxgeneActions = puppeteerPage => ({ export const cellxgeneActions = puppeteerPage => ({
async drag(testid, start, end, lasso = false) { async drag(testid, start, end, lasso = false) {
const layout = await puppeteerUtils(puppeteerPage).waitByID(testid); const layout = await puppeteerUtils(puppeteerPage).waitByID(testid);
const elBox = await layout.boxModel(); const elBox = await layout.boxModel();
@@ -116,7 +115,7 @@ export const cellxgeneActions = puppeteerPage => ({
testclass testclass
); );
return allHistograms.map(hist => return allHistograms.map(hist =>
hist.substr("histogram_".length, hist.length) hist.substr("histogram-".length, hist.length)
); );
}, },
@@ -196,9 +195,7 @@ export const cellxgeneActions = puppeteerPage => ({
await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-expand`); await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-expand`);
await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-select`); await puppeteerUtils(puppeteerPage).clickOn(`${category}:category-select`);
for (const val of values) { for (const val of values) {
await puppeteerUtils(puppeteerPage).clickOn( await puppeteerUtils(puppeteerPage).clickOn(`categorical-value-select-${category}-${val}`);
`categorical-value-select-${category}-${val}`
);
} }
}, },
@@ -210,12 +207,6 @@ export const cellxgeneActions = puppeteerPage => ({
} }
}, },
async reset() {
await puppeteerUtils(puppeteerPage).clickOn("reset");
// loading state never actually happens, reset is too fast
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( await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
@@ -269,6 +260,44 @@ export const cellxgeneActions = puppeteerPage => ({
newLabelName newLabelName
); );
await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`); await puppeteerUtils(puppeteerPage).clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`);
},
async addGeneToSearch(geneName) {
await puppeteerUtils(puppeteerPage).typeInto("gene-search", geneName);
await puppeteerPage.keyboard.press("Enter");
await puppeteerPage.waitForSelector(
`[data-testid='histogram-${geneName}']`
);
},
async subset(coordinatesAsPercent) {
// In order to deselect the selection after the subset, make sure we have some clear part
// of the scatterplot we can click on
assert(coordinatesAsPercent.x2 < 0.99 || coordinatesAsPercent.y2 < 0.99);
const lassoSelection = await this.calcDragCoordinates( "layout-graph", coordinatesAsPercent);
await this.drag("layout-graph", lassoSelection.start, lassoSelection.end, true );
await puppeteerUtils(puppeteerPage).clickOn("subset-button");
const clearCoordinate = await this.calcCoordinate(
"layout-graph",
0.5,
0.99
);
await this.clickOnCoordinate("layout-graph", clearCoordinate);
},
async setSellSet(cellSet, cellSetNum) {
for (const selection of cellSet) {
if (selection.kind === "categorical") {
await this.selectCategory(selection.metadata, selection.values, true);
}
}
await this.cellSet(cellSetNum);
},
async runDiffExp(cellSet1, cellSet2) {
await this.setSellSet(cellSet1, 1);
await this.setSellSet(cellSet2, 2);
await puppeteerUtils(puppeteerPage).clickOn("diffexp-button");
} }
}); });
@@ -0,0 +1,24 @@
/*
test controls helpers
*/
import { subsetAndResetGeneLists } from "../../../src/util/stateManager/controlsHelpers";
import * as globals from "../../../src/globals";
describe("controls helpers", () => {
test("subsetAndResetGeneLists", () => {
const geneList = [...Array(150).keys()].map(() =>
Math.random().toString(36).substring(2, 6) // random string of 4 characters
);
const state = {
userDefinedGenes: geneList.slice(0, 20),
diffexpGenes: geneList.slice(20),
};
const [newUserDefinedGenes, newDiffExpGenes] = subsetAndResetGeneLists(state);
expect(globals.maxUserDefinedGenes).toBeLessThan(globals.maxGenes);
expect(geneList.length).toBeGreaterThan(globals.maxGenes);
expect(newUserDefinedGenes).toHaveLength(globals.maxGenes);
expect(newUserDefinedGenes).toStrictEqual(geneList.slice(0, globals.maxGenes));
expect(newDiffExpGenes).toStrictEqual([]);
});
});
+9 -34
View File
@@ -149,7 +149,7 @@ const doInitialDataLoad = () =>
Set the view (world) to current selection. Placeholder for an async action Set the view (world) to current selection. Placeholder for an async action
which also does re-layout. which also does re-layout.
*/ */
const regraph = () => (dispatch, getState) => { const setWorldToSelection = () => (dispatch, getState) => {
const { universe, world, crossfilter } = getState(); const { universe, world, crossfilter } = getState();
dispatch({ dispatch({
type: "set World to current selection", type: "set World to current selection",
@@ -167,7 +167,7 @@ const dispatchExpressionErrors = (dispatch, res) => {
}; };
/* /*
Fetch expression vectors for each gene in genes. This is NOT an action Fetch expression vectors for each gene in genes. This is NOT an action
function, but rather a helper to be called from an action helper that function, but rather a helper to be called from an action helper that
needs expression data. needs expression data.
@@ -385,37 +385,12 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
} }
}; };
const resetInterface = () => (dispatch, getState) => { const resetWorldToUniverse = () => (dispatch, getState) => {
const { universe } = getState(); const { universe } = getState();
dispatch({
type: "user reset start"
});
dispatch({
type: "clear all user defined genes"
});
dispatch({
type: "clear differential expression"
});
dispatch({
type: "reset colorscale"
});
dispatch({
type: "reset centroid labels"
});
dispatch({
type: "clear scatterplot"
});
dispatch({ dispatch({
type: "reset World to eq Universe", type: "reset World to eq Universe",
universe universe
}); });
dispatch({
type: "increment graph render counter"
});
dispatch({
type: "user reset end"
});
}; };
const saveObsAnnotations = () => async (dispatch, getState) => { const saveObsAnnotations = () => async (dispatch, getState) => {
@@ -472,11 +447,11 @@ const saveObsAnnotations = () => async (dispatch, getState) => {
}; };
export default { export default {
regraph,
resetInterface,
requestSingleGeneExpressionCountsForColoringPOST,
requestDifferentialExpression,
requestUserDefinedGene,
doInitialDataLoad, doInitialDataLoad,
saveObsAnnotations requestDifferentialExpression,
requestSingleGeneExpressionCountsForColoringPOST,
requestUserDefinedGene,
resetWorldToUniverse,
saveObsAnnotations,
setWorldToSelection,
}; };
@@ -182,9 +182,9 @@ class GeneExpression extends React.Component {
const gene = g.target; const gene = g.target;
if (userDefinedGenes.indexOf(gene) !== -1) { if (userDefinedGenes.indexOf(gene) !== -1) {
postUserErrorToast("That gene already exists"); postUserErrorToast("That gene already exists");
} else if (userDefinedGenes.length > 15) { } else if (userDefinedGenes.length > globals.maxUserDefinedGenes) {
postUserErrorToast( postUserErrorToast(
"That's too many genes, you can have at most 15 user defined genes" `That's too many genes, you can have at most ${globals.maxUserDefinedGenes} user defined genes`
); );
} else if ( } else if (
world.varAnnotations.col(varIndexName).indexOf(gene) === undefined world.varAnnotations.col(varIndexName).indexOf(gene) === undefined
+31 -90
View File
@@ -10,22 +10,20 @@ import {
Position, Position,
RadioGroup, RadioGroup,
Radio, Radio,
Icon
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import { World } from "../../util/stateManager"; import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
import CellSetButton from "./cellSetButtons"; import CellSetButton from "./cellSetButtons";
import InformationMenu from "./infoMenu";
import UndoRedoReset from "./undoRedoReset";
import Clip from "./clip"; import Clip from "./clip";
import * as globals from "../../globals"; import InformationMenu from "./infoMenu";
import Subset from "./subset";
import UndoRedoReset from "./undoRedo";
@connect(state => ({ @connect(state => ({
universe: state.universe, universe: state.universe,
world: state.world, world: state.world,
crossfilter: state.crossfilter, crossfilter: state.crossfilter,
differential: state.differential, differential: state.differential,
resettingInterface: state.controls.resettingInterface,
layoutChoice: state.layoutChoice, layoutChoice: state.layoutChoice,
graphInteractionMode: state.controls.graphInteractionMode, graphInteractionMode: state.controls.graphInteractionMode,
clipPercentileMin: Math.round(100 * (state.world?.clipQuantiles?.min ?? 0)), clipPercentileMin: Math.round(100 * (state.world?.clipQuantiles?.min ?? 0)),
@@ -99,61 +97,6 @@ class MenuBar extends React.Component {
return isDisabled; 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 => { handleClipOnKeyPress = e => {
/* /*
allow only numbers, plus other critical keys which allow only numbers, plus other critical keys which
@@ -268,6 +211,20 @@ class MenuBar extends React.Component {
}); });
}; };
subsetPossible = () => {
const { crossfilter } = this.props;
return (
crossfilter.countSelected() !== 0 &&
crossfilter.countSelected() !== crossfilter.size()
);
};
subsetResetPossible = () => {
const { world, universe } = this.props;
return world.nObs !== universe.nObs;
};
renderDiffExp() { renderDiffExp() {
/* diffexp-related buttons may be disabled */ /* diffexp-related buttons may be disabled */
const { disableDiffexp, differential, diffexpMayBeSlow } = this.props; const { disableDiffexp, differential, diffexpMayBeSlow } = this.props;
@@ -332,8 +289,6 @@ class MenuBar extends React.Component {
render() { render() {
const { const {
dispatch, dispatch,
crossfilter,
resettingInterface,
libraryVersions, libraryVersions,
undoDisabled, undoDisabled,
redoDisabled, redoDisabled,
@@ -368,30 +323,19 @@ class MenuBar extends React.Component {
}} }}
> >
{this.renderDiffExp()} {this.renderDiffExp()}
<Tooltip <Subset
content="Show only metadata and cells which are currently selected" subsetPossible={this.subsetPossible()}
position="bottom" subsetResetPossible={this.subsetResetPossible()}
hoverOpenDelay={globals.tooltipHoverOpenDelay} handleSubset={() => {
> dispatch(actions.setWorldToSelection());
<AnchorButton dispatch({ type: "increment graph render counter" });
data-testid="subset-button" }}
disabled={ handleSubsetReset={() => {
crossfilter && dispatch(actions.resetWorldToUniverse());
(crossfilter.countSelected() === 0 || dispatch({ type: "increment graph render counter" });
crossfilter.countSelected() === crossfilter.size()) }}
} />
style={{ <ButtonGroup style={{marginRight: "10px"}}>
marginRight: 10
}}
onClick={() => {
dispatch(actions.regraph());
dispatch({ type: "increment graph render counter" });
}}
>
<Icon icon="double-chevron-down" />
</AnchorButton>
</Tooltip>
<ButtonGroup style={{ marginRight: "10px" }}>
<Tooltip <Tooltip
content={selectionTooltip} content={selectionTooltip}
position="bottom" position="bottom"
@@ -514,9 +458,6 @@ class MenuBar extends React.Component {
/> />
<UndoRedoReset <UndoRedoReset
dispatch={dispatch} dispatch={dispatch}
isResetDisabled={this.isResetDisabled}
resetInterface={this.resetInterface}
resettingInterface={resettingInterface}
undoDisabled={undoDisabled} undoDisabled={undoDisabled}
redoDisabled={redoDisabled} redoDisabled={redoDisabled}
/> />
+48
View File
@@ -0,0 +1,48 @@
import React from "react";
import {AnchorButton, ButtonGroup, Tooltip} from "@blueprintjs/core";
import * as globals from "../../globals";
function Subset(props) {
const {
subsetPossible,
subsetResetPossible,
handleSubset,
handleSubsetReset,
} = props;
return (
<ButtonGroup style={{marginRight: "10px"}}>
<Tooltip
content="Subset to currently selected cells and associated metadata"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
data-testid="subset-button"
active={!subsetPossible}
icon="pie-chart"
onClick={() => {
if (subsetPossible) handleSubset();
}}
/>
</Tooltip>
<Tooltip
content="Undo subset and show all cells and associated metadata"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
data-testid="reset-subset-button"
active={!subsetResetPossible}
icon="full-circle"
onClick={() => {
if (subsetResetPossible) handleSubsetReset();
}}
/>
</Tooltip>
</ButtonGroup>
);
}
export default Subset;
@@ -5,11 +5,8 @@ import { tooltipHoverOpenDelay } from "../../globals";
function InformationMenu(props) { function InformationMenu(props) {
const { const {
resettingInterface,
undoDisabled, undoDisabled,
redoDisabled, redoDisabled,
resetInterface,
isResetDisabled,
dispatch dispatch
} = props; } = props;
return ( return (
@@ -50,23 +47,6 @@ function InformationMenu(props) {
data-testid="redo" data-testid="redo"
/> />
</Tooltip> </Tooltip>
<Tooltip
content="Reset cellxgene, clearing all selections"
position="bottom"
hoverOpenDelay={tooltipHoverOpenDelay}
>
<AnchorButton
disabled={isResetDisabled()}
style={{ marginLeft: 10 }}
type="button"
loading={resettingInterface}
intent="none"
icon="refresh"
onClick={resetInterface}
data-testid="reset"
data-testclass={`resetting-${resettingInterface}`}
/>
</Tooltip>
</div> </div>
); );
} }
+3
View File
@@ -74,6 +74,9 @@ export const leftSidebarSectionPadding = 10;
export const categoryLabelDisplayStringLongLength = 27; export const categoryLabelDisplayStringLongLength = 27;
export const categoryLabelDisplayStringShortLength = 11; export const categoryLabelDisplayStringShortLength = 11;
export const maxUserDefinedGenes = 25;
export const maxGenes = 100;
/* various timing-related behaviors */ /* various timing-related behaviors */
export const tooltipHoverOpenDelay = 1000; /* ms delay before a tooltip displays */ export const tooltipHoverOpenDelay = 1000; /* ms delay before a tooltip displays */
export const tooltipHoverOpenDelayQuick = 500; export const tooltipHoverOpenDelayQuick = 500;
+10 -2
View File
@@ -1,6 +1,8 @@
// jshint esversion: 6 // jshint esversion: 6
import _ from "lodash"; import _ from "lodash";
import * as globals from "../globals";
import { subsetAndResetGeneLists } from "../util/stateManager/controlsHelpers";
const Controls = ( const Controls = (
state = { state = {
@@ -74,16 +76,22 @@ const Controls = (
}; };
} }
case "reset World to eq Universe": { case "reset World to eq Universe": {
const [ newUserDefinedGenes, newDiffExpGenes ] = subsetAndResetGeneLists(state);
return { return {
...state, ...state,
resettingInterface: false resettingInterface: false,
userDefinedGenes: newUserDefinedGenes,
diffexpGenes: newDiffExpGenes
}; };
} }
case "set World to current selection": { case "set World to current selection": {
const [ newUserDefinedGenes, newDiffExpGenes ] = subsetAndResetGeneLists(state);
return { return {
...state, ...state,
loading: false, loading: false,
error: null error: null,
userDefinedGenes: newUserDefinedGenes,
diffexpGenes: newDiffExpGenes
}; };
} }
case "request user defined gene started": { case "request user defined gene started": {
-2
View File
@@ -16,7 +16,6 @@ const skipOnActions = new Set([
"configuration load complete", "configuration load complete",
"increment graph render counter", "increment graph render counter",
"window resize", "window resize",
"user reset start",
"reset colorscale", "reset colorscale",
"reset centroid labels", "reset centroid labels",
@@ -65,7 +64,6 @@ const clearOnActions = new Set([
"initial data load complete (universe exists)", "initial data load complete (universe exists)",
"reset World to eq Universe", "reset World to eq Universe",
"initial data load error", "initial data load error",
"user reset end"
]); ]);
/* /*
+1 -1
View File
@@ -586,7 +586,7 @@ class Dataframe {
withRowIndex allows assignment of new row index during subset operation. withRowIndex allows assignment of new row index during subset operation.
If withRowIndex === null, it will reset the index to identity (offset) If withRowIndex === null, it will reset the index to identity (offset)
indexing. if withRowIndex is a label index object, it will be used indexing. If withRowIndex is a label index object, it will be used
for the new dataframe. for the new dataframe.
*/ */
return this.__subset(rowOffsets, colOffsets, withRowIndex); return this.__subset(rowOffsets, colOffsets, withRowIndex);
@@ -200,3 +200,10 @@ export function pruneVarDataCache(varData, needed) {
} }
return varData; return varData;
} }
export function subsetAndResetGeneLists(state) {
const { userDefinedGenes, diffexpGenes } = state;
const newUserDefinedGenes = [].concat(userDefinedGenes, diffexpGenes).slice(0, globals.maxGenes);
const newDiffExpGenes = [];
return [newUserDefinedGenes, newDiffExpGenes];
}
+2 -2
View File
@@ -5,8 +5,8 @@ import { isContinuousAnnotation } from "./annotationsHelpers";
/* /*
World is a subset of universe. Most code should use world, and should World is a subset of universe. Most code should use world, and should
(generally) not use Universe. World contains any per-obs or per-var data (generally) not use Universe. World contains any per-obs or per-var data
that must be consistent acorss the app when we view/manipulate subsets that must be consistent acorss the app when we view/manipulate subsets
of Universe. of Universe.