From 05323ae643274b6bca1e44edda138c17bd683ea9 Mon Sep 17 00:00:00 2001 From: Matt Weiden <538456+mweiden@users.noreply.github.com> Date: Mon, 2 Mar 2020 12:07:12 -0800 Subject: [PATCH] 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 --- client/__tests__/e2e/e2e.test.js | 106 +++++++-------- client/__tests__/e2e/e2eAnnotations.test.js | 18 +-- client/__tests__/e2e/puppeteerUtils.js | 63 ++++++--- .../util/stateManager/controlsHelpers.test.js | 24 ++++ client/src/actions/index.js | 43 ++----- client/src/components/geneExpression/index.js | 4 +- client/src/components/menubar/index.js | 121 +++++------------- client/src/components/menubar/subset.js | 48 +++++++ .../menubar/{undoRedoReset.js => undoRedo.js} | 20 --- client/src/globals.js | 3 + client/src/reducers/controls.js | 12 +- client/src/reducers/undoableConfig.js | 2 - client/src/util/dataframe/dataframe.js | 2 +- .../src/util/stateManager/controlsHelpers.js | 7 + client/src/util/stateManager/world.js | 4 +- 15 files changed, 231 insertions(+), 246 deletions(-) create mode 100644 client/__tests__/util/stateManager/controlsHelpers.test.js create mode 100644 client/src/components/menubar/subset.js rename client/src/components/menubar/{undoRedoReset.js => undoRedo.js} (69%) diff --git a/client/__tests__/e2e/e2e.test.js b/client/__tests__/e2e/e2e.test.js index 1c21c939..cf5cbba1 100644 --- a/client/__tests__/e2e/e2e.test.js +++ b/client/__tests__/e2e/e2e.test.js @@ -20,9 +20,7 @@ beforeEach(async () => { }); afterAll(() => { - if (!DEBUG) { - browser.close(); - } + browser.close(); }); describe("did launch", () => { @@ -111,16 +109,10 @@ describe("cell selection", () => { describe("gene entry", () => { test("search for single gene", async () => { - // blueprint's typeahead is treating typing weird, clicking & waiting first solves this - await utils.typeInto("gene-search", data.genes.search); - await page.keyboard.press("Enter"); - await page.waitForSelector( - `[data-testid='histogram-${data.genes.search}']` - ); + await cxgActions.addGeneToSearch(data.genes.search); }); test("bulk add genes", async () => { - await cxgActions.reset(); const testGenes = data.genes.bulkadd; await utils.clickOn("section-bulk-add"); 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 () => { - for (const select of data.diffexp.cellset1) { - 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"); + await cxgActions.runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2); const allHistograms = await cxgActions.getAllHistograms( "histogram-diffexp", data.diffexp["gene-results"] @@ -161,7 +141,7 @@ describe("diffexp", () => { }); }); -describe("subset/reset", () => { +describe("subset", () => { test("subset - cell count matches", async () => { for (const select of data.subset.cellset1) { 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 () => { for (const select of data.subset.cellset1) { if (select.kind === "categorical") { @@ -233,11 +180,52 @@ describe("subset/reset", () => { const cellCount = await cxgActions.cellSet(1); 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", () => { test("scatter plot appears", async () => { - await cxgActions.reset(); const testGenes = data.scatter.genes; await utils.clickOn("section-bulk-add"); await utils.typeInto("input-bulk-add", Object.values(testGenes).join(",")); diff --git a/client/__tests__/e2e/e2eAnnotations.test.js b/client/__tests__/e2e/e2eAnnotations.test.js index 94db26b7..b74e55d9 100644 --- a/client/__tests__/e2e/e2eAnnotations.test.js +++ b/client/__tests__/e2e/e2eAnnotations.test.js @@ -22,22 +22,6 @@ describe.each([ {withSubset: false, tag: "whole"} ])("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 perTestLabelName = "per-test-label"; @@ -48,7 +32,7 @@ describe.each([ // setup the test fixtures await cxgActions.createCategory(perTestCategoryName); 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"); }); diff --git a/client/__tests__/e2e/puppeteerUtils.js b/client/__tests__/e2e/puppeteerUtils.js index 90873357..15404ad3 100644 --- a/client/__tests__/e2e/puppeteerUtils.js +++ b/client/__tests__/e2e/puppeteerUtils.js @@ -1,5 +1,6 @@ import {DEBUG, DEV} from "./config"; import puppeteer from "puppeteer"; +import { strict as assert } from "assert"; export const puppeteerUtils = puppeteerPage => ({ async waitByID(testid, props = {}) { @@ -16,7 +17,7 @@ export const puppeteerUtils = puppeteerPage => ({ ); }, - async waitForAllByIds(testids, props = {}) { + async waitForAllByIds(testids) { await Promise.all( testids.map(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( `[data-testclass=${testclass}]`, - els => { - return els.map(el => { - return el.dataset.testid; - }); - } + eles => eles.map(ele => ele.dataset.testid) ); return elements; }, async typeInto(testid, text) { + // blueprint's typeahead is treating typing weird, clicking & waiting first solves this // only works for text without special characters await this.waitByID(testid); const selector = `[data-testid='${testid}']`; @@ -80,6 +78,7 @@ export const puppeteerUtils = puppeteerPage => ({ }); export const cellxgeneActions = puppeteerPage => ({ + async drag(testid, start, end, lasso = false) { const layout = await puppeteerUtils(puppeteerPage).waitByID(testid); const elBox = await layout.boxModel(); @@ -116,7 +115,7 @@ export const cellxgeneActions = puppeteerPage => ({ testclass ); 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-select`); for (const val of values) { - await puppeteerUtils(puppeteerPage).clickOn( - `categorical-value-select-${category}-${val}` - ); + await puppeteerUtils(puppeteerPage).clickOn(`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) { await puppeteerUtils(puppeteerPage).clickOn("visualization-settings"); await puppeteerUtils(puppeteerPage).clearInputAndTypeInto( @@ -269,6 +260,44 @@ export const cellxgeneActions = puppeteerPage => ({ newLabelName ); 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"); } }); diff --git a/client/__tests__/util/stateManager/controlsHelpers.test.js b/client/__tests__/util/stateManager/controlsHelpers.test.js new file mode 100644 index 00000000..1e7b1a7a --- /dev/null +++ b/client/__tests__/util/stateManager/controlsHelpers.test.js @@ -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([]); + }); +}); diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 992304a3..fb640e5f 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -149,7 +149,7 @@ const doInitialDataLoad = () => Set the view (world) to current selection. Placeholder for an async action which also does re-layout. */ -const regraph = () => (dispatch, getState) => { +const setWorldToSelection = () => (dispatch, getState) => { const { universe, world, crossfilter } = getState(); dispatch({ 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 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(); - - 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({ type: "reset World to eq Universe", universe }); - dispatch({ - type: "increment graph render counter" - }); - dispatch({ - type: "user reset end" - }); }; const saveObsAnnotations = () => async (dispatch, getState) => { @@ -472,11 +447,11 @@ const saveObsAnnotations = () => async (dispatch, getState) => { }; export default { - regraph, - resetInterface, - requestSingleGeneExpressionCountsForColoringPOST, - requestDifferentialExpression, - requestUserDefinedGene, doInitialDataLoad, - saveObsAnnotations + requestDifferentialExpression, + requestSingleGeneExpressionCountsForColoringPOST, + requestUserDefinedGene, + resetWorldToUniverse, + saveObsAnnotations, + setWorldToSelection, }; diff --git a/client/src/components/geneExpression/index.js b/client/src/components/geneExpression/index.js index dcfcb46b..4a9448bf 100644 --- a/client/src/components/geneExpression/index.js +++ b/client/src/components/geneExpression/index.js @@ -182,9 +182,9 @@ class GeneExpression extends React.Component { const gene = g.target; if (userDefinedGenes.indexOf(gene) !== -1) { postUserErrorToast("That gene already exists"); - } else if (userDefinedGenes.length > 15) { + } else if (userDefinedGenes.length > globals.maxUserDefinedGenes) { 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 ( world.varAnnotations.col(varIndexName).indexOf(gene) === undefined diff --git a/client/src/components/menubar/index.js b/client/src/components/menubar/index.js index bc461cb1..6d7e2d1f 100644 --- a/client/src/components/menubar/index.js +++ b/client/src/components/menubar/index.js @@ -10,22 +10,20 @@ import { Position, RadioGroup, Radio, - Icon } from "@blueprintjs/core"; -import { World } from "../../util/stateManager"; +import * as globals from "../../globals"; import actions from "../../actions"; import CellSetButton from "./cellSetButtons"; -import InformationMenu from "./infoMenu"; -import UndoRedoReset from "./undoRedoReset"; import Clip from "./clip"; -import * as globals from "../../globals"; +import InformationMenu from "./infoMenu"; +import Subset from "./subset"; +import UndoRedoReset from "./undoRedo"; @connect(state => ({ universe: state.universe, world: state.world, 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)), @@ -99,61 +97,6 @@ class MenuBar extends React.Component { 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 @@ -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() { /* diffexp-related buttons may be disabled */ const { disableDiffexp, differential, diffexpMayBeSlow } = this.props; @@ -332,8 +289,6 @@ class MenuBar extends React.Component { render() { const { dispatch, - crossfilter, - resettingInterface, libraryVersions, undoDisabled, redoDisabled, @@ -368,30 +323,19 @@ class MenuBar extends React.Component { }} > {this.renderDiffExp()} - - { - dispatch(actions.regraph()); - dispatch({ type: "increment graph render counter" }); - }} - > - - - - + { + dispatch(actions.setWorldToSelection()); + dispatch({ type: "increment graph render counter" }); + }} + handleSubsetReset={() => { + dispatch(actions.resetWorldToUniverse()); + dispatch({ type: "increment graph render counter" }); + }} + /> + diff --git a/client/src/components/menubar/subset.js b/client/src/components/menubar/subset.js new file mode 100644 index 00000000..aa09374f --- /dev/null +++ b/client/src/components/menubar/subset.js @@ -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 ( + + + { + if (subsetPossible) handleSubset(); + }} + /> + + + { + if (subsetResetPossible) handleSubsetReset(); + }} + /> + + + ); +} + +export default Subset; diff --git a/client/src/components/menubar/undoRedoReset.js b/client/src/components/menubar/undoRedo.js similarity index 69% rename from client/src/components/menubar/undoRedoReset.js rename to client/src/components/menubar/undoRedo.js index 9f54db07..523d6e7e 100644 --- a/client/src/components/menubar/undoRedoReset.js +++ b/client/src/components/menubar/undoRedo.js @@ -5,11 +5,8 @@ import { tooltipHoverOpenDelay } from "../../globals"; function InformationMenu(props) { const { - resettingInterface, undoDisabled, redoDisabled, - resetInterface, - isResetDisabled, dispatch } = props; return ( @@ -50,23 +47,6 @@ function InformationMenu(props) { data-testid="redo" /> - - - ); } diff --git a/client/src/globals.js b/client/src/globals.js index 8e4219b8..81d43c4c 100644 --- a/client/src/globals.js +++ b/client/src/globals.js @@ -74,6 +74,9 @@ export const leftSidebarSectionPadding = 10; export const categoryLabelDisplayStringLongLength = 27; export const categoryLabelDisplayStringShortLength = 11; +export const maxUserDefinedGenes = 25; +export const maxGenes = 100; + /* various timing-related behaviors */ export const tooltipHoverOpenDelay = 1000; /* ms delay before a tooltip displays */ export const tooltipHoverOpenDelayQuick = 500; diff --git a/client/src/reducers/controls.js b/client/src/reducers/controls.js index 8625ed40..d38b3dac 100644 --- a/client/src/reducers/controls.js +++ b/client/src/reducers/controls.js @@ -1,6 +1,8 @@ // jshint esversion: 6 import _ from "lodash"; +import * as globals from "../globals"; +import { subsetAndResetGeneLists } from "../util/stateManager/controlsHelpers"; const Controls = ( state = { @@ -74,16 +76,22 @@ const Controls = ( }; } case "reset World to eq Universe": { + const [ newUserDefinedGenes, newDiffExpGenes ] = subsetAndResetGeneLists(state); return { ...state, - resettingInterface: false + resettingInterface: false, + userDefinedGenes: newUserDefinedGenes, + diffexpGenes: newDiffExpGenes }; } case "set World to current selection": { + const [ newUserDefinedGenes, newDiffExpGenes ] = subsetAndResetGeneLists(state); return { ...state, loading: false, - error: null + error: null, + userDefinedGenes: newUserDefinedGenes, + diffexpGenes: newDiffExpGenes }; } case "request user defined gene started": { diff --git a/client/src/reducers/undoableConfig.js b/client/src/reducers/undoableConfig.js index b514a0da..2d08f4ca 100644 --- a/client/src/reducers/undoableConfig.js +++ b/client/src/reducers/undoableConfig.js @@ -16,7 +16,6 @@ const skipOnActions = new Set([ "configuration load complete", "increment graph render counter", "window resize", - "user reset start", "reset colorscale", "reset centroid labels", @@ -65,7 +64,6 @@ const clearOnActions = new Set([ "initial data load complete (universe exists)", "reset World to eq Universe", "initial data load error", - "user reset end" ]); /* diff --git a/client/src/util/dataframe/dataframe.js b/client/src/util/dataframe/dataframe.js index f01c3fa8..74dc0aa4 100644 --- a/client/src/util/dataframe/dataframe.js +++ b/client/src/util/dataframe/dataframe.js @@ -586,7 +586,7 @@ class Dataframe { withRowIndex allows assignment of new row index during subset operation. 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. */ return this.__subset(rowOffsets, colOffsets, withRowIndex); diff --git a/client/src/util/stateManager/controlsHelpers.js b/client/src/util/stateManager/controlsHelpers.js index 397730e3..a8d1cd56 100644 --- a/client/src/util/stateManager/controlsHelpers.js +++ b/client/src/util/stateManager/controlsHelpers.js @@ -200,3 +200,10 @@ export function pruneVarDataCache(varData, needed) { } return varData; } + +export function subsetAndResetGeneLists(state) { + const { userDefinedGenes, diffexpGenes } = state; + const newUserDefinedGenes = [].concat(userDefinedGenes, diffexpGenes).slice(0, globals.maxGenes); + const newDiffExpGenes = []; + return [newUserDefinedGenes, newDiffExpGenes]; +} diff --git a/client/src/util/stateManager/world.js b/client/src/util/stateManager/world.js index 0419814f..19acca5a 100644 --- a/client/src/util/stateManager/world.js +++ b/client/src/util/stateManager/world.js @@ -5,8 +5,8 @@ import { isContinuousAnnotation } from "./annotationsHelpers"; /* -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 +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 that must be consistent acorss the app when we view/manipulate subsets of Universe.