mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-09 07:38:12 +08:00
refactor categorical controls state (#1549)
* refactor categorical controls state * lint * fix race condition in tests * fix typo * add missing update on subset * remove obsolete code * update jest and puppeteer major version; update all minors * update when label changes * remove lint from tests; increase timeouts in e2e tests * changes in response to PR review * lint * more PR comment changes * more PR comment fixes * lint * more PR comment resolutions
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
|
|
||||||
export const cellxgeneActions = (page, utils) => ({
|
const cellxgeneActions = (page, utils) => ({
|
||||||
async drag(testId, start, end, lasso = false) {
|
async drag(testId, start, end, lasso = false) {
|
||||||
const layout = await utils.waitByID(testId);
|
const layout = await utils.waitByID(testId);
|
||||||
const elBox = await layout.boxModel();
|
const elBox = await layout.boxModel();
|
||||||
@@ -31,14 +31,16 @@ export const cellxgeneActions = (page, utils) => ({
|
|||||||
|
|
||||||
async getAllHistograms(testclass, testIds) {
|
async getAllHistograms(testclass, testIds) {
|
||||||
const histTestIds = testIds.map((tid) => `histogram-${tid}`);
|
const histTestIds = testIds.map((tid) => `histogram-${tid}`);
|
||||||
// these load asynchronously, so we need to wait for each histogram individually
|
// these load asynchronously, so we need to wait for each histogram individually,
|
||||||
await utils.waitForAllByIds(histTestIds);
|
// and they may be quite slow in some cases.
|
||||||
|
await utils.waitForAllByIds(histTestIds, { timeout: 240000 });
|
||||||
const allHistograms = await utils.getAllByClass(testclass);
|
const allHistograms = await utils.getAllByClass(testclass);
|
||||||
return allHistograms.map((hist) => hist.replace(/^histogram-/, ""));
|
return allHistograms.map((hist) => hist.replace(/^histogram-/, ""));
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAllCategoriesAndCounts(category) {
|
async getAllCategoriesAndCounts(category) {
|
||||||
await utils.waitByClass("categorical-row");
|
// these load asynchronously, so we have to wait for the specific category.
|
||||||
|
await utils.waitByID(`category-${category}`);
|
||||||
return page.$$eval(
|
return page.$$eval(
|
||||||
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
|
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
|
||||||
(rows) =>
|
(rows) =>
|
||||||
@@ -222,3 +224,5 @@ export const cellxgeneActions = (page, utils) => ({
|
|||||||
await page.keyboard.press("Enter");
|
await page.keyboard.press("Enter");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default cellxgeneActions;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
export const jestEnv = process.env.JEST_ENV;
|
export const jestEnv = process.env.JEST_ENV;
|
||||||
export const appPort = process.env.CXG_SERVER_PORT;
|
export const appPort = process.env.CXG_SERVER_PORT;
|
||||||
export const appUrlBase =
|
export const appUrlBase =
|
||||||
process.env.CXG_URL_BASE || `http://localhost:${appPort}`;
|
process.env.CXG_URL_BASE || `http://localhost:${appPort}`;
|
||||||
export const DEV = jestEnv === "dev";
|
export const DEV = jestEnv === "dev";
|
||||||
export const DEBUG = jestEnv === "debug";
|
export const DEBUG = jestEnv === "debug";
|
||||||
export const DATASET = "pbmc3k";
|
export const DATASET = "pbmc3k";
|
||||||
|
|
||||||
if (DEBUG) jest.setTimeout(100000);
|
if (DEBUG) jest.setTimeout(2 * 60 * 1000);
|
||||||
if (DEV) jest.setTimeout(10000);
|
if (DEV) jest.setTimeout(30 * 1000);
|
||||||
|
if (!DEBUG && !DEV) jest.setTimeout(10 * 1000);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ Smoke test suite that will be run in Travis CI
|
|||||||
|
|
||||||
Tests included in this file are expected to be relatively stable and test core features
|
Tests included in this file are expected to be relatively stable and test core features
|
||||||
*/
|
*/
|
||||||
import { appUrlBase, DATASET } from "./config";
|
import { appUrlBase, DATASET, DEBUG } from "./config";
|
||||||
import { setupTestBrowser } from "./testBrowser";
|
import setupTestBrowser from "./testBrowser";
|
||||||
import { datasets } from "./data";
|
import { datasets } from "./data";
|
||||||
|
|
||||||
let browser;
|
let browser;
|
||||||
@@ -22,7 +22,7 @@ beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
if (browser !== undefined) browser.close();
|
if (!DEBUG && browser !== undefined) browser.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("did launch", () => {
|
describe("did launch", () => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
Tests included in this file are specific to annotation features
|
Tests included in this file are specific to annotation features
|
||||||
*/
|
*/
|
||||||
import { appUrlBase, DATASET } from "./config";
|
import { appUrlBase, DATASET, DEBUG } from "./config";
|
||||||
import { setupTestBrowser } from "./testBrowser";
|
import setupTestBrowser from "./testBrowser";
|
||||||
import { datasets } from "./data";
|
import { datasets } from "./data";
|
||||||
|
|
||||||
let browser;
|
let browser;
|
||||||
@@ -15,8 +15,8 @@ beforeAll(async () => {
|
|||||||
[browser, page, utils, actions] = await setupTestBrowser();
|
[browser, page, utils, actions] = await setupTestBrowser();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(async () => {
|
||||||
if (browser !== undefined) browser.close();
|
if (!DEBUG && browser !== undefined) await browser.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
@@ -28,6 +28,7 @@ describe.each([
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await page.goto(appUrlBase);
|
await page.goto(appUrlBase);
|
||||||
|
|
||||||
// wait for the page to load
|
// wait for the page to load
|
||||||
await utils.waitByClass("autosave-complete");
|
await utils.waitByClass("autosave-complete");
|
||||||
// setup the test fixtures
|
// setup the test fixtures
|
||||||
@@ -238,13 +239,9 @@ describe.each([
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCategoryIfExists(categoryName) {
|
async function deleteCategoryIfExists(categoryName) {
|
||||||
try {
|
const handle = await page.$(
|
||||||
const category = await page.waitForSelector(
|
`[data-testid='${categoryName}:category-expand']`
|
||||||
`[data-testid='${categoryName}:category-expand']`,
|
);
|
||||||
{ timeout: 200 }
|
if (handle) await actions.deleteCategory(categoryName);
|
||||||
);
|
|
||||||
if (category !== null) return await actions.deleteCategory(categoryName);
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
export const puppeteerUtils = (page) => ({
|
const puppeteerUtils = (page) => ({
|
||||||
async waitByID(testId, props = {}) {
|
waitByID(testId, props = {}) {
|
||||||
return page.waitForSelector(`[data-testid='${testId}']`, props);
|
return page.waitForSelector(`[data-testid='${testId}']`, props);
|
||||||
},
|
},
|
||||||
|
|
||||||
async waitByClass(testClass, props = {}) {
|
waitByClass(testClass, props = {}) {
|
||||||
return page.waitForSelector(`[data-testclass='${testClass}']`, props);
|
return page.waitForSelector(`[data-testclass='${testClass}']`, props);
|
||||||
},
|
},
|
||||||
|
|
||||||
async waitForAllByIds(testIds) {
|
async waitForAllByIds(testIds, props = {}) {
|
||||||
await Promise.all(
|
return Promise.all(
|
||||||
testIds.map((testId) => page.waitForSelector(`[data-testid='${testId}']`))
|
testIds.map((testId) =>
|
||||||
|
page.waitForSelector(`[data-testid='${testId}']`, props)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -67,3 +69,5 @@ export const puppeteerUtils = (page) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default puppeteerUtils;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import puppeteer from "puppeteer";
|
import puppeteer from "puppeteer";
|
||||||
import { DEBUG, DEV } from "./config";
|
import { DEBUG, DEV } from "./config";
|
||||||
import { puppeteerUtils } from "./puppeteerUtils";
|
import puppeteerUtils from "./puppeteerUtils";
|
||||||
import { cellxgeneActions } from "./cellxgeneActions";
|
import cellxgeneActions from "./cellxgeneActions";
|
||||||
|
|
||||||
export async function setupTestBrowser() {
|
export default async function setupTestBrowser() {
|
||||||
const browserViewport = { width: 1280, height: 960 };
|
const browserViewport = { width: 1280, height: 960 };
|
||||||
const browserParams = DEV
|
const browserParams = DEV
|
||||||
? {
|
? {
|
||||||
@@ -35,21 +35,30 @@ export async function setupTestBrowser() {
|
|||||||
if (DEV || DEBUG) {
|
if (DEV || DEBUG) {
|
||||||
page.on("console", async (msg) => {
|
page.on("console", async (msg) => {
|
||||||
// If there is a console.error but an error is not thrown, this will ensure the test fails
|
// If there is a console.error but an error is not thrown, this will ensure the test fails
|
||||||
|
console.log(`PAGE LOG: ${msg.text()}`);
|
||||||
if (msg.type() === "error") {
|
if (msg.type() === "error") {
|
||||||
// TODO: chromium does not currently support the CSP directive on the
|
// TODO: chromium does not currently support the CSP directive on the
|
||||||
// line below, so we swallow this error. Remove this when the test
|
// line below, so we swallow this error. Remove this when the test
|
||||||
// suite uses a browser version that supports this directive.
|
// suite uses a browser version that supports this directive.
|
||||||
if (msg.text() === "Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.\n") return;
|
if (
|
||||||
|
msg.text() ===
|
||||||
|
"Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.\n"
|
||||||
|
)
|
||||||
|
return;
|
||||||
const errorMsgText = await Promise.all(
|
const errorMsgText = await Promise.all(
|
||||||
// TODO can we do this without internal properties?
|
// TODO can we do this without internal properties?
|
||||||
msg.args().map((arg) => arg._remoteObject.description)
|
msg.args().map((arg) => arg._remoteObject.description)
|
||||||
);
|
);
|
||||||
throw new Error(`Console error: ${errorMsgText}`);
|
throw new Error(`Console error: ${errorMsgText}`);
|
||||||
}
|
}
|
||||||
console.log(`PAGE LOG: ${msg.text()}`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
page.on("pageerror", (err) => {
|
page.on("pageerror", (err) => {
|
||||||
|
console.log(`PAGE LOG: ${msg.text()}`);
|
||||||
|
throw new Error(`Console error: ${err}`);
|
||||||
|
});
|
||||||
|
page.on("error", (err) => {
|
||||||
|
console.log(`PAGE LOG: ${msg.text()}`);
|
||||||
throw new Error(`Console error: ${err}`);
|
throw new Error(`Console error: ${err}`);
|
||||||
});
|
});
|
||||||
const utils = puppeteerUtils(page);
|
const utils = puppeteerUtils(page);
|
||||||
|
|||||||
@@ -35,19 +35,15 @@ describe("centroid", () => {
|
|||||||
|
|
||||||
// Create categorical selection from world
|
// Create categorical selection from world
|
||||||
categoricalSelection = CH.createCategoricalSelection(
|
categoricalSelection = CH.createCategoricalSelection(
|
||||||
world,
|
CH.selectableCategoryNames(world.schema)
|
||||||
CH.selectableCategoryNames(world.schema, CH.maxCategoryItems(REST.config))
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("field4 (categorical obsAnnotation)", () => {
|
test("field4 (categorical obsAnnotation)", () => {
|
||||||
const centroidResult = calcCentroid(
|
const centroidResult = calcCentroid(
|
||||||
world.obsAnnotations,
|
world,
|
||||||
world.obsLayout,
|
|
||||||
"field4",
|
"field4",
|
||||||
["umap_0", "umap_1"],
|
["umap_0", "umap_1"]
|
||||||
categoricalSelection,
|
|
||||||
world.schema.annotations.obsByName
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check to see that a centroid has been calculated for every categorical value
|
// Check to see that a centroid has been calculated for every categorical value
|
||||||
@@ -69,12 +65,9 @@ describe("centroid", () => {
|
|||||||
|
|
||||||
test("field3 (boolean obsAnnotation)", () => {
|
test("field3 (boolean obsAnnotation)", () => {
|
||||||
const centroidResult = calcCentroid(
|
const centroidResult = calcCentroid(
|
||||||
world.obsAnnotations,
|
world,
|
||||||
world.obsLayout,
|
|
||||||
"field3",
|
"field3",
|
||||||
["umap_0", "umap_1"],
|
["umap_0", "umap_1"]
|
||||||
categoricalSelection,
|
|
||||||
world.schema.annotations.obsByName
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check to see that a centroid has been calculated for every categorical value
|
// Check to see that a centroid has been calculated for every categorical value
|
||||||
|
|||||||
Generated
+4753
-3144
File diff suppressed because it is too large
Load Diff
+29
-29
@@ -26,9 +26,9 @@
|
|||||||
"eslint-scope": "3.7.1"
|
"eslint-scope": "3.7.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blueprintjs/core": "^3.24.0",
|
"@blueprintjs/core": "^3.28.1",
|
||||||
"@blueprintjs/icons": "^3.14.0",
|
"@blueprintjs/icons": "^3.18.0",
|
||||||
"@blueprintjs/select": "^3.12.0",
|
"@blueprintjs/select": "^3.13.2",
|
||||||
"d3": "^4.10.0",
|
"d3": "^4.10.0",
|
||||||
"d3-scale-chromatic": "^1.5.0",
|
"d3-scale-chromatic": "^1.5.0",
|
||||||
"flatbuffers": "^1.11.0",
|
"flatbuffers": "^1.11.0",
|
||||||
@@ -43,39 +43,39 @@
|
|||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-flip-toolkit": "7.0.6",
|
"react-flip-toolkit": "7.0.6",
|
||||||
"react-helmet": "^5.2.1",
|
"react-helmet": "^5.2.1",
|
||||||
"react-icons": "^3.9.0",
|
"react-icons": "^3.10.0",
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.0",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"regl": "^1.4.0"
|
"regl": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.10.2",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.10.1",
|
||||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
"@babel/plugin-proposal-decorators": "^7.10.1",
|
||||||
"@babel/plugin-proposal-export-namespace-from": "^7.8.3",
|
"@babel/plugin-proposal-export-namespace-from": "^7.10.1",
|
||||||
"@babel/plugin-proposal-function-bind": "^7.8.3",
|
"@babel/plugin-proposal-function-bind": "^7.10.1",
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
|
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1",
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.10.1",
|
||||||
"@babel/plugin-transform-react-constant-elements": "^7.9.0",
|
"@babel/plugin-transform-react-constant-elements": "^7.10.1",
|
||||||
"@babel/plugin-transform-runtime": "^7.9.0",
|
"@babel/plugin-transform-runtime": "^7.10.1",
|
||||||
"@babel/preset-env": "^7.9.0",
|
"@babel/preset-env": "^7.10.2",
|
||||||
"@babel/preset-react": "^7.9.4",
|
"@babel/preset-react": "^7.10.1",
|
||||||
"@babel/register": "^7.9.0",
|
"@babel/register": "^7.10.1",
|
||||||
"@babel/runtime": "^7.9.2",
|
"@babel/runtime": "^7.10.2",
|
||||||
"@sentry/webpack-plugin": "^1.11.1",
|
"@sentry/webpack-plugin": "^1.11.1",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^25.2.6",
|
"babel-jest": "^26.0.1",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"babel-preset-modern-browsers": "^14.2.1",
|
"babel-preset-modern-browsers": "^14.2.1",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"cheerio": "^1.0.0-rc.3",
|
"cheerio": "^1.0.0-rc.3",
|
||||||
"clean-css": "^4.2.3",
|
"clean-css": "^4.2.3",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"codecov": "^3.6.5",
|
"codecov": "^3.7.0",
|
||||||
"connect-history-api-fallback": "^1.6.0",
|
"connect-history-api-fallback": "^1.6.0",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.5.3",
|
||||||
"eslint": "^7.2.0",
|
"eslint": "^7.2.0",
|
||||||
"eslint-config-airbnb": "^18.0.1",
|
"eslint-config-airbnb": "^18.0.1",
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"eslint-config-prettier": "^6.11.0",
|
||||||
@@ -92,23 +92,23 @@
|
|||||||
"favicons-webpack-plugin": "^3.0.1",
|
"favicons-webpack-plugin": "^3.0.1",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
|
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
|
||||||
"html-webpack-plugin": "^4.0.0",
|
"html-webpack-plugin": "^4.3.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"jest": "^25.2.7",
|
"jest": "^26.0.1",
|
||||||
"jest-puppeteer": "^4.4.0",
|
"jest-puppeteer": "^4.4.0",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"lint-staged": "^10.2.4",
|
"lint-staged": "^10.2.9",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"puppeteer": "^2.1.1",
|
"puppeteer": "^3.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"style-loader": "^1.1.3",
|
"style-loader": "^1.2.1",
|
||||||
"sw-precache-webpack-plugin": "^1.0.0",
|
"sw-precache-webpack-plugin": "^1.0.0",
|
||||||
"terser-webpack-plugin": "^2.3.6",
|
"terser-webpack-plugin": "^2.3.7",
|
||||||
"url-loader": "^4.0.0",
|
"url-loader": "^4.1.0",
|
||||||
"webpack": "^4.42.1",
|
"webpack": "^4.43.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-middleware": "^3.7.2"
|
"webpack-dev-middleware": "^3.7.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core";
|
import { Button, Tooltip, Dialog, Classes, Colors } from "@blueprintjs/core";
|
||||||
|
|
||||||
@connect((state) => ({
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
categoricalSelection: state.categoricalSelection,
|
|
||||||
annotations: state.annotations,
|
|
||||||
universe: state.universe,
|
|
||||||
}))
|
|
||||||
class AnnoDialog extends React.PureComponent {
|
class AnnoDialog extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Button, MenuItem } from "@blueprintjs/core";
|
import { Button, MenuItem } from "@blueprintjs/core";
|
||||||
import { Select } from "@blueprintjs/select";
|
import { Select } from "@blueprintjs/select";
|
||||||
|
|
||||||
@connect((state) => ({
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
categoricalSelection: state.categoricalSelection,
|
|
||||||
annotations: state.annotations,
|
|
||||||
universe: state.universe,
|
|
||||||
}))
|
|
||||||
class DuplicateCategorySelect extends React.PureComponent {
|
class DuplicateCategorySelect extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import LabelInput from "../labelInput";
|
|||||||
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
||||||
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
categoricalSelection: state.categoricalSelection,
|
|
||||||
annotations: state.annotations,
|
annotations: state.annotations,
|
||||||
universe: state.universe,
|
universe: state.universe,
|
||||||
ontology: state.ontology,
|
ontology: state.ontology,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import AnnoDialog from "../annoDialog";
|
import AnnoDialog from "../annoDialog";
|
||||||
import LabelInput from "../labelInput";
|
import LabelInput from "../labelInput";
|
||||||
@@ -8,9 +7,9 @@ import { labelPrompt } from "../labelUtil";
|
|||||||
import { AnnotationsHelpers } from "../../../util/stateManager";
|
import { AnnotationsHelpers } from "../../../util/stateManager";
|
||||||
|
|
||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
categoricalSelection: state.categoricalSelection,
|
|
||||||
annotations: state.annotations,
|
annotations: state.annotations,
|
||||||
universe: state.universe,
|
universe: state.universe,
|
||||||
|
schema: state.world?.schema,
|
||||||
ontology: state.ontology,
|
ontology: state.ontology,
|
||||||
}))
|
}))
|
||||||
class AnnoDialogEditCategoryName extends React.PureComponent {
|
class AnnoDialogEditCategoryName extends React.PureComponent {
|
||||||
@@ -36,11 +35,14 @@ class AnnoDialogEditCategoryName extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleEditCategory = (e) => {
|
handleEditCategory = (e) => {
|
||||||
const { dispatch, metadataField, categoricalSelection } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
const { newCategoryText } = this.state;
|
const { newCategoryText } = this.state;
|
||||||
|
|
||||||
const allCategoryNames = _.keys(categoricalSelection);
|
/*
|
||||||
|
test for uniqueness against *all* annotation names, not just the subset
|
||||||
|
we render as categorical.
|
||||||
|
*/
|
||||||
|
const allCategoryNames = this.allCategoryNames();
|
||||||
if (
|
if (
|
||||||
(allCategoryNames.indexOf(newCategoryText) > -1 &&
|
(allCategoryNames.indexOf(newCategoryText) > -1 &&
|
||||||
newCategoryText !== metadataField) ||
|
newCategoryText !== metadataField) ||
|
||||||
@@ -60,7 +62,7 @@ class AnnoDialogEditCategoryName extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
editedCategoryNameError = (name) => {
|
editedCategoryNameError = (name) => {
|
||||||
const { metadataField, categoricalSelection } = this.props;
|
const { metadataField } = this.props;
|
||||||
|
|
||||||
/* check for syntax errors in category name */
|
/* check for syntax errors in category name */
|
||||||
const error = AnnotationsHelpers.annotationNameIsErroneous(name);
|
const error = AnnotationsHelpers.annotationNameIsErroneous(name);
|
||||||
@@ -69,7 +71,12 @@ class AnnoDialogEditCategoryName extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* check for duplicative categories */
|
/* check for duplicative categories */
|
||||||
const allCategoryNames = _.keys(categoricalSelection);
|
|
||||||
|
/*
|
||||||
|
test for uniqueness against *all* annotation names, not just the subset
|
||||||
|
we render as categorical.
|
||||||
|
*/
|
||||||
|
const allCategoryNames = this.allCategoryNames();
|
||||||
const categoryNameAlreadyExists = allCategoryNames.indexOf(name) > -1;
|
const categoryNameAlreadyExists = allCategoryNames.indexOf(name) > -1;
|
||||||
const sameName = name === metadataField;
|
const sameName = name === metadataField;
|
||||||
if (categoryNameAlreadyExists && !sameName) {
|
if (categoryNameAlreadyExists && !sameName) {
|
||||||
@@ -88,6 +95,11 @@ class AnnoDialogEditCategoryName extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
allCategoryNames() {
|
||||||
|
const { schema } = this.props;
|
||||||
|
return schema.annotations.obs.columns.map((c) => c.name);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { newCategoryText } = this.state;
|
const { newCategoryText } = this.state;
|
||||||
const { metadataField, annotations, ontology } = this.props;
|
const { metadataField, annotations, ontology } = this.props;
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Flipper, Flipped } from "react-flip-toolkit";
|
import { Flipper, Flipped } from "react-flip-toolkit";
|
||||||
|
|
||||||
import * as globals from "../../../globals";
|
import * as globals from "../../../globals";
|
||||||
import Value from "../value";
|
import Value from "../value";
|
||||||
|
|
||||||
@connect((state) => ({
|
|
||||||
categoricalSelection: state.categoricalSelection,
|
|
||||||
}))
|
|
||||||
class Category extends React.Component {
|
class Category extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -16,9 +11,9 @@ class Category extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCategoryItems(optTuples) {
|
renderCategoryItems(optTuples) {
|
||||||
const { metadataField, isUserAnno } = this.props;
|
const { metadataField, isUserAnno, categorySummary } = this.props;
|
||||||
|
|
||||||
return _.map(optTuples, (tuple, i) => {
|
return optTuples.map((tuple, i) => {
|
||||||
return (
|
return (
|
||||||
<Flipped key={tuple[1]} flipId={tuple[1]}>
|
<Flipped key={tuple[1]} flipId={tuple[1]}>
|
||||||
{(flippedProps) => (
|
{(flippedProps) => (
|
||||||
@@ -30,6 +25,7 @@ class Category extends React.Component {
|
|||||||
categoryIndex={tuple[1]}
|
categoryIndex={tuple[1]}
|
||||||
i={i}
|
i={i}
|
||||||
flippedProps={flippedProps}
|
flippedProps={flippedProps}
|
||||||
|
categorySummary={categorySummary}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flipped>
|
</Flipped>
|
||||||
@@ -38,16 +34,10 @@ class Category extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { metadataField, categorySummary, children, isExpanded } = this.props;
|
||||||
metadataField,
|
const { isTruncated } = categorySummary;
|
||||||
categoricalSelection,
|
const optTuples = [...categorySummary.categoryValueIndices];
|
||||||
children,
|
const optTuplesAsKey = optTuples.map((t) => t[0]).join(""); // animation
|
||||||
isExpanded,
|
|
||||||
} = this.props;
|
|
||||||
const { isTruncated } = categoricalSelection[metadataField];
|
|
||||||
const cat = categoricalSelection[metadataField];
|
|
||||||
const optTuples = [...cat.categoryValueIndices];
|
|
||||||
const optTuplesAsKey = _.map(optTuples, (t) => t[0]).join(""); // animation
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
||||||
import { AnchorButton, Button, Tooltip } from "@blueprintjs/core";
|
import { AnchorButton, Button, Tooltip } from "@blueprintjs/core";
|
||||||
@@ -10,6 +9,7 @@ import AnnoDialogAddLabel from "./annoDialogAddLabel";
|
|||||||
import Truncate from "../../util/truncate";
|
import Truncate from "../../util/truncate";
|
||||||
|
|
||||||
import * as globals from "../../../globals";
|
import * as globals from "../../../globals";
|
||||||
|
import { createCategorySummary as _createCategorySummary } from "../../../util/stateManager/controlsHelpers";
|
||||||
|
|
||||||
const LABEL_WIDTH = globals.leftSidebarWidth - 100;
|
const LABEL_WIDTH = globals.leftSidebarWidth - 100;
|
||||||
const ANNO_BUTTON_WIDTH = 50;
|
const ANNO_BUTTON_WIDTH = 50;
|
||||||
@@ -22,6 +22,7 @@ const LABEL_WIDTH_ANNO = LABEL_WIDTH - ANNO_BUTTON_WIDTH;
|
|||||||
categoricalSelection: state.categoricalSelection,
|
categoricalSelection: state.categoricalSelection,
|
||||||
annotations: state.annotations,
|
annotations: state.annotations,
|
||||||
universe: state.universe,
|
universe: state.universe,
|
||||||
|
world: state.world,
|
||||||
schema: state.world?.schema,
|
schema: state.world?.schema,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -30,37 +31,50 @@ class Category extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
|
categorySummary: this.createCategorySummary(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { categoricalSelection, metadataField } = this.props;
|
const { categoricalSelection, metadataField, world } = this.props;
|
||||||
|
let { categorySummary } = this.state;
|
||||||
|
|
||||||
|
if (
|
||||||
|
world !== prevProps.world ||
|
||||||
|
metadataField !== prevProps.metadataField ||
|
||||||
|
!categorySummary
|
||||||
|
) {
|
||||||
|
const newCategorySummary = this.createCategorySummary();
|
||||||
|
if (categorySummary !== newCategorySummary) {
|
||||||
|
categorySummary = newCategorySummary;
|
||||||
|
/* eslint-disable-next-line react/no-did-update-set-state -- Contained in if statement to prevent infinite looping */
|
||||||
|
this.setState({ categorySummary });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cat = categoricalSelection?.[metadataField];
|
const cat = categoricalSelection?.[metadataField];
|
||||||
if (
|
if (
|
||||||
categoricalSelection !== prevProps.categoricalSelection &&
|
categoricalSelection !== prevProps.categoricalSelection &&
|
||||||
!!cat &&
|
!!cat &&
|
||||||
!!this.checkbox
|
!!this.checkbox
|
||||||
) {
|
) {
|
||||||
const categoryCount = {
|
// total number of categories in this dimension
|
||||||
// total number of categories in this dimension
|
const totalCatCount = categorySummary.numCategoryValues;
|
||||||
totalCatCount: cat.numCategoryValues,
|
// number of selected options in this category
|
||||||
// number of selected options in this category
|
const selectedCatCount = categorySummary.categoryValues.reduce(
|
||||||
selectedCatCount: _.reduce(
|
(res, label) => (cat.get(label) ?? true ? res + 1 : res),
|
||||||
cat.categoryValueSelected,
|
0
|
||||||
(res, cond) => (cond ? res + 1 : res),
|
);
|
||||||
0
|
|
||||||
),
|
|
||||||
};
|
|
||||||
/* eslint-disable react/no-did-update-set-state -- Contained in if statement to prevent infinite looping */
|
/* eslint-disable react/no-did-update-set-state -- Contained in if statement to prevent infinite looping */
|
||||||
if (categoryCount.selectedCatCount === categoryCount.totalCatCount) {
|
if (selectedCatCount === totalCatCount) {
|
||||||
/* everything is on, so not indeterminate */
|
/* everything is on, so not indeterminate */
|
||||||
this.checkbox.indeterminate = false;
|
this.checkbox.indeterminate = false;
|
||||||
this.setState({ isChecked: true });
|
this.setState({ isChecked: true });
|
||||||
} else if (categoryCount.selectedCatCount === 0) {
|
} else if (selectedCatCount === 0) {
|
||||||
/* nothing is on, so no */
|
/* nothing is on, so no */
|
||||||
this.checkbox.indeterminate = false;
|
this.checkbox.indeterminate = false;
|
||||||
this.setState({ isChecked: false });
|
this.setState({ isChecked: false });
|
||||||
} else if (categoryCount.selectedCatCount < categoryCount.totalCatCount) {
|
} else if (selectedCatCount < totalCatCount) {
|
||||||
/* to be explicit... */
|
/* to be explicit... */
|
||||||
this.checkbox.indeterminate = true;
|
this.checkbox.indeterminate = true;
|
||||||
this.setState({ isChecked: false });
|
this.setState({ isChecked: false });
|
||||||
@@ -87,27 +101,37 @@ class Category extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createCategorySummary() {
|
||||||
|
const { world, metadataField } = this.props;
|
||||||
|
if (!world || !metadataField || !world.obsAnnotations.hasCol(metadataField))
|
||||||
|
return null;
|
||||||
|
return _createCategorySummary(world, metadataField);
|
||||||
|
}
|
||||||
|
|
||||||
toggleNone() {
|
toggleNone() {
|
||||||
const { dispatch, metadataField } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
|
const { categorySummary } = this.state;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "categorical metadata filter none of these",
|
type: "categorical metadata filter none of these",
|
||||||
metadataField,
|
metadataField,
|
||||||
|
labels: categorySummary.categoryValues,
|
||||||
});
|
});
|
||||||
this.setState({ isChecked: false });
|
this.setState({ isChecked: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAll() {
|
toggleAll() {
|
||||||
const { dispatch, metadataField } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
|
const { categorySummary } = this.state;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "categorical metadata filter all of these",
|
type: "categorical metadata filter all of these",
|
||||||
metadataField,
|
metadataField,
|
||||||
|
labels: categorySummary.categoryValues,
|
||||||
});
|
});
|
||||||
this.setState({ isChecked: true });
|
this.setState({ isChecked: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleToggleAllClick() {
|
handleToggleAllClick() {
|
||||||
const { isChecked } = this.state;
|
const { isChecked } = this.state;
|
||||||
// || this.checkbox.indeterminate === false
|
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
this.toggleNone();
|
this.toggleNone();
|
||||||
} else {
|
} else {
|
||||||
@@ -168,29 +192,19 @@ class Category extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isChecked } = this.state;
|
const { isChecked, categorySummary } = this.state;
|
||||||
const {
|
const { metadataField, isColorAccessor, isExpanded, schema } = this.props;
|
||||||
metadataField,
|
|
||||||
categoricalSelection,
|
|
||||||
isColorAccessor,
|
|
||||||
isExpanded,
|
|
||||||
schema,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isStillLoading = !(categoricalSelection?.[metadataField] ?? false);
|
const isStillLoading = !categorySummary;
|
||||||
if (isStillLoading) {
|
if (isStillLoading) {
|
||||||
return this.renderIsStillLoading();
|
return this.renderIsStillLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkboxID = `category-select-${metadataField}`;
|
const checkboxID = `category-select-${metadataField}`;
|
||||||
|
|
||||||
const isUserAnno =
|
const isUserAnno = !!schema?.annotations?.obsByName[metadataField]
|
||||||
schema?.annotations?.obsByName[metadataField]?.writable ?? false;
|
?.writable;
|
||||||
const isTruncated = _.get(
|
const isTruncated = !!categorySummary?.isTruncated;
|
||||||
categoricalSelection,
|
|
||||||
[metadataField, "isTruncated"],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isUserAnno &&
|
!isUserAnno &&
|
||||||
@@ -217,6 +231,7 @@ class Category extends React.Component {
|
|||||||
metadataField={metadataField}
|
metadataField={metadataField}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
isUserAnno={isUserAnno}
|
isUserAnno={isUserAnno}
|
||||||
|
categorySummary={categorySummary}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { labelPrompt } from "./labelUtil";
|
|||||||
@connect((state) => ({
|
@connect((state) => ({
|
||||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||||
schema: state.world?.schema,
|
schema: state.world?.schema,
|
||||||
config: state.config,
|
|
||||||
ontology: state.ontology,
|
ontology: state.ontology,
|
||||||
}))
|
}))
|
||||||
class Categories extends React.Component {
|
class Categories extends React.Component {
|
||||||
@@ -126,12 +125,11 @@ class Categories extends React.Component {
|
|||||||
newCategoryText,
|
newCategoryText,
|
||||||
expandedCats,
|
expandedCats,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { writableCategoriesEnabled, schema, config, ontology } = this.props;
|
const { writableCategoriesEnabled, schema, ontology } = this.props;
|
||||||
const ontologyEnabled = ontology?.enabled ?? false;
|
const ontologyEnabled = ontology?.enabled ?? false;
|
||||||
/* all names, sorted in display order. Will be rendered in this order */
|
/* all names, sorted in display order. Will be rendered in this order */
|
||||||
const allCategoryNames = ControlsHelpers.selectableCategoryNames(
|
const allCategoryNames = ControlsHelpers.selectableCategoryNames(
|
||||||
schema,
|
schema
|
||||||
ControlsHelpers.maxCategoryItems(config)
|
|
||||||
).sort();
|
).sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ import { AnnotationsHelpers } from "../../../util/stateManager";
|
|||||||
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
import { labelPrompt, isLabelErroneous } from "../labelUtil";
|
||||||
|
|
||||||
/* this is defined outside of the class so we can use it in connect() */
|
/* this is defined outside of the class so we can use it in connect() */
|
||||||
function _currentLabel(ownProps, categoricalSelection) {
|
function _currentLabelAsString(ownProps) {
|
||||||
const { metadataField, categoryIndex } = ownProps;
|
const { categorySummary, categoryIndex } = ownProps;
|
||||||
return String(
|
// when called as a function, the String() constructor performs type conversion,
|
||||||
categoricalSelection[metadataField].categoryValues[categoryIndex]
|
// and returns a primtive string.
|
||||||
).valueOf();
|
return String(categorySummary.categoryValues[categoryIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@connect((state, ownProps) => {
|
@connect((state, ownProps) => {
|
||||||
@@ -33,8 +33,7 @@ function _currentLabel(ownProps, categoricalSelection) {
|
|||||||
const { metadataField } = ownProps;
|
const { metadataField } = ownProps;
|
||||||
const isDilated =
|
const isDilated =
|
||||||
pointDilation.metadataField === metadataField &&
|
pointDilation.metadataField === metadataField &&
|
||||||
pointDilation.categoryField ===
|
pointDilation.categoryField === _currentLabelAsString(ownProps);
|
||||||
_currentLabel(ownProps, categoricalSelection);
|
|
||||||
return {
|
return {
|
||||||
categoricalSelection,
|
categoricalSelection,
|
||||||
annotations: state.annotations,
|
annotations: state.annotations,
|
||||||
@@ -51,28 +50,39 @@ class CategoryValue extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
editedLabelText: this.currentLabel(),
|
editedLabelText: this.currentLabelAsString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { categoricalSelection, metadataField, categoryIndex } = this.props;
|
const {
|
||||||
|
categoricalSelection,
|
||||||
|
metadataField,
|
||||||
|
categoryIndex,
|
||||||
|
categorySummary,
|
||||||
|
} = this.props;
|
||||||
if (
|
if (
|
||||||
prevProps.categoricalSelection !== categoricalSelection ||
|
prevProps.categoricalSelection !== categoricalSelection ||
|
||||||
prevProps.metadataField !== metadataField ||
|
prevProps.metadataField !== metadataField ||
|
||||||
prevProps.categoryIndex !== categoryIndex
|
prevProps.categoryIndex !== categoryIndex ||
|
||||||
|
prevProps.categorySummary !== categorySummary
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line react/no-did-update-set-state --- adequately checked to prevent looping
|
// eslint-disable-next-line react/no-did-update-set-state --- adequately checked to prevent looping
|
||||||
this.setState({
|
this.setState({
|
||||||
editedLabelText: this.currentLabel(),
|
editedLabelText: this.currentLabelAsString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLabel() {
|
||||||
|
const { categoryIndex, categorySummary } = this.props;
|
||||||
|
const label = categorySummary.categoryValues[categoryIndex];
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
handleDeleteValue = () => {
|
handleDeleteValue = () => {
|
||||||
const { dispatch, metadataField } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
const label = this.getLabel();
|
const label = this.getLabel();
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "annotation: delete label",
|
type: "annotation: delete label",
|
||||||
metadataField,
|
metadataField,
|
||||||
@@ -121,7 +131,7 @@ class CategoryValue extends React.Component {
|
|||||||
|
|
||||||
labelNameError = (name) => {
|
labelNameError = (name) => {
|
||||||
const { metadataField, ontology, schema } = this.props;
|
const { metadataField, ontology, schema } = this.props;
|
||||||
if (name === this.currentLabel()) return false;
|
if (name === this.currentLabelAsString()) return false;
|
||||||
return isLabelErroneous(name, metadataField, ontology, schema);
|
return isLabelErroneous(name, metadataField, ontology, schema);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,31 +141,44 @@ class CategoryValue extends React.Component {
|
|||||||
|
|
||||||
activateEditLabelMode = () => {
|
activateEditLabelMode = () => {
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||||
|
const label = this.getLabel();
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "annotation: activate edit label mode",
|
type: "annotation: activate edit label mode",
|
||||||
metadataField,
|
metadataField,
|
||||||
categoryIndex,
|
categoryIndex,
|
||||||
|
label,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
cancelEditMode = () => {
|
cancelEditMode = () => {
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||||
|
const label = this.getLabel();
|
||||||
this.setState({
|
this.setState({
|
||||||
editedLabelText: this.currentLabel(),
|
editedLabelText: this.currentLabelAsString(),
|
||||||
});
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "annotation: cancel edit label mode",
|
type: "annotation: cancel edit label mode",
|
||||||
metadataField,
|
metadataField,
|
||||||
categoryIndex,
|
categoryIndex,
|
||||||
|
label,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleOff = () => {
|
toggleOff = () => {
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
const {
|
||||||
|
dispatch,
|
||||||
|
metadataField,
|
||||||
|
categoryIndex,
|
||||||
|
categorySummary,
|
||||||
|
} = this.props;
|
||||||
|
const labels = categorySummary.categoryValues;
|
||||||
|
const label = labels[categoryIndex];
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "categorical metadata filter deselect",
|
type: "categorical metadata filter deselect",
|
||||||
metadataField,
|
metadataField,
|
||||||
categoryIndex,
|
categoryIndex,
|
||||||
|
label,
|
||||||
|
labels,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,16 +193,24 @@ class CategoryValue extends React.Component {
|
|||||||
If and only if true, update the component
|
If and only if true, update the component
|
||||||
*/
|
*/
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
const { metadataField, categoryIndex, categoricalSelection } = props;
|
const {
|
||||||
const { categoricalSelection: newCategoricalSelection } = nextProps;
|
metadataField,
|
||||||
|
categoryIndex,
|
||||||
|
categoricalSelection,
|
||||||
|
categorySummary,
|
||||||
|
} = props;
|
||||||
|
const {
|
||||||
|
categoryIndex: newCategoryIndex,
|
||||||
|
categoricalSelection: newCategoricalSelection,
|
||||||
|
categorySummary: newCategorySummary,
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
|
const label = categorySummary.categoryValues[categoryIndex];
|
||||||
|
const newLabel = newCategorySummary.categoryValues[newCategoryIndex];
|
||||||
|
const labelChanged = label !== newLabel;
|
||||||
const valueSelectionChange =
|
const valueSelectionChange =
|
||||||
categoricalSelection[metadataField].categoryValueSelected[
|
categoricalSelection[metadataField].get(label) !==
|
||||||
categoryIndex
|
newCategoricalSelection[metadataField].get(newLabel);
|
||||||
] !==
|
|
||||||
newCategoricalSelection[metadataField].categoryValueSelected[
|
|
||||||
categoryIndex
|
|
||||||
];
|
|
||||||
|
|
||||||
const worldChange = props.world !== nextProps.world;
|
const worldChange = props.world !== nextProps.world;
|
||||||
const colorAccessorChange = props.colorAccessor !== nextProps.colorAccessor;
|
const colorAccessorChange = props.colorAccessor !== nextProps.colorAccessor;
|
||||||
@@ -189,41 +220,60 @@ class CategoryValue extends React.Component {
|
|||||||
const editingLabel = state.editedLabelText !== nextState.editedLabelText;
|
const editingLabel = state.editedLabelText !== nextState.editedLabelText;
|
||||||
const dilationChange = props.isDilated !== nextProps.isDilated;
|
const dilationChange = props.isDilated !== nextProps.isDilated;
|
||||||
|
|
||||||
|
const count = categorySummary.categoryValueCounts[categoryIndex];
|
||||||
|
const newCount = newCategorySummary.categoryValueCounts[newCategoryIndex];
|
||||||
|
const countChanged = count !== newCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
labelChanged ||
|
||||||
valueSelectionChange ||
|
valueSelectionChange ||
|
||||||
worldChange ||
|
worldChange ||
|
||||||
colorAccessorChange ||
|
colorAccessorChange ||
|
||||||
annotationsChange ||
|
annotationsChange ||
|
||||||
crossfilterChange ||
|
crossfilterChange ||
|
||||||
editingLabel ||
|
editingLabel ||
|
||||||
dilationChange
|
dilationChange ||
|
||||||
|
countChanged
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleOn = () => {
|
toggleOn = () => {
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
const {
|
||||||
|
dispatch,
|
||||||
|
metadataField,
|
||||||
|
categoryIndex,
|
||||||
|
categorySummary,
|
||||||
|
} = this.props;
|
||||||
|
const labels = categorySummary.categoryValues;
|
||||||
|
const label = labels[categoryIndex];
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "categorical metadata filter select",
|
type: "categorical metadata filter select",
|
||||||
metadataField,
|
metadataField,
|
||||||
categoryIndex,
|
categoryIndex,
|
||||||
|
label,
|
||||||
|
labels,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMouseEnter = () => {
|
handleMouseEnter = () => {
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||||
|
const label = this.getLabel();
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "category value mouse hover start",
|
type: "category value mouse hover start",
|
||||||
metadataField,
|
metadataField,
|
||||||
categoryIndex,
|
categoryIndex,
|
||||||
|
label,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMouseExit = () => {
|
handleMouseExit = () => {
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||||
|
const label = this.getLabel();
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "category value mouse hover end",
|
type: "category value mouse hover end",
|
||||||
metadataField,
|
metadataField,
|
||||||
categoryIndex,
|
categoryIndex,
|
||||||
|
label,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,17 +286,8 @@ class CategoryValue extends React.Component {
|
|||||||
this.setState({ editedLabelText: e.target });
|
this.setState({ editedLabelText: e.target });
|
||||||
};
|
};
|
||||||
|
|
||||||
getLabel = () => {
|
currentLabelAsString() {
|
||||||
const { metadataField, categoryIndex, categoricalSelection } = this.props;
|
return _currentLabelAsString(this.props);
|
||||||
const category = categoricalSelection[metadataField];
|
|
||||||
const label = category.categoryValues[categoryIndex];
|
|
||||||
|
|
||||||
return label;
|
|
||||||
};
|
|
||||||
|
|
||||||
currentLabel() {
|
|
||||||
const { categoricalSelection } = this.props;
|
|
||||||
return _currentLabel(this.props, categoricalSelection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isAddCurrentSelectionDisabled(category, value) {
|
isAddCurrentSelectionDisabled(category, value) {
|
||||||
@@ -294,6 +335,7 @@ class CategoryValue extends React.Component {
|
|||||||
flippedProps,
|
flippedProps,
|
||||||
isDilated,
|
isDilated,
|
||||||
world,
|
world,
|
||||||
|
categorySummary,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const ontologyEnabled = ontology?.enabled ?? false;
|
const ontologyEnabled = ontology?.enabled ?? false;
|
||||||
|
|
||||||
@@ -302,10 +344,10 @@ class CategoryValue extends React.Component {
|
|||||||
if (!categoricalSelection) return null;
|
if (!categoricalSelection) return null;
|
||||||
|
|
||||||
const category = categoricalSelection[metadataField];
|
const category = categoricalSelection[metadataField];
|
||||||
const selected = category.categoryValueSelected[categoryIndex];
|
const selected = category.get(this.getLabel()) ?? true;
|
||||||
const count = category.categoryValueCounts[categoryIndex];
|
const count = categorySummary.categoryValueCounts[categoryIndex];
|
||||||
const value = category.categoryValues[categoryIndex];
|
const value = categorySummary.categoryValues[categoryIndex];
|
||||||
const displayString = this.currentLabel();
|
const displayString = this.currentLabelAsString();
|
||||||
|
|
||||||
/* this is the color scale, so add swatches below */
|
/* this is the color scale, so add swatches below */
|
||||||
const isColorBy = metadataField === colorAccessor;
|
const isColorBy = metadataField === colorAccessor;
|
||||||
|
|||||||
@@ -40,10 +40,7 @@ class CentroidLabels extends PureComponent {
|
|||||||
if (!colorAccessor || labels.size === undefined || labels.size === 0)
|
if (!colorAccessor || labels.size === undefined || labels.size === 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const {
|
const category = categoricalSelection[colorAccessor];
|
||||||
categoryValueIndices,
|
|
||||||
categoryValueSelected,
|
|
||||||
} = categoricalSelection?.[colorAccessor];
|
|
||||||
|
|
||||||
const labelSVGS = [];
|
const labelSVGS = [];
|
||||||
let fontSize = "15px";
|
let fontSize = "15px";
|
||||||
@@ -57,7 +54,7 @@ class CentroidLabels extends PureComponent {
|
|||||||
fontWeight = "800";
|
fontWeight = "800";
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = categoryValueSelected[categoryValueIndices.get(label)];
|
const selected = category.get(label) ?? true;
|
||||||
|
|
||||||
// Mirror LSB middle truncation
|
// Mirror LSB middle truncation
|
||||||
let displayLabel = label;
|
let displayLabel = label;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const configDefaults = {
|
|||||||
features: {},
|
features: {},
|
||||||
displayNames: {},
|
displayNames: {},
|
||||||
parameters: {
|
parameters: {
|
||||||
"max-category-items": 1000,
|
|
||||||
"disable-diffexp": false,
|
"disable-diffexp": false,
|
||||||
"diffexp-may-be-slow": false,
|
"diffexp-may-be-slow": false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { ControlsHelpers as CH } from "../util/stateManager";
|
import { ControlsHelpers as CH } from "../util/stateManager";
|
||||||
|
|
||||||
const CategoricalSelection = (
|
/*
|
||||||
state,
|
State is an object, with a key for each categorical annotation, and a
|
||||||
action,
|
value which is a Map of label->t/f, reflecting selection state for the label.
|
||||||
nextSharedState,
|
|
||||||
prevSharedState
|
Label state default (if missing) is up to the component, but typically true.
|
||||||
) => {
|
|
||||||
|
{
|
||||||
|
"louvain": Map(),
|
||||||
|
...
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const CategoricalSelection = (state, action, nextSharedState) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "initial data load complete (universe exists)":
|
case "initial data load complete (universe exists)":
|
||||||
case "set World to current selection":
|
case "set World to current selection":
|
||||||
@@ -13,11 +19,7 @@ const CategoricalSelection = (
|
|||||||
case "set clip quantiles": {
|
case "set clip quantiles": {
|
||||||
const { world } = nextSharedState;
|
const { world } = nextSharedState;
|
||||||
const newState = CH.createCategoricalSelection(
|
const newState = CH.createCategoricalSelection(
|
||||||
world,
|
CH.selectableCategoryNames(world.schema)
|
||||||
CH.selectableCategoryNames(
|
|
||||||
world.schema,
|
|
||||||
CH.maxCategoryItems(prevSharedState.config)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
@@ -30,13 +32,12 @@ const CategoricalSelection = (
|
|||||||
const { world } = nextSharedState;
|
const { world } = nextSharedState;
|
||||||
const names = CH.selectableCategoryNames(
|
const names = CH.selectableCategoryNames(
|
||||||
world.schema,
|
world.schema,
|
||||||
CH.maxCategoryItems(prevSharedState.config),
|
|
||||||
dataframe.colIndex.labels()
|
dataframe.colIndex.labels()
|
||||||
);
|
);
|
||||||
if (names.length === 0) return state;
|
if (names.length === 0) return state;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...CH.createCategoricalSelection(world, names),
|
...CH.createCategoricalSelection(names),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,16 +45,12 @@ const CategoricalSelection = (
|
|||||||
/*
|
/*
|
||||||
Set the specific category in this field to false
|
Set the specific category in this field to false
|
||||||
*/
|
*/
|
||||||
const newCategoryValueSelected = Array.from(
|
const { metadataField, label } = action;
|
||||||
state[action.metadataField].categoryValueSelected
|
const newSelected = new Map(state[metadataField]);
|
||||||
);
|
newSelected.set(label, true);
|
||||||
newCategoryValueSelected[action.categoryIndex] = true;
|
|
||||||
const newCategoricalSelection = {
|
const newCategoricalSelection = {
|
||||||
...state,
|
...state,
|
||||||
[action.metadataField]: {
|
[action.metadataField]: newSelected,
|
||||||
...state[action.metadataField],
|
|
||||||
categoryValueSelected: newCategoryValueSelected,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return newCategoricalSelection;
|
return newCategoricalSelection;
|
||||||
}
|
}
|
||||||
@@ -62,16 +59,12 @@ const CategoricalSelection = (
|
|||||||
/*
|
/*
|
||||||
Set the specific category in this field to false
|
Set the specific category in this field to false
|
||||||
*/
|
*/
|
||||||
const newCategoryValueSelected = Array.from(
|
const { metadataField, label } = action;
|
||||||
state[action.metadataField].categoryValueSelected
|
const newSelected = new Map(state[metadataField]);
|
||||||
);
|
newSelected.set(label, false);
|
||||||
newCategoryValueSelected[action.categoryIndex] = false;
|
|
||||||
const newCategoricalSelection = {
|
const newCategoricalSelection = {
|
||||||
...state,
|
...state,
|
||||||
[action.metadataField]: {
|
[action.metadataField]: newSelected,
|
||||||
...state[action.metadataField],
|
|
||||||
categoryValueSelected: newCategoryValueSelected,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return newCategoricalSelection;
|
return newCategoricalSelection;
|
||||||
}
|
}
|
||||||
@@ -80,15 +73,13 @@ const CategoricalSelection = (
|
|||||||
/*
|
/*
|
||||||
set all categories in this field to false.
|
set all categories in this field to false.
|
||||||
*/
|
*/
|
||||||
|
const { metadataField, labels } = action;
|
||||||
|
const { selected } = state[metadataField];
|
||||||
|
const newSelected = new Map(selected);
|
||||||
|
labels.forEach((label) => newSelected.set(label, false));
|
||||||
const newCategoricalSelection = {
|
const newCategoricalSelection = {
|
||||||
...state,
|
...state,
|
||||||
[action.metadataField]: {
|
[action.metadataField]: newSelected,
|
||||||
...state[action.metadataField],
|
|
||||||
categorySelected: false,
|
|
||||||
categoryValueSelected: Array.from(
|
|
||||||
state[action.metadataField].categoryValueSelected
|
|
||||||
).fill(false),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return newCategoricalSelection;
|
return newCategoricalSelection;
|
||||||
}
|
}
|
||||||
@@ -97,25 +88,22 @@ const CategoricalSelection = (
|
|||||||
/*
|
/*
|
||||||
set all categories in this field to true.
|
set all categories in this field to true.
|
||||||
*/
|
*/
|
||||||
|
const { metadataField, labels } = action;
|
||||||
|
const { selected } = state[metadataField];
|
||||||
|
const newSelected = new Map(selected);
|
||||||
|
labels.forEach((label) => newSelected.set(label, true));
|
||||||
const newCategoricalSelection = {
|
const newCategoricalSelection = {
|
||||||
...state,
|
...state,
|
||||||
[action.metadataField]: {
|
[action.metadataField]: newSelected,
|
||||||
...state[action.metadataField],
|
|
||||||
categorySelected: true,
|
|
||||||
categoryValueSelected: Array.from(
|
|
||||||
state[action.metadataField].categoryValueSelected
|
|
||||||
).fill(true),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return newCategoricalSelection;
|
return newCategoricalSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "annotation: create category": {
|
case "annotation: create category": {
|
||||||
const { world } = nextSharedState;
|
|
||||||
const name = action.data;
|
const name = action.data;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...CH.createCategoricalSelection(world, [name]),
|
...CH.createCategoricalSelection([name]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,12 +126,11 @@ const CategoricalSelection = (
|
|||||||
case "annotation: label edited":
|
case "annotation: label edited":
|
||||||
case "annotation: delete label": {
|
case "annotation: delete label": {
|
||||||
/* need to rebuild the state for this annotation */
|
/* need to rebuild the state for this annotation */
|
||||||
const { world } = nextSharedState;
|
|
||||||
const name = action.metadataField;
|
const name = action.metadataField;
|
||||||
const { [name]: _, ...partialState } = state;
|
const { [name]: _, ...partialState } = state;
|
||||||
return {
|
return {
|
||||||
...partialState,
|
...partialState,
|
||||||
...CH.createCategoricalSelection(world, [name]),
|
...CH.createCategoricalSelection([name]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,7 @@ const centroidLabels = (state = initialState, action, sharedNextState) => {
|
|||||||
...state,
|
...state,
|
||||||
labels:
|
labels:
|
||||||
!!colorAccessor && showLabels && !!categoricalSelection[colorAccessor]
|
!!colorAccessor && showLabels && !!categoricalSelection[colorAccessor]
|
||||||
? calcCentroid(
|
? calcCentroid(world, colorAccessor, layoutChoice.currentDimNames)
|
||||||
world.obsAnnotations,
|
|
||||||
world.obsLayout,
|
|
||||||
colorAccessor,
|
|
||||||
layoutChoice.currentDimNames,
|
|
||||||
categoricalSelection,
|
|
||||||
world.schema.annotations.obsByName
|
|
||||||
)
|
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,12 +45,9 @@ const centroidLabels = (state = initialState, action, sharedNextState) => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
labels: calcCentroid(
|
labels: calcCentroid(
|
||||||
world.obsAnnotations,
|
world,
|
||||||
world.obsLayout,
|
|
||||||
colorAccessor,
|
colorAccessor,
|
||||||
layoutChoice.currentDimNames,
|
layoutChoice.currentDimNames
|
||||||
categoricalSelection,
|
|
||||||
world.schema.annotations.obsByName
|
|
||||||
),
|
),
|
||||||
showLabels,
|
showLabels,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -260,10 +260,9 @@ const CrossfilterReducerBase = (
|
|||||||
|
|
||||||
case "categorical metadata filter select":
|
case "categorical metadata filter select":
|
||||||
case "categorical metadata filter deselect": {
|
case "categorical metadata filter deselect": {
|
||||||
const { categoricalSelection } = nextSharedState;
|
const { labels, metadataField } = action;
|
||||||
const cat = categoricalSelection[action.metadataField];
|
const selected = nextSharedState.categoricalSelection[metadataField];
|
||||||
const { categoryValues, categoryValueSelected } = cat;
|
const values = labels.filter((label) => selected.get(label) ?? true);
|
||||||
const values = categoryValues.filter((v, i) => categoryValueSelected[i]);
|
|
||||||
return state.select(obsAnnoDimensionName(action.metadataField), {
|
return state.select(obsAnnoDimensionName(action.metadataField), {
|
||||||
mode: "exact",
|
mode: "exact",
|
||||||
values,
|
values,
|
||||||
|
|||||||
@@ -3,12 +3,8 @@ const initialState = {
|
|||||||
categoryField: "",
|
categoryField: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const pointDialation = (state = initialState, action, sharedNextState) => {
|
const pointDialation = (state = initialState, action) => {
|
||||||
const { categoricalSelection } = sharedNextState;
|
const { metadataField, label: categoryField } = action;
|
||||||
const { metadataField, categoryIndex } = action;
|
|
||||||
const categoryField =
|
|
||||||
action.categoryField ||
|
|
||||||
categoricalSelection?.[metadataField]?.categoryValues[categoryIndex];
|
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "category value mouse hover start":
|
case "category value mouse hover start":
|
||||||
|
|||||||
+22
-49
@@ -1,6 +1,10 @@
|
|||||||
import quantile from "./quantile";
|
import quantile from "./quantile";
|
||||||
import { memoize } from "./dataframe/util";
|
import { memoize } from "./dataframe/util";
|
||||||
import { unassignedCategoryLabel } from "../globals";
|
import { unassignedCategoryLabel } from "../globals";
|
||||||
|
import {
|
||||||
|
createCategorySummary,
|
||||||
|
isSelectableCategoryName,
|
||||||
|
} from "./stateManager/controlsHelpers";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Centroid coordinate calculation
|
Centroid coordinate calculation
|
||||||
@@ -18,31 +22,24 @@ label -> {
|
|||||||
yCoordinates: Float32Array
|
yCoordinates: Float32Array
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
const getCoordinatesByLabel = (
|
const getCoordinatesByLabel = (world, categoryName, layoutDimNames) => {
|
||||||
obsAnnotations,
|
|
||||||
obsLayout,
|
|
||||||
categoryName,
|
|
||||||
layoutDimNames,
|
|
||||||
categoricalSelection,
|
|
||||||
schemaObsByName
|
|
||||||
) => {
|
|
||||||
const coordsByCategoryLabel = new Map();
|
const coordsByCategoryLabel = new Map();
|
||||||
|
|
||||||
const categoryArray = obsAnnotations.col(categoryName).asArray();
|
|
||||||
|
|
||||||
const layoutXArray = obsLayout.col(layoutDimNames[0]).asArray();
|
|
||||||
const layoutYArray = obsLayout.col(layoutDimNames[1]).asArray();
|
|
||||||
|
|
||||||
const { categoryValueIndices, categoryValueCounts } =
|
|
||||||
categoricalSelection?.[categoryName] || {};
|
|
||||||
|
|
||||||
// If the coloredBy is not a categorical col
|
// If the coloredBy is not a categorical col
|
||||||
if (categoryValueIndices === undefined) {
|
if (!isSelectableCategoryName(world.schema, categoryName)) {
|
||||||
return coordsByCategoryLabel;
|
return coordsByCategoryLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if the current category is a user created annotation
|
const { obsAnnotations, obsLayout } = world;
|
||||||
const isUserAnno = schemaObsByName[categoryName].writable;
|
const categoryArray = obsAnnotations.col(categoryName).asArray();
|
||||||
|
const layoutXArray = obsLayout.col(layoutDimNames[0]).asArray();
|
||||||
|
const layoutYArray = obsLayout.col(layoutDimNames[1]).asArray();
|
||||||
|
|
||||||
|
const categorySummary = createCategorySummary(world, categoryName);
|
||||||
|
const {
|
||||||
|
isUserAnno,
|
||||||
|
categoryValueIndices,
|
||||||
|
categoryValueCounts,
|
||||||
|
} = categorySummary;
|
||||||
|
|
||||||
// Iterate over all cells
|
// Iterate over all cells
|
||||||
for (let i = 0, len = categoryArray.length; i < len; i += 1) {
|
for (let i = 0, len = categoryArray.length; i < len; i += 1) {
|
||||||
@@ -96,23 +93,9 @@ const getCoordinatesByLabel = (
|
|||||||
label -> [x-Coordinate, y-Coordinate]
|
label -> [x-Coordinate, y-Coordinate]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const calcMedianCentroid = (
|
const calcMedianCentroid = (world, categoryName, layoutDimNames) => {
|
||||||
obsAnnotations,
|
|
||||||
obsLayout,
|
|
||||||
categoryName,
|
|
||||||
layoutDimNames,
|
|
||||||
categoricalSelection,
|
|
||||||
schemaObsByName
|
|
||||||
) => {
|
|
||||||
// generate a map describing the coordinates for each label within the given category
|
// generate a map describing the coordinates for each label within the given category
|
||||||
const dataMap = getCoordinatesByLabel(
|
const dataMap = getCoordinatesByLabel(world, categoryName, layoutDimNames);
|
||||||
obsAnnotations,
|
|
||||||
obsLayout,
|
|
||||||
categoryName,
|
|
||||||
layoutDimNames,
|
|
||||||
categoricalSelection,
|
|
||||||
schemaObsByName
|
|
||||||
);
|
|
||||||
|
|
||||||
// label => [medianXCoordinate, medianYCoordinate]
|
// label => [medianXCoordinate, medianYCoordinate]
|
||||||
const coordinates = new Map();
|
const coordinates = new Map();
|
||||||
@@ -137,19 +120,9 @@ const calcMedianCentroid = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// A simple function to hash the parameters
|
// A simple function to hash the parameters
|
||||||
const hashMedianCentroid = (
|
const hashMedianCentroid = (world, categoryName, layoutDimNames) => {
|
||||||
obsAnnotations,
|
const { obsAnnotations, obsLayout } = world;
|
||||||
obsLayout,
|
return `${obsAnnotations.__id}+${obsLayout.__id}:${categoryName}:${layoutDimNames}`;
|
||||||
categoryName,
|
|
||||||
layoutDimNames,
|
|
||||||
categorySelection,
|
|
||||||
schemaObsByName
|
|
||||||
) => {
|
|
||||||
return `${obsAnnotations.__id}+${
|
|
||||||
obsLayout.__id
|
|
||||||
}:${categoryName}:${layoutDimNames}:${Object.keys(
|
|
||||||
categorySelection
|
|
||||||
)}:${Object.keys(schemaObsByName)}`;
|
|
||||||
};
|
};
|
||||||
// export the memoized calculation function
|
// export the memoized calculation function
|
||||||
export default memoize(calcMedianCentroid, hashMedianCentroid);
|
export default memoize(calcMedianCentroid, hashMedianCentroid);
|
||||||
|
|||||||
@@ -6,17 +6,12 @@ import _ from "lodash";
|
|||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import { rangeFill as fillRange } from "../range";
|
import { rangeFill as fillRange } from "../range";
|
||||||
|
import fromEntries from "../fromEntries";
|
||||||
import {
|
import {
|
||||||
userDefinedDimensionName,
|
userDefinedDimensionName,
|
||||||
diffexpDimensionName,
|
diffexpDimensionName,
|
||||||
} from "../nameCreators";
|
} from "../nameCreators";
|
||||||
|
import { isCategoricalAnnotation } from "./annotationsHelpers";
|
||||||
export function maxCategoryItems(config) {
|
|
||||||
return (
|
|
||||||
config.parameters?.["max-category-items"] ??
|
|
||||||
globals.configDefaults.parameters["max-category-items"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Selection state for categoricals are tracked in an Object that
|
Selection state for categoricals are tracked in an Object that
|
||||||
@@ -69,7 +64,12 @@ function topNCategories(colSchema, summary, N) {
|
|||||||
return [_topNCategories, topNCounts];
|
return [_topNCategories, topNCounts];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectableCategoryNames(schema, maxCatItems, names) {
|
export function isSelectableCategoryName(schema, name) {
|
||||||
|
const { index } = schema.annotations.obs;
|
||||||
|
return name && name !== index && isCategoricalAnnotation(schema, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectableCategoryNames(schema, names) {
|
||||||
/*
|
/*
|
||||||
return all obs annotation names that are categorical AND have a
|
return all obs annotation names that are categorical AND have a
|
||||||
"reasonably" small number of categories AND are not the index column.
|
"reasonably" small number of categories AND are not the index column.
|
||||||
@@ -77,56 +77,44 @@ export function selectableCategoryNames(schema, maxCatItems, names) {
|
|||||||
If the initial name list not provided, use everything in the schema.
|
If the initial name list not provided, use everything in the schema.
|
||||||
*/
|
*/
|
||||||
if (!schema) return [];
|
if (!schema) return [];
|
||||||
const { index, columns } = schema.annotations.obs;
|
if (!names) names = schema.annotations.obs.columns.map((c) => c.name);
|
||||||
|
return names.filter((name) => isSelectableCategoryName(schema, name));
|
||||||
return columns
|
|
||||||
.filter((colSchema) => !names || names.indexOf(colSchema.name) !== -1)
|
|
||||||
.filter((colSchema) => {
|
|
||||||
const { type, name } = colSchema;
|
|
||||||
const isSelectableType =
|
|
||||||
type === "string" || type === "boolean" || type === "categorical";
|
|
||||||
return isSelectableType && name !== index;
|
|
||||||
})
|
|
||||||
.map((v) => v.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCategoricalSelection(world, names) {
|
export function createCategorySummary(world, name) {
|
||||||
const N = globals.maxCategoricalOptionsToDisplay;
|
const N = globals.maxCategoricalOptionsToDisplay;
|
||||||
const { obsAnnotations, schema } = world;
|
const { obsAnnotations, schema } = world;
|
||||||
|
|
||||||
const res = names.reduce((acc, name) => {
|
const colSchema = schema.annotations.obsByName[name];
|
||||||
const colSchema = schema.annotations.obsByName[name];
|
const { writable: isUserAnno } = colSchema;
|
||||||
const { writable: isUserAnno } = colSchema;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Summarize the annotation data currently in world. Must return categoryValues
|
Summarize the annotation data currently in world. Must return categoryValues
|
||||||
in sorted order, and must include all category values even if they are not
|
in sorted order, and must include all category values even if they are not
|
||||||
actively used in the current world.
|
actively used in the current world.
|
||||||
*/
|
*/
|
||||||
const summary = obsAnnotations.col(name).summarizeCategorical();
|
const summary = obsAnnotations.col(name).summarizeCategorical();
|
||||||
const [categoryValues, categoryValueCounts] = topNCategories(
|
const [categoryValues, categoryValueCounts] = topNCategories(
|
||||||
colSchema,
|
colSchema,
|
||||||
summary,
|
summary,
|
||||||
N
|
N
|
||||||
);
|
);
|
||||||
const categoryValueIndices = new Map(categoryValues.map((v, i) => [v, i]));
|
const categoryValueIndices = new Map(categoryValues.map((v, i) => [v, i]));
|
||||||
const numCategoryValues = categoryValueIndices.size;
|
const numCategoryValues = categoryValueIndices.size;
|
||||||
const categoryValueSelected = new Array(numCategoryValues).fill(true);
|
const isTruncated = categoryValues.length < summary.numCategories;
|
||||||
const isTruncated = categoryValues.length < summary.numCategories;
|
|
||||||
|
|
||||||
acc[name] = {
|
return {
|
||||||
categoryValues, // array: of natively typed category values
|
categoryValues, // array: of natively typed category values
|
||||||
categoryValueIndices, // map: category value (native type) -> category index
|
categoryValueIndices, // map: category value (native type) -> category index
|
||||||
categoryValueSelected, // array: t/f selection state
|
numCategoryValues, // number: of values in the category
|
||||||
numCategoryValues, // number: of values in the category
|
isTruncated, // bool: true if list was truncated
|
||||||
isTruncated, // bool: true if list was truncated
|
categoryValueCounts, // array: cardinality of each category,
|
||||||
categoryValueCounts, // array: cardinality of each category,
|
isUserAnno, // bool
|
||||||
categorySelected: true, // bool - default state for entire category
|
};
|
||||||
isUserAnno, // bool
|
}
|
||||||
};
|
|
||||||
return acc;
|
export function createCategoricalSelection(names) {
|
||||||
}, {});
|
return fromEntries(names.map((name) => [name, new Map()]));
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user