mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-06 02:28:11 +08:00
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:
@@ -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(","));
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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;
|
||||||
-20
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Vendored
+10
-2
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user