mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-03 21:18:11 +08:00
Remove unnecessary files
This commit is contained in:
-20
@@ -1,20 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2013
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
recursive-include server/app/web/templates *
|
|
||||||
recursive-include server/app/web/static *
|
|
||||||
|
|
||||||
include server/requirements.txt
|
|
||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
# cellxgene roadmap
|
|
||||||
|
|
||||||
We are very exited for _cellxgene_ to become a valuable tool in collaborations
|
|
||||||
between computational biologists and experimental biologists working on
|
|
||||||
single-cell transcriptomics data. _cellxgene_ is in active development, and we
|
|
||||||
would love to include the community as we plan new features to work on. If you
|
|
||||||
have questions of feedback about this roadmap, please submit an issue on
|
|
||||||
GitHub.
|
|
||||||
|
|
||||||
Please note: this roadmap is subject to change.
|
|
||||||
|
|
||||||
*Last updated: April 11, 2019*
|
|
||||||
|
|
||||||
## what we are building now
|
|
||||||
|
|
||||||
In the near term, our goal is to enable teams of computational and experimental
|
|
||||||
biologists to collaboratively explore and annotate their single-cell RNA-seq data.
|
|
||||||
|
|
||||||
There are 4 key features we plan to implement in the near term.
|
|
||||||
|
|
||||||
- Click install and launch
|
|
||||||
- Manual annotation workflows
|
|
||||||
- Toggle embeddings
|
|
||||||
- Gene information
|
|
||||||
|
|
||||||
### simple install and launch
|
|
||||||
|
|
||||||
The command line interface for installing and launching cellxgene is a barrier
|
|
||||||
for users who are not used to Python or using the command line. We plan to
|
|
||||||
support installation and launch of cellxgene on Mac and Windows. See
|
|
||||||
[Issue #687](https://github.com/chanzuckerberg/cellxgene/issues/687) for more details.
|
|
||||||
|
|
||||||
### manual annotation workflows
|
|
||||||
|
|
||||||
The exploratory visualization that cellxgene offers is critical for manual
|
|
||||||
annotation workflows, especially in collaborative environments. We plan to
|
|
||||||
support manually annotate cells with labels (i.e., cell type or QC flags) for
|
|
||||||
downstream analysis. See [Issue #524](https://github.com/chanzuckerberg/cellxgene/issues/524)
|
|
||||||
for more details.
|
|
||||||
|
|
||||||
### toggle embeddings
|
|
||||||
|
|
||||||
While a single dataset may have multiple embeddings calculated (tSNE, umap, in
|
|
||||||
situ coordinates, trajectories, etc), cellxgene currently requires the user to select the
|
|
||||||
embedding to use in the main layout at launch. We plan to support letting users
|
|
||||||
toggle between any embedding present in a file from the cellxgene interface.
|
|
||||||
See [Issue #594](https://github.com/chanzuckerberg/cellxgene/issues/594) for details.
|
|
||||||
|
|
||||||
### gene information
|
|
||||||
|
|
||||||
Differential expression returns only the names of genes, but no additional information
|
|
||||||
about gene metadata, function, or known associations. We plan to help users learn
|
|
||||||
more about genes they discover by exposing additional gene metadata. See
|
|
||||||
[Issue #96](https://github.com/chanzuckerberg/cellxgene/issues/96) for details.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export const jest_env = process.env.JEST_ENV || "dev";
|
|
||||||
export const appPort = process.env.JEST_CXG_PORT || 3000;
|
|
||||||
export const appUrlBase = `http://localhost:${appPort}`;
|
|
||||||
export const DEV = jest_env === "dev";
|
|
||||||
export const DEBUG = jest_env === "debug";
|
|
||||||
export const DATASET = "pbmc3k";
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
export const datasets = {
|
|
||||||
pbmc3k: {
|
|
||||||
title: "pbmc3k",
|
|
||||||
dataframe: {
|
|
||||||
nObs: "2638",
|
|
||||||
nVar: "1838",
|
|
||||||
type: "float32"
|
|
||||||
},
|
|
||||||
categorical: {
|
|
||||||
louvain: {
|
|
||||||
"B cells": "342",
|
|
||||||
"CD14+ Monocytes": "480",
|
|
||||||
"CD4 T cells": "1144",
|
|
||||||
"CD8 T cells": "316",
|
|
||||||
"Dendritic cells": "37",
|
|
||||||
"FCGR3A+ Monocytes": "150",
|
|
||||||
Megakaryocytes: "15",
|
|
||||||
"NK cells": "154"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
continuous: {
|
|
||||||
n_genes: "int32",
|
|
||||||
percent_mito: "float32",
|
|
||||||
n_counts: "float32"
|
|
||||||
},
|
|
||||||
cellsets: {
|
|
||||||
lasso: [
|
|
||||||
{
|
|
||||||
"coordinates-as-percent": { x1: 0.05, y1: 0.25, x2: 0.15, y2: 0.35 },
|
|
||||||
count: "71"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
categorical: [
|
|
||||||
{
|
|
||||||
metadata: "louvain",
|
|
||||||
values: ["B cells", "Megakaryocytes"],
|
|
||||||
count: "357"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
continuous: [
|
|
||||||
{
|
|
||||||
metadata: "n_genes",
|
|
||||||
"coordinates-as-percent": { x1: 0.25, y1: 0.5, x2: 0.55, y2: 0.5 },
|
|
||||||
count: "1537"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
diffexp: {
|
|
||||||
cellset1: [
|
|
||||||
{ kind: "categorical", metadata: "louvain", values: ["B cells"] }
|
|
||||||
],
|
|
||||||
cellset2: [
|
|
||||||
{
|
|
||||||
kind: "categorical",
|
|
||||||
metadata: "louvain",
|
|
||||||
values: ["CD4 T cells", "NK cells"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"gene-results": [
|
|
||||||
"HLA-DRB1",
|
|
||||||
"HLA-DPB1",
|
|
||||||
"CD79A",
|
|
||||||
"HLA-DPA1",
|
|
||||||
"HLA-DQA1",
|
|
||||||
"CD79B",
|
|
||||||
"HLA-DQB1",
|
|
||||||
"MS4A1",
|
|
||||||
"IL32",
|
|
||||||
"CD37"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
genes: {
|
|
||||||
bulkadd: ["S100A8", "FCGR3A", "LGALS2", "GSTP1"],
|
|
||||||
search: "ACD"
|
|
||||||
},
|
|
||||||
subset: {
|
|
||||||
cellset1: [
|
|
||||||
{
|
|
||||||
kind: "categorical",
|
|
||||||
metadata: "louvain",
|
|
||||||
values: ["B cells", "Megakaryocytes"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
count: "357",
|
|
||||||
categorical: {
|
|
||||||
louvain: {
|
|
||||||
"B cells": "342",
|
|
||||||
Megakaryocytes: "15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lasso: {
|
|
||||||
"coordinates-as-percent": { x1: 0.45, y1: 0.05, x2: 0.65, y2: 0.15 },
|
|
||||||
count: "36"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scatter: {
|
|
||||||
genes: { x: "S100A8", y: "FCGR3A" }
|
|
||||||
},
|
|
||||||
pan: {
|
|
||||||
"coordinates-as-percent": { x1: 0.75, y1: 0.75, x2: 0.35, y2: 0.35 }
|
|
||||||
},
|
|
||||||
features: {
|
|
||||||
panzoom: {
|
|
||||||
lasso: {
|
|
||||||
"coordinates-as-percent": { x1: 0.3, y1: 0.3, x2: 0.5, y2: 0.5 },
|
|
||||||
count: "24"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clip: {
|
|
||||||
min: "30",
|
|
||||||
max: "70",
|
|
||||||
metadata: "n_genes",
|
|
||||||
gene: "S100A8",
|
|
||||||
"coordinates-as-percent": { x1: 0.25, y1: 0.5, x2: 0.55, y2: 0.5 },
|
|
||||||
count: "392",
|
|
||||||
"gene-cell-count": "421"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
/*
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
import puppeteer from "puppeteer";
|
|
||||||
import { appUrlBase, DEBUG, DEV, DATASET } from "./config";
|
|
||||||
import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils";
|
|
||||||
import { datasets } from "./data";
|
|
||||||
|
|
||||||
let browser, page, utils, cxgActions, spy;
|
|
||||||
const browserViewport = { width: 1280, height: 960 };
|
|
||||||
let data = datasets[DATASET];
|
|
||||||
|
|
||||||
if (DEBUG) jest.setTimeout(100000);
|
|
||||||
if (DEV) jest.setTimeout(10000);
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const browserParams = DEV
|
|
||||||
? { headless: false, slowMo: 5 }
|
|
||||||
: DEBUG
|
|
||||||
? { headless: false, slowMo: 100, devtools: true }
|
|
||||||
: {};
|
|
||||||
browser = await puppeteer.launch(browserParams);
|
|
||||||
page = await browser.newPage();
|
|
||||||
await page.setViewport(browserViewport);
|
|
||||||
if (DEV || DEBUG) {
|
|
||||||
page.on("console", async msg => {
|
|
||||||
// If there is a console.error but an error is not thrown, this will ensure the test fails
|
|
||||||
if (msg.type() === "error") {
|
|
||||||
const errorMsgText = await Promise.all(
|
|
||||||
// TODO can we do this without internal properties?
|
|
||||||
msg.args().map(arg => arg._remoteObject.description)
|
|
||||||
);
|
|
||||||
throw new Error(`Console error: ${errorMsgText}`);
|
|
||||||
}
|
|
||||||
console.log(`PAGE LOG: ${msg.text()}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
page.on("pageerror", err => {
|
|
||||||
throw new Error(`Console error: ${err}`);
|
|
||||||
});
|
|
||||||
utils = puppeteerUtils(page);
|
|
||||||
cxgActions = cellxgeneActions(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await page.goto(appUrlBase);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
if (!DEBUG) {
|
|
||||||
browser.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("did launch", () => {
|
|
||||||
test("page launched", async () => {
|
|
||||||
let el = await utils.getOneElementInnerHTML("[data-testid='header']");
|
|
||||||
expect(el).toBe(data.title);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("metadata loads", () => {
|
|
||||||
test("categories and values from dataset appear", async () => {
|
|
||||||
for (const label in data.categorical) {
|
|
||||||
await utils.waitByID(`category-${label}`);
|
|
||||||
const categoryName = await utils.getOneElementInnerText(
|
|
||||||
`[data-testid="category-${label}"]`
|
|
||||||
);
|
|
||||||
expect(categoryName).toMatch(label);
|
|
||||||
await utils.clickOn(`category-expand-${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("continuous data appears", async () => {
|
|
||||||
for (const label in data.continuous) {
|
|
||||||
await utils.waitByID(`histogram-${label}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("cell selection", () => {
|
|
||||||
test("selects all cells cellset 1", async () => {
|
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
|
||||||
expect(cellCount).toBe(data.dataframe.nObs);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("selects all cells cellset 2", async () => {
|
|
||||||
const cellCount = await cxgActions.cellSet(2);
|
|
||||||
expect(cellCount).toBe(data.dataframe.nObs);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("selects cells via lasso", async () => {
|
|
||||||
for (const cellset of data.cellsets.lasso) {
|
|
||||||
const cellset1 = await cxgActions.calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
cellset["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag("layout-graph", cellset1.start, cellset1.end, true);
|
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
|
||||||
expect(cellCount).toBe(cellset.count);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("selects cells via categorical", async () => {
|
|
||||||
for (const cellset of data.cellsets.categorical) {
|
|
||||||
await utils.clickOn(`category-expand-${cellset.metadata}`);
|
|
||||||
await utils.clickOn(`category-select-${cellset.metadata}`);
|
|
||||||
for (const val of cellset.values) {
|
|
||||||
await utils.clickOn(
|
|
||||||
`categorical-value-select-${cellset.metadata}-${val}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
|
||||||
expect(cellCount).toBe(cellset.count);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("selects cells via continuous", async () => {
|
|
||||||
for (const cellset of data.cellsets.continuous) {
|
|
||||||
const histId = `histogram-${cellset.metadata}-plot-brush`;
|
|
||||||
const coords = await cxgActions.calcDragCoordinates(
|
|
||||||
histId,
|
|
||||||
cellset["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(histId, coords.start, coords.end);
|
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
|
||||||
expect(cellCount).toBe(cellset.count);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("gene entry", () => {
|
|
||||||
test("search for single gene", async () => {
|
|
||||||
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
|
|
||||||
await utils.typeInto("gene-search", data.genes.search);
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
await page.waitForSelector(
|
|
||||||
`[data-testid='histogram-${data.genes.search}']`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("bulk add genes", async () => {
|
|
||||||
await cxgActions.reset();
|
|
||||||
const testGenes = data.genes.bulkadd;
|
|
||||||
await utils.clickOn("section-bulk-add");
|
|
||||||
await utils.typeInto("input-bulk-add", testGenes.join(","));
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
|
|
||||||
const allHistograms = await cxgActions.getAllHistograms(
|
|
||||||
"histogram-user-gene",
|
|
||||||
testGenes
|
|
||||||
);
|
|
||||||
expect(allHistograms).toEqual(expect.arrayContaining(testGenes));
|
|
||||||
expect(allHistograms.length).toEqual(testGenes.length);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("diffexp", () => {
|
|
||||||
test("selects cells, saves them and performs diffexp", async () => {
|
|
||||||
for (const select of data.diffexp.cellset1) {
|
|
||||||
if (select.kind === "categorical") {
|
|
||||||
await cxgActions.selectCategory(select.metadata, select.values, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await cxgActions.cellSet(1);
|
|
||||||
for (const select of data.diffexp.cellset2) {
|
|
||||||
if (select.kind === "categorical") {
|
|
||||||
await cxgActions.selectCategory(select.metadata, select.values, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await cxgActions.cellSet(2);
|
|
||||||
await utils.clickOn("diffexp-button");
|
|
||||||
const allHistograms = await cxgActions.getAllHistograms(
|
|
||||||
"histogram-diffexp",
|
|
||||||
data.diffexp["gene-results"]
|
|
||||||
);
|
|
||||||
expect(allHistograms).toEqual(
|
|
||||||
expect.arrayContaining(data.diffexp["gene-results"])
|
|
||||||
);
|
|
||||||
expect(allHistograms.length).toEqual(data.diffexp["gene-results"].length);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("subset/reset", () => {
|
|
||||||
test("subset - cell count matches", 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])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("reset after subset", async () => {
|
|
||||||
for (const select of data.subset.cellset1) {
|
|
||||||
if (select.kind === "categorical") {
|
|
||||||
await cxgActions.selectCategory(select.metadata, select.values, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await utils.clickOn("subset-button");
|
|
||||||
for (const label in data.subset.categorical) {
|
|
||||||
const categories = await cxgActions.getAllCategoriesAndCounts(label);
|
|
||||||
expect(Object.keys(categories)).toMatchObject(
|
|
||||||
Object.keys(data.subset.categorical[label])
|
|
||||||
);
|
|
||||||
expect(Object.values(categories)).toMatchObject(
|
|
||||||
Object.values(data.subset.categorical[label])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await cxgActions.reset();
|
|
||||||
for (const label in data.categorical) {
|
|
||||||
await utils.waitByID(`category-${label}`);
|
|
||||||
const categoryName = await utils.getOneElementInnerText(
|
|
||||||
`[data-testid="category-${label}"]`
|
|
||||||
);
|
|
||||||
expect(categoryName).toMatch(label);
|
|
||||||
const categories = await cxgActions.getAllCategoriesAndCounts(label);
|
|
||||||
expect(Object.keys(categories)).toMatchObject(
|
|
||||||
Object.keys(data.categorical[label])
|
|
||||||
);
|
|
||||||
expect(Object.values(categories)).toMatchObject(
|
|
||||||
Object.values(data.categorical[label])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("lasso after subset", async () => {
|
|
||||||
for (const select of data.subset.cellset1) {
|
|
||||||
if (select.kind === "categorical") {
|
|
||||||
await cxgActions.selectCategory(select.metadata, select.values, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await utils.clickOn("subset-button");
|
|
||||||
const lassoSelection = await cxgActions.calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
data.subset.lasso["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(
|
|
||||||
"layout-graph",
|
|
||||||
lassoSelection.start,
|
|
||||||
lassoSelection.end,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
|
||||||
expect(cellCount).toBe(data.subset.lasso.count);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("scatter plot", () => {
|
|
||||||
test("scatter plot appears", async () => {
|
|
||||||
await cxgActions.reset();
|
|
||||||
const testGenes = data.scatter.genes;
|
|
||||||
await utils.clickOn("section-bulk-add");
|
|
||||||
await utils.typeInto("input-bulk-add", Object.values(testGenes).join(","));
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
await utils.clickOn(`plot-x-${data.scatter.genes.x}`);
|
|
||||||
await utils.clickOn(`plot-y-${data.scatter.genes.y}`);
|
|
||||||
await utils.waitByID("scatterplot");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("clipping", () => {
|
|
||||||
test("clip continuous", async () => {
|
|
||||||
await cxgActions.clip(data.clip.min, data.clip.max);
|
|
||||||
const histId = `histogram-${data.clip.metadata}-plot-brush`;
|
|
||||||
const coords = await cxgActions.calcDragCoordinates(
|
|
||||||
histId,
|
|
||||||
data.clip["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(histId, coords.start, coords.end);
|
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
|
||||||
expect(cellCount).toBe(data.clip.count);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("clip gene", async () => {
|
|
||||||
await utils.typeInto("gene-search", data.clip.gene);
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
await page.waitForSelector(`[data-testid='histogram-${data.clip.gene}']`);
|
|
||||||
await cxgActions.clip(data.clip.min, data.clip.max);
|
|
||||||
const histId = `histogram-${data.clip.gene}-plot-brush`;
|
|
||||||
const coords = await cxgActions.calcDragCoordinates(
|
|
||||||
histId,
|
|
||||||
data.clip["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(histId, coords.start, coords.end);
|
|
||||||
const cellCount = await cxgActions.cellSet(1);
|
|
||||||
expect(cellCount).toBe(data.clip["gene-cell-count"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// interact with UI elements just that they do not break
|
|
||||||
describe("ui elements don't error", () => {
|
|
||||||
test("color by", async () => {
|
|
||||||
for (const label in data.categorical) {
|
|
||||||
await utils.clickOn(`colorby-${label}`);
|
|
||||||
}
|
|
||||||
for (const label in data.continuous) {
|
|
||||||
await utils.clickOn(`colorby-${label}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("color by for gene", async () => {
|
|
||||||
await utils.typeInto("gene-search", data.genes.search);
|
|
||||||
await page.keyboard.press("Enter");
|
|
||||||
await page.waitForSelector(
|
|
||||||
`[data-testid='histogram-${data.genes.search}']`
|
|
||||||
);
|
|
||||||
await utils.clickOn(`colorby-${data.genes.search}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("pan and zoom", async () => {
|
|
||||||
await utils.clickOn("mode-pan-zoom");
|
|
||||||
const panCoords = await cxgActions.calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
data.pan["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(
|
|
||||||
"layout-graph",
|
|
||||||
panCoords.start,
|
|
||||||
panCoords.end,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await page.evaluate(`window.scrollBy(0, 1000);`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"preset": "jest-puppeteer",
|
|
||||||
"testMatch": [
|
|
||||||
"**/__tests__/**/?(*.)(spec|test).js?(x)"
|
|
||||||
],
|
|
||||||
"testURL": "http://localhost/",
|
|
||||||
"setupFiles": [
|
|
||||||
"../setupMissingGlobals.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
NOT run in Travis CI
|
|
||||||
|
|
||||||
UX tests using puppeteer to be run locally.
|
|
||||||
|
|
||||||
To run locally, ensure you are running the client is running on port 3000.
|
|
||||||
Then run jest --verbose false --config __tests__/e2e/e2eJestConfig.json feature.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import puppeteer from "puppeteer";
|
|
||||||
import { appUrlBase, DEBUG, DEV, DATASET } from "./config";
|
|
||||||
import { puppeteerUtils, cellxgeneActions } from "./puppeteerUtils";
|
|
||||||
import { datasets } from "./data";
|
|
||||||
|
|
||||||
let browser, page, utils, cxgActions, spy;
|
|
||||||
const browserViewport = { width: 1280, height: 960 };
|
|
||||||
let data = datasets[DATASET].features;
|
|
||||||
|
|
||||||
if (DEBUG) jest.setTimeout(100000);
|
|
||||||
if (DEV) jest.setTimeout(10000);
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const browserParams = DEV
|
|
||||||
? { headless: false, slowMo: 5 }
|
|
||||||
: DEBUG
|
|
||||||
? { headless: false, slowMo: 100, devtools: true }
|
|
||||||
: {};
|
|
||||||
browser = await puppeteer.launch(browserParams);
|
|
||||||
page = await browser.newPage();
|
|
||||||
await page.setViewport(browserViewport);
|
|
||||||
if (DEV || DEBUG) {
|
|
||||||
page.on("console", msg => console.log(`PAGE LOG: ${msg.text()}`));
|
|
||||||
}
|
|
||||||
page.on("pageerror", err => {
|
|
||||||
throw new Error(`Console error: ${err}`);
|
|
||||||
});
|
|
||||||
utils = puppeteerUtils(page);
|
|
||||||
cxgActions = cellxgeneActions(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await page.goto(appUrlBase);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
if (!DEBUG) {
|
|
||||||
browser.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("zoom interaction", async () => {
|
|
||||||
// Skip this test since UI is to hide lasso path when switching modes
|
|
||||||
test.skip("lasso visible after switching modes to pan/zoom", async () => {
|
|
||||||
const lassoSelection = await cxgActions.calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
data.panzoom.lasso["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(
|
|
||||||
"layout-graph",
|
|
||||||
lassoSelection.start,
|
|
||||||
lassoSelection.end,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
await utils.waitByID("lasso-element", { visible: true });
|
|
||||||
await utils.clickOn("mode-pan-zoom");
|
|
||||||
await utils.waitByID("lasso-element", { visible: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("pan zoom mode resets lasso selection", async () => {
|
|
||||||
const lassoSelection = await cxgActions.calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
data.panzoom.lasso["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(
|
|
||||||
"layout-graph",
|
|
||||||
lassoSelection.start,
|
|
||||||
lassoSelection.end,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
await utils.waitByID("lasso-element", { visible: true });
|
|
||||||
const initialCount = await cxgActions.cellSet(1);
|
|
||||||
expect(initialCount).toBe(data.panzoom.lasso.count);
|
|
||||||
await utils.clickOn("mode-pan-zoom");
|
|
||||||
await utils.clickOn("mode-lasso");
|
|
||||||
const modeSwitchCount = await cxgActions.cellSet(1);
|
|
||||||
expect(modeSwitchCount).toBe(initialCount);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("lasso moves after pan", async () => {
|
|
||||||
const lassoSelection = await cxgActions.calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
data.panzoom.lasso["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(
|
|
||||||
"layout-graph",
|
|
||||||
lassoSelection.start,
|
|
||||||
lassoSelection.end,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
await utils.waitByID("lasso-element", { visible: true });
|
|
||||||
const initialCount = await cxgActions.cellSet(1);
|
|
||||||
expect(initialCount).toBe(data.panzoom.lasso.count);
|
|
||||||
await utils.clickOn("mode-pan-zoom");
|
|
||||||
const panCoords = await cxgActions.calcDragCoordinates(
|
|
||||||
"layout-graph",
|
|
||||||
data.panzoom.lasso["coordinates-as-percent"]
|
|
||||||
);
|
|
||||||
await cxgActions.drag(
|
|
||||||
"layout-graph",
|
|
||||||
panCoords.start,
|
|
||||||
panCoords.end,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await utils.clickOn("mode-lasso");
|
|
||||||
const panCount = await cxgActions.cellSet(2);
|
|
||||||
expect(panCount).toBe(initialCount);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
export const puppeteerUtils = puppeteerPage => ({
|
|
||||||
async waitByID(testid, props = {}) {
|
|
||||||
return await puppeteerPage.waitForSelector(
|
|
||||||
`[data-testid='${testid}']`,
|
|
||||||
props
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async waitByClass(testclass, props = {}) {
|
|
||||||
return await puppeteerPage.waitForSelector(
|
|
||||||
`[data-testclass='${testclass}']`,
|
|
||||||
props
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async waitForAllByIds(testids, props = {}) {
|
|
||||||
await Promise.all(
|
|
||||||
testids.map(testid =>
|
|
||||||
puppeteerPage.waitForSelector(`[data-testid='${testid}']`)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getAllByClass(testclass, props = {}) {
|
|
||||||
const elements = await puppeteerPage.$$eval(
|
|
||||||
`[data-testclass=${testclass}]`,
|
|
||||||
els => {
|
|
||||||
return els.map(el => {
|
|
||||||
return el.dataset.testid;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return elements;
|
|
||||||
},
|
|
||||||
|
|
||||||
async typeInto(testid, text) {
|
|
||||||
// only works for text without special characters
|
|
||||||
await this.waitByID(testid);
|
|
||||||
const selector = `[data-testid='${testid}']`;
|
|
||||||
// type ahead can be annoying if you don't pause before you type
|
|
||||||
await puppeteerPage.click(selector);
|
|
||||||
await puppeteerPage.waitFor(200);
|
|
||||||
await puppeteerPage.type(selector, text);
|
|
||||||
},
|
|
||||||
|
|
||||||
async clearInputAndTypeInto(testid, text) {
|
|
||||||
await this.waitByID(testid);
|
|
||||||
const selector = `[data-testid='${testid}']`;
|
|
||||||
// only works for text without special characters
|
|
||||||
// type ahead can be annoying if you don't pause before you type
|
|
||||||
await puppeteerPage.click(selector);
|
|
||||||
await puppeteerPage.waitFor(200);
|
|
||||||
// select all
|
|
||||||
|
|
||||||
await puppeteerPage.click(selector, { clickCount: 3 });
|
|
||||||
await puppeteerPage.keyboard.type("Backspace");
|
|
||||||
await puppeteerPage.type(selector, text);
|
|
||||||
},
|
|
||||||
|
|
||||||
async clickOn(testid) {
|
|
||||||
await this.waitByID(testid);
|
|
||||||
await puppeteerPage.click(`[data-testid='${testid}']`);
|
|
||||||
await puppeteerPage.waitFor(50);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getOneElementInnerHTML(selector) {
|
|
||||||
await puppeteerPage.waitForSelector(selector);
|
|
||||||
let text = await puppeteerPage.$eval(selector, el => el.innerHTML);
|
|
||||||
return text;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getOneElementInnerText(selector) {
|
|
||||||
await puppeteerPage.waitForSelector(selector);
|
|
||||||
let text = await puppeteerPage.$eval(selector, el => el.innerText);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const cellxgeneActions = puppeteerPage => ({
|
|
||||||
async drag(testid, start, end, lasso = false) {
|
|
||||||
const layout = await puppeteerUtils(puppeteerPage).waitByID(testid);
|
|
||||||
const elBox = await layout.boxModel();
|
|
||||||
const x1 = elBox.content[0].x + start.x;
|
|
||||||
const x2 = elBox.content[0].x + end.x;
|
|
||||||
const y1 = elBox.content[0].y + start.y;
|
|
||||||
const y2 = elBox.content[0].y + end.y;
|
|
||||||
await puppeteerPage.mouse.move(x1, y1);
|
|
||||||
await puppeteerPage.mouse.down();
|
|
||||||
if (lasso) {
|
|
||||||
await puppeteerPage.mouse.move(x2, y1);
|
|
||||||
await puppeteerPage.mouse.move(x2, y2);
|
|
||||||
await puppeteerPage.mouse.move(x1, y2);
|
|
||||||
await puppeteerPage.mouse.move(x1, y1);
|
|
||||||
} else {
|
|
||||||
await puppeteerPage.mouse.move(x2, y2);
|
|
||||||
}
|
|
||||||
await puppeteerPage.mouse.up();
|
|
||||||
},
|
|
||||||
|
|
||||||
async getAllHistograms(testclass, testids) {
|
|
||||||
const histTestIds = testids.map(tid => `histogram-${tid}`);
|
|
||||||
// these load asynchronously, so we need to wait for each histogram individually
|
|
||||||
await puppeteerUtils(puppeteerPage).waitForAllByIds(histTestIds);
|
|
||||||
const allHistograms = await puppeteerUtils(puppeteerPage).getAllByClass(
|
|
||||||
testclass
|
|
||||||
);
|
|
||||||
return allHistograms.map(hist =>
|
|
||||||
hist.substr("histogram_".length, hist.length)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async getAllCategoriesAndCounts(category) {
|
|
||||||
await puppeteerUtils(puppeteerPage).waitByClass("categorical-row");
|
|
||||||
const categories = await puppeteerPage.$$eval(
|
|
||||||
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
|
|
||||||
els => {
|
|
||||||
let result = {};
|
|
||||||
els.forEach(el => {
|
|
||||||
const cat = el.querySelector("[data-testclass='categorical-value']")
|
|
||||||
.innerText;
|
|
||||||
const count = el.querySelector(
|
|
||||||
"[data-testclass='categorical-value-count']"
|
|
||||||
).innerText;
|
|
||||||
result[cat] = count;
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return categories;
|
|
||||||
},
|
|
||||||
|
|
||||||
async cellSet(num) {
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn(`cellset-button-${num}`);
|
|
||||||
return await puppeteerUtils(puppeteerPage).getOneElementInnerText(
|
|
||||||
`[data-testid='cellset-count-${num}']`
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
async resetCategory(category) {
|
|
||||||
const checkboxId = `category-select-${category}`;
|
|
||||||
await puppeteerUtils(puppeteerPage).waitByID(checkboxId);
|
|
||||||
const checkedPseudoclass = await puppeteerPage.$eval(
|
|
||||||
`[data-testid='${checkboxId}']`,
|
|
||||||
el => {
|
|
||||||
return el.matches(":checked");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!checkedPseudoclass) {
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn(checkboxId);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const categoryRow = await puppeteerUtils(puppeteerPage).waitByID(
|
|
||||||
`category-expand-${category}`
|
|
||||||
);
|
|
||||||
const isExpanded = await categoryRow.$(
|
|
||||||
"[data-testclass='category-expand-is-expanded']"
|
|
||||||
);
|
|
||||||
if (isExpanded) {
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn(
|
|
||||||
`category-expand-${category}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
},
|
|
||||||
|
|
||||||
async calcDragCoordinates(testid, coordinateAsPercent) {
|
|
||||||
const el = await puppeteerUtils(puppeteerPage).waitByID(testid);
|
|
||||||
const size = await el.boxModel();
|
|
||||||
const coords = {
|
|
||||||
start: {
|
|
||||||
x: Math.floor(size.width * coordinateAsPercent.x1),
|
|
||||||
y: Math.floor(size.height * coordinateAsPercent.y1)
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
x: Math.floor(size.width * coordinateAsPercent.x2),
|
|
||||||
y: Math.floor(size.height * coordinateAsPercent.y2)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return coords;
|
|
||||||
},
|
|
||||||
|
|
||||||
async selectCategory(category, values, reset = true) {
|
|
||||||
if (reset) await this.resetCategory(category);
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn(`category-expand-${category}`);
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn(`category-select-${category}`);
|
|
||||||
for (const val of values) {
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn(
|
|
||||||
`categorical-value-select-${category}-${val}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async reset() {
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn("reset");
|
|
||||||
// loading state never actually happens, reset is too fast
|
|
||||||
await page.waitFor(200);
|
|
||||||
},
|
|
||||||
|
|
||||||
async clip(min = 0, max = 100) {
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn("visualization-settings");
|
|
||||||
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
|
|
||||||
"clip-min-input",
|
|
||||||
min
|
|
||||||
);
|
|
||||||
await puppeteerUtils(puppeteerPage).clearInputAndTypeInto(
|
|
||||||
"clip-max-input",
|
|
||||||
max
|
|
||||||
);
|
|
||||||
await puppeteerUtils(puppeteerPage).clickOn("clip-commit");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import cascadeReducers from "../../src/reducers/cascade";
|
|
||||||
|
|
||||||
describe("create", () => {
|
|
||||||
test("from Array", () => {
|
|
||||||
expect(cascadeReducers([["foo", () => 0]])).toBeInstanceOf(Function);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("from Map", () => {
|
|
||||||
expect(cascadeReducers(new Map([["foo", () => 0]]))).toBeInstanceOf(
|
|
||||||
Function
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("cascade", () => {
|
|
||||||
test("expected arguments provided & cascade ordering", () => {
|
|
||||||
const topLevelState = {};
|
|
||||||
const topLevelAction = { type: "test" };
|
|
||||||
|
|
||||||
const reducer = cascadeReducers([
|
|
||||||
[
|
|
||||||
"foo",
|
|
||||||
(currentState, action, nextSharedState, prevSharedState) => {
|
|
||||||
expect(currentState).toBeUndefined();
|
|
||||||
expect(action).toEqual(topLevelAction);
|
|
||||||
expect(nextSharedState).toStrictEqual({});
|
|
||||||
expect(prevSharedState).toBe(topLevelState);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"bar",
|
|
||||||
(currentState, action, nextSharedState, prevSharedState) => {
|
|
||||||
expect(currentState).toBeUndefined();
|
|
||||||
expect(action).toEqual(topLevelAction);
|
|
||||||
expect(nextSharedState).toStrictEqual({ foo: 0 });
|
|
||||||
expect(prevSharedState).toBe(topLevelState);
|
|
||||||
return 99;
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
const nextState = reducer(topLevelState, topLevelAction);
|
|
||||||
expect(nextState).toStrictEqual({ foo: 0, bar: 99 });
|
|
||||||
expect(topLevelState).toStrictEqual({});
|
|
||||||
expect(topLevelAction).toStrictEqual({ type: "test" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import undoable from "../../src/reducers/undoable";
|
|
||||||
|
|
||||||
describe("create", () => {
|
|
||||||
test("no keys", () => {
|
|
||||||
expect(() => undoable(() => {})).toThrow();
|
|
||||||
expect(() => undoable(() => {}, null)).toThrow();
|
|
||||||
expect(() => undoable(() => {}, [])).toThrow();
|
|
||||||
expect(() => undoable(() => {}, [], {})).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple", () => {
|
|
||||||
expect(undoable(() => {}, ["foo"])).toBeInstanceOf(Function);
|
|
||||||
expect(undoable(() => {}, ["foo"], {})).toBeInstanceOf(Function);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("handles undefined initial state", () => {
|
|
||||||
expect(
|
|
||||||
undoable(() => {}, ["a"])(undefined, { type: "test" })
|
|
||||||
).toMatchObject({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("undo", () => {
|
|
||||||
test("expected state modifications", () => {
|
|
||||||
const initialState = { a: 0, b: 1000 };
|
|
||||||
const reducer = state => {
|
|
||||||
return { a: state.a + 1, b: state.b + 1 };
|
|
||||||
};
|
|
||||||
const undoableReducer = undoable(reducer, ["a"]);
|
|
||||||
|
|
||||||
const s1 = undoableReducer(initialState, { type: "test" });
|
|
||||||
expect(s1).toMatchObject({ a: 1, b: 1001 });
|
|
||||||
|
|
||||||
// test that only specified keys are undone
|
|
||||||
const s2 = undoableReducer(s1, { type: "@@undoable/undo" });
|
|
||||||
expect(s2).toMatchObject({ a: 0, b: 1001 });
|
|
||||||
|
|
||||||
// test backstop when no more history
|
|
||||||
const s3 = undoableReducer(s2, { type: "@@undoable/undo" });
|
|
||||||
expect(s3).toMatchObject({ a: 0, b: 1001 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("redo", () => {
|
|
||||||
const initialState = { a: 0, b: 1000 };
|
|
||||||
const reducer = state => {
|
|
||||||
return { a: state.a + 1, b: state.b + 1 };
|
|
||||||
};
|
|
||||||
let UR;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
UR = undoable(reducer, ["a"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("expected state modifications", () => {
|
|
||||||
const s1 = UR(initialState, { type: "test" });
|
|
||||||
expect(s1).toMatchObject({ a: 1, b: 1001 });
|
|
||||||
|
|
||||||
// verify undo->redo reverts state.
|
|
||||||
const s2 = UR(UR(s1, { type: "@@undoable/undo" }), {
|
|
||||||
type: "@@undoable/redo"
|
|
||||||
});
|
|
||||||
expect(s2).toMatchObject({ a: 1, b: 1001 });
|
|
||||||
|
|
||||||
// verify backstop when no redo future
|
|
||||||
const s3 = UR(s2, { type: "@@undoable/redo" });
|
|
||||||
expect(s3).toMatchObject({ a: 1, b: 1001 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("history cleared", () => {
|
|
||||||
// verify future cleared upon a normal state transition
|
|
||||||
const s1 = UR(initialState, { type: "test" });
|
|
||||||
expect(s1).toMatchObject({ a: 1, b: 1001 });
|
|
||||||
const s2 = UR(s1, { type: "@@undoable/undo" });
|
|
||||||
expect(s2).toMatchObject({ a: 0, b: 1001 });
|
|
||||||
const s3 = UR(s2, { type: "test" });
|
|
||||||
expect(s3).toMatchObject({ a: 1, b: 1002 });
|
|
||||||
const s4 = UR(s3, { type: "@@undoable/redo" });
|
|
||||||
expect(s4).toMatchObject({ a: 1, b: 1002 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO:
|
|
||||||
- historyLimit is enforced
|
|
||||||
- action filters
|
|
||||||
*/
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/*
|
|
||||||
Define globals which are present in the client, but not in node (and therefore not in
|
|
||||||
the jest test environment).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { TextDecoder, TextEncoder } from "util";
|
|
||||||
|
|
||||||
global.TextDecoder = TextDecoder;
|
|
||||||
global.TextEncoder = TextEncoder;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { rangeEncodeIndices } from "../../src/util/actionHelpers";
|
|
||||||
|
|
||||||
describe("rangeEncodeIndices", () => {
|
|
||||||
test("small array edge cases", () => {
|
|
||||||
expect(rangeEncodeIndices([])).toMatchObject([]);
|
|
||||||
expect(rangeEncodeIndices([1])).toMatchObject([1]);
|
|
||||||
expect(rangeEncodeIndices([1, 99])).toMatchObject([1, 99]);
|
|
||||||
expect(rangeEncodeIndices([99, 1])).toMatchObject([1, 99]);
|
|
||||||
expect(rangeEncodeIndices([1, 100, 4])).toMatchObject([1, 4, 100]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("sorted flag", () => {
|
|
||||||
expect(rangeEncodeIndices([1, 9, 432], 10, true)).toMatchObject([
|
|
||||||
1,
|
|
||||||
9,
|
|
||||||
432
|
|
||||||
]);
|
|
||||||
expect(rangeEncodeIndices([1, 9, 432], 10, false)).toMatchObject([
|
|
||||||
1,
|
|
||||||
9,
|
|
||||||
432
|
|
||||||
]);
|
|
||||||
expect(rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, true)).toMatchObject(
|
|
||||||
[[0, 3], [9, 10], 432]
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, false)
|
|
||||||
).toMatchObject([[0, 3], [9, 10], 432]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("begin or end edge cases", () => {
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([3, 4, 5, 6, 7, 10, 11, 12, 99], 3, false)
|
|
||||||
).toMatchObject([[3, 7], [10, 12], 99]);
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([3, 4, 5, 6, 7, 10, 11, 12], 3, false)
|
|
||||||
).toMatchObject([[3, 7], [10, 12]]);
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([0, 3, 4, 5, 6, 7, 10, 11, 12], 3, false)
|
|
||||||
).toMatchObject([0, [3, 7], [10, 12]]);
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([0, 3, 4, 5, 6, 7, 10, 11, 12, 99], 3, false)
|
|
||||||
).toMatchObject([0, [3, 7], [10, 12], 99]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("minRangeLength", () => {
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([3, 4, 5, 6, 7, 10, 11, 12, 99], 4, false)
|
|
||||||
).toMatchObject([[3, 7], 10, 11, 12, 99]);
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([3, 4, 5, 6, 7, 10, 11, 12], 4, false)
|
|
||||||
).toMatchObject([[3, 7], 10, 11, 12]);
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([0, 3, 4, 5, 6, 7, 10, 11, 12], 4, false)
|
|
||||||
).toMatchObject([0, [3, 7], 10, 11, 12]);
|
|
||||||
expect(
|
|
||||||
rangeEncodeIndices([0, 3, 4, 5, 6, 7, 10, 11, 12, 99], 4, false)
|
|
||||||
).toMatchObject([0, [3, 7], 10, 11, 12, 99]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,652 +0,0 @@
|
|||||||
import * as Dataframe from "../../../src/util/dataframe";
|
|
||||||
|
|
||||||
describe("dataframe constructor", () => {
|
|
||||||
test("empty dataframe", () => {
|
|
||||||
const df = new Dataframe.Dataframe([0, 0], []);
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.dims).toEqual([0, 0]);
|
|
||||||
expect(df).toHaveLength(0);
|
|
||||||
expect(df.icol(0)).not.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("create with default indices", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[3, 2],
|
|
||||||
[new Int32Array(3).fill(0), new Int32Array(3).fill(1)]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.dims).toEqual([3, 2]);
|
|
||||||
expect(df.rowIndex).toBeInstanceOf(Dataframe.IdentityInt32Index);
|
|
||||||
expect(df.colIndex).toBeInstanceOf(Dataframe.IdentityInt32Index);
|
|
||||||
expect(df.at(0, 0)).toEqual(0);
|
|
||||||
expect(df.at(2, 1)).toEqual(1);
|
|
||||||
expect(df.iat(0, 0)).toEqual(0);
|
|
||||||
expect(df.iat(2, 1)).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("create with labelled indices", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[3, 2],
|
|
||||||
[new Int32Array([0, 1, 2]), new Int32Array([3, 4, 5])],
|
|
||||||
new Dataframe.DenseInt32Index([2, 1, 0]),
|
|
||||||
new Dataframe.KeyIndex(["A", "B"])
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.dims).toEqual([3, 2]);
|
|
||||||
|
|
||||||
expect(df.rowIndex).toBeInstanceOf(Dataframe.DenseInt32Index);
|
|
||||||
expect(df.colIndex).toBeInstanceOf(Dataframe.KeyIndex);
|
|
||||||
expect(df.rowIndex.keys()).toEqual(new Int32Array([2, 1, 0]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(["A", "B"]);
|
|
||||||
|
|
||||||
expect(df.at(0, "A")).toEqual(2);
|
|
||||||
expect(df.at(2, "B")).toEqual(3);
|
|
||||||
expect(df.iat(0, 0)).toEqual(0);
|
|
||||||
expect(df.iat(2, 1)).toEqual(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("simple data access", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[4, 2],
|
|
||||||
[
|
|
||||||
new Float64Array([0.0, Number.NaN, Number.POSITIVE_INFINITY, 3.14159]),
|
|
||||||
["red", "blue", "green", "nan"]
|
|
||||||
],
|
|
||||||
new Dataframe.DenseInt32Index([3, 2, 1, 0]),
|
|
||||||
new Dataframe.KeyIndex(["numbers", "colors"])
|
|
||||||
);
|
|
||||||
|
|
||||||
test("iat", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
|
|
||||||
// present
|
|
||||||
expect(df.iat(0, 0)).toEqual(0.0);
|
|
||||||
expect(df.iat(0, 1)).toEqual("red");
|
|
||||||
expect(df.iat(1, 0)).toEqual(Number.NaN);
|
|
||||||
expect(df.iat(1, 1)).toEqual("blue");
|
|
||||||
expect(df.iat(2, 0)).toEqual(Number.POSITIVE_INFINITY);
|
|
||||||
expect(df.iat(2, 1)).toEqual("green");
|
|
||||||
expect(df.iat(3, 0)).toEqual(3.14159);
|
|
||||||
expect(df.iat(3, 1)).toEqual("nan");
|
|
||||||
|
|
||||||
// labels out of range have no defined behavior
|
|
||||||
});
|
|
||||||
|
|
||||||
test("at", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
|
|
||||||
// present
|
|
||||||
expect(df.at(3, "numbers")).toEqual(0.0);
|
|
||||||
expect(df.at(3, "colors")).toEqual("red");
|
|
||||||
expect(df.at(2, "numbers")).toEqual(Number.NaN);
|
|
||||||
expect(df.at(2, "colors")).toEqual("blue");
|
|
||||||
expect(df.at(1, "numbers")).toEqual(Number.POSITIVE_INFINITY);
|
|
||||||
expect(df.at(1, "colors")).toEqual("green");
|
|
||||||
expect(df.at(0, "numbers")).toEqual(3.14159);
|
|
||||||
expect(df.at(0, "colors")).toEqual("nan");
|
|
||||||
|
|
||||||
// labels out of range have no defined behavior
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ihas", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
|
|
||||||
// present
|
|
||||||
expect(df.ihas(0, 0)).toBeTruthy();
|
|
||||||
expect(df.ihas(1, 1)).toBeTruthy();
|
|
||||||
expect(df.ihas(3, 1)).toBeTruthy();
|
|
||||||
|
|
||||||
// not present
|
|
||||||
expect(df.ihas(-1, -1)).toBeFalsy();
|
|
||||||
expect(df.ihas(0, 99)).toBeFalsy();
|
|
||||||
expect(df.ihas(99, 0)).toBeFalsy();
|
|
||||||
expect(df.ihas(99, 99)).toBeFalsy();
|
|
||||||
expect(df.ihas(-1, 0)).toBeFalsy();
|
|
||||||
expect(df.ihas(0, -1)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("has", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
|
|
||||||
// present
|
|
||||||
expect(df.has(3, "numbers")).toBeTruthy();
|
|
||||||
expect(df.has(0, "numbers")).toBeTruthy();
|
|
||||||
expect(df.has(3, "colors")).toBeTruthy();
|
|
||||||
expect(df.has(0, "colors")).toBeTruthy();
|
|
||||||
|
|
||||||
// not present
|
|
||||||
expect(df.has(3, "foo")).toBeFalsy();
|
|
||||||
expect(df.has(-1, "numbers")).toBeFalsy();
|
|
||||||
expect(df.has(-1, -1)).toBeFalsy();
|
|
||||||
expect(df.has(null, null)).toBeFalsy();
|
|
||||||
expect(df.has(0, "foo")).toBeFalsy();
|
|
||||||
expect(df.has(99, "numbers")).toBeFalsy();
|
|
||||||
expect(df.has(99, "foo")).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("dataframe subsetting", () => {
|
|
||||||
describe("subset", () => {
|
|
||||||
const sourceDf = new Dataframe.Dataframe(
|
|
||||||
[3, 4],
|
|
||||||
[
|
|
||||||
new Int32Array([0, 1, 2]),
|
|
||||||
["A", "B", "C"],
|
|
||||||
new Float32Array([4.4, 5.5, 6.6]),
|
|
||||||
["red", "green", "blue"]
|
|
||||||
],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex(["int32", "string", "float32", "colors"])
|
|
||||||
);
|
|
||||||
|
|
||||||
test("all rows, one column", () => {
|
|
||||||
const dfA = sourceDf.subset(null, ["colors"]);
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([3, 1]);
|
|
||||||
expect(dfA.iat(0, 0)).toEqual("red");
|
|
||||||
expect(dfA.at(2, "colors")).toEqual("blue");
|
|
||||||
expect(dfA.col("colors").asArray()).toEqual(["red", "green", "blue"]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "green", "blue"]);
|
|
||||||
expect(dfA.col("colors").asArray()).toEqual(
|
|
||||||
sourceDf.col("colors").asArray()
|
|
||||||
);
|
|
||||||
expect(dfA.rowIndex.keys()).toEqual(sourceDf.rowIndex.keys());
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(["colors"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("all rows, two columns", () => {
|
|
||||||
const dfB = sourceDf.subset(null, ["colors", "float32"]);
|
|
||||||
expect(dfB).toBeDefined();
|
|
||||||
expect(dfB.dims).toEqual([3, 2]);
|
|
||||||
expect(dfB.iat(0, 0)).toBeCloseTo(4.4);
|
|
||||||
expect(dfB.iat(0, 1)).toEqual("red");
|
|
||||||
expect(dfB.at(2, "colors")).toEqual("blue");
|
|
||||||
expect(dfB.at(2, "float32")).toBeCloseTo(6.6);
|
|
||||||
expect(dfB.col("colors").asArray()).toEqual(["red", "green", "blue"]);
|
|
||||||
expect(dfB.col("float32").asArray()).toEqual(
|
|
||||||
new Float32Array([4.4, 5.5, 6.6])
|
|
||||||
);
|
|
||||||
expect(dfB.icol(0).asArray()).toEqual(dfB.col("float32").asArray());
|
|
||||||
expect(dfB.icol(1).asArray()).toEqual(dfB.col("colors").asArray());
|
|
||||||
expect(dfB.col("colors").asArray()).toEqual(
|
|
||||||
sourceDf.col("colors").asArray()
|
|
||||||
);
|
|
||||||
expect(dfB.col("float32").asArray()).toEqual(
|
|
||||||
sourceDf.col("float32").asArray()
|
|
||||||
);
|
|
||||||
expect(dfB.rowIndex.keys()).toEqual(sourceDf.rowIndex.keys());
|
|
||||||
expect(dfB.colIndex.keys()).toEqual(["float32", "colors"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("one row, all columns", () => {
|
|
||||||
const dfC = sourceDf.subset([1], null);
|
|
||||||
expect(dfC).toBeDefined();
|
|
||||||
expect(dfC.dims).toEqual([1, 4]);
|
|
||||||
expect(dfC.iat(0, 0)).toEqual(1);
|
|
||||||
expect(dfC.iat(0, 1)).toEqual("B");
|
|
||||||
expect(dfC.iat(0, 2)).toBeCloseTo(5.5);
|
|
||||||
expect(dfC.iat(0, 3)).toEqual("green");
|
|
||||||
expect(dfC.rowIndex.keys()).toEqual(new Int32Array([1]));
|
|
||||||
expect(dfC.colIndex.keys()).toEqual(sourceDf.colIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("two rows, all columns", () => {
|
|
||||||
const dfD = sourceDf.subset([0, 2], null);
|
|
||||||
expect(dfD).toBeDefined();
|
|
||||||
expect(dfD.dims).toEqual([2, 4]);
|
|
||||||
expect(dfD.icol(0).asArray()).toEqual(new Int32Array([0, 2]));
|
|
||||||
expect(dfD.icol(1).asArray()).toEqual(["A", "C"]);
|
|
||||||
expect(dfD.icol(2).asArray()).toEqual(new Float32Array([4.4, 6.6]));
|
|
||||||
expect(dfD.icol(3).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfD.rowIndex.keys()).toEqual(new Int32Array([0, 2]));
|
|
||||||
expect(dfD.colIndex.keys()).toEqual(sourceDf.colIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("all rows, all columns", () => {
|
|
||||||
const dfE = sourceDf.subset(null, null);
|
|
||||||
expect(dfE).toBeDefined();
|
|
||||||
expect(dfE.dims).toEqual([3, 4]);
|
|
||||||
expect(dfE.icol(0).asArray()).toEqual(sourceDf.icol(0).asArray());
|
|
||||||
expect(dfE.icol(1).asArray()).toEqual(sourceDf.icol(1).asArray());
|
|
||||||
expect(dfE.icol(2).asArray()).toEqual(sourceDf.icol(2).asArray());
|
|
||||||
expect(dfE.icol(3).asArray()).toEqual(sourceDf.icol(3).asArray());
|
|
||||||
expect(dfE.rowIndex.keys()).toEqual(sourceDf.rowIndex.keys());
|
|
||||||
expect(dfE.colIndex.keys()).toEqual(sourceDf.colIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("two rows, two colums", () => {
|
|
||||||
const dfF = sourceDf.subset([0, 2], ["int32", "float32"]);
|
|
||||||
expect(dfF).toBeDefined();
|
|
||||||
expect(dfF.dims).toEqual([2, 2]);
|
|
||||||
expect(dfF.icol(0).asArray()).toEqual(new Int32Array([0, 2]));
|
|
||||||
expect(dfF.icol(1).asArray()).toEqual(new Float32Array([4.4, 6.6]));
|
|
||||||
expect(dfF.rowIndex.keys()).toEqual(new Int32Array([0, 2]));
|
|
||||||
expect(dfF.colIndex.keys()).toEqual(["int32", "float32"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("withRowIndex", () => {
|
|
||||||
const df = sourceDf.subset(
|
|
||||||
null,
|
|
||||||
["int32", "float32"],
|
|
||||||
new Dataframe.DenseInt32Index([3, 2, 1])
|
|
||||||
);
|
|
||||||
expect(df.colIndex).toBeInstanceOf(Dataframe.KeyIndex);
|
|
||||||
expect(df.rowIndex).toBeInstanceOf(Dataframe.DenseInt32Index);
|
|
||||||
expect(df.at(3, "int32")).toEqual(df.iat(0, 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("withRowIndex error checks", () => {
|
|
||||||
expect(() =>
|
|
||||||
sourceDf.subset(null, ["red"], new Dataframe.IdentityInt32Index(1))
|
|
||||||
).toThrow(RangeError);
|
|
||||||
expect(() =>
|
|
||||||
sourceDf.subset(null, ["red"], new Dataframe.DenseInt32Index([0, 1]))
|
|
||||||
).toThrow(RangeError);
|
|
||||||
expect(() =>
|
|
||||||
sourceDf.subset(null, ["red"], new Dataframe.KeyIndex([0, 1, 2, 3]))
|
|
||||||
).toThrow(RangeError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isubsetMask", () => {
|
|
||||||
const sourceDf = new Dataframe.Dataframe(
|
|
||||||
[3, 4],
|
|
||||||
[
|
|
||||||
new Int32Array([0, 1, 2]),
|
|
||||||
["A", "B", "C"],
|
|
||||||
new Float32Array([4.4, 5.5, 6.6]),
|
|
||||||
["red", "green", "blue"]
|
|
||||||
],
|
|
||||||
new Dataframe.DenseInt32Index([2, 4, 6]),
|
|
||||||
new Dataframe.KeyIndex(["int32", "string", "float32", "colors"])
|
|
||||||
);
|
|
||||||
|
|
||||||
const dfA = sourceDf.isubsetMask(
|
|
||||||
new Uint8Array([0, 1, 1]),
|
|
||||||
new Uint8Array([1, 0, 0, 1])
|
|
||||||
);
|
|
||||||
expect(dfA.dims).toEqual([2, 2]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(new Int32Array([1, 2]));
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual(["green", "blue"]);
|
|
||||||
expect(dfA.rowIndex.keys()).toEqual(new Int32Array([4, 6]));
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(["int32", "colors"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("dataframe factories", () => {
|
|
||||||
test("create", () => {
|
|
||||||
const df = Dataframe.Dataframe.create(
|
|
||||||
[3, 3],
|
|
||||||
[
|
|
||||||
new Array(3).fill(0),
|
|
||||||
new Int16Array(3).fill(99),
|
|
||||||
new Float64Array(3).fill(1.1)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.dims).toEqual([3, 3]);
|
|
||||||
expect(df).toHaveLength(3);
|
|
||||||
expect(df.iat(0, 0)).toEqual(0);
|
|
||||||
expect(df.iat(1, 1)).toEqual(99);
|
|
||||||
expect(df.iat(2, 2)).toBeCloseTo(1.1);
|
|
||||||
expect(df.iat(0, 0)).toEqual(df.at(0, 0));
|
|
||||||
expect(df.iat(1, 1)).toEqual(df.at(1, 1));
|
|
||||||
expect(df.iat(2, 2)).toEqual(df.at(2, 2));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("clone", () => {
|
|
||||||
const dfA = new Dataframe.Dataframe(
|
|
||||||
[3, 2],
|
|
||||||
[new Int32Array([0, 1, 2]), new Int32Array([3, 4, 5])],
|
|
||||||
new Dataframe.DenseInt32Index([2, 1, 0]),
|
|
||||||
new Dataframe.KeyIndex(["A", "B"])
|
|
||||||
);
|
|
||||||
|
|
||||||
const dfB = dfA.clone();
|
|
||||||
expect(dfB).not.toBe(dfA);
|
|
||||||
expect(dfB.dims).toEqual(dfA.dims);
|
|
||||||
expect(dfB).toHaveLength(dfA.length);
|
|
||||||
expect(dfB.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
expect(dfB.colIndex.keys()).toEqual(dfA.colIndex.keys());
|
|
||||||
for (let i = 0, l = dfB.dims[1]; i < l; i += 1) {
|
|
||||||
expect(dfB.icol(i).asArray()).toEqual(dfA.icol(i).asArray());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("withCol", () => {
|
|
||||||
test("KeyIndex", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 2],
|
|
||||||
[["red", "blue"], [true, false]],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex(["colors", "bools"])
|
|
||||||
);
|
|
||||||
const dfA = df.withCol("numbers", [1, 0]);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 3]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.icol(2).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col("numbers").asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(["colors", "bools", "numbers"]);
|
|
||||||
expect(df.colIndex.keys()).toEqual(["colors", "bools"]);
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("DenseInt32Index", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 2],
|
|
||||||
[["red", "blue"], [true, false]],
|
|
||||||
null,
|
|
||||||
new Dataframe.DenseInt32Index([74, 75])
|
|
||||||
);
|
|
||||||
const dfA = df.withCol(72, [1, 0]);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 3]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.icol(2).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col(74).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.col(75).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.col(72).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(new Int32Array([74, 75, 72]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(new Int32Array([74, 75]));
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("DenseInt32Index promote", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 2],
|
|
||||||
[["red", "blue"], [true, false]],
|
|
||||||
null,
|
|
||||||
new Dataframe.DenseInt32Index([74, 75])
|
|
||||||
);
|
|
||||||
const dfA = df.withCol(999, [1, 0]);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 3]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.icol(2).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col(74).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.col(75).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.col(999).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(new Int32Array([74, 75, 999]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(new Int32Array([74, 75]));
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("IdentityInt32Index with last", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 2],
|
|
||||||
[["red", "blue"], [true, false]],
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const dfA = df.withCol(2, [1, 0]);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 3]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.icol(2).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.col(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.col(2).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1, 2]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1]));
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("IdentityInt32Index promote", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 2],
|
|
||||||
[["red", "blue"], [true, false]],
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const dfA = df.withCol(99, [1, 0]);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 3]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.icol(2).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.col(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.col(99).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1, 99]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1]));
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("handle column dimensions correctly", () => {
|
|
||||||
/*
|
|
||||||
there are two conditions:
|
|
||||||
- empty dataframe - will accept an add of any dimensionality
|
|
||||||
- non-empty dataframe - added column must match row-count dimension
|
|
||||||
*/
|
|
||||||
test("empty.withCol", () => {
|
|
||||||
const edf = Dataframe.Dataframe.empty();
|
|
||||||
const df = edf.withCol("foo", [1, 2, 3]);
|
|
||||||
|
|
||||||
expect(edf).toBeDefined();
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(edf).not.toEqual(df);
|
|
||||||
expect(df.dims).toEqual([3, 1]);
|
|
||||||
expect(df.icol(0).asArray()).toEqual([1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("withCol dimension check", () => {
|
|
||||||
const dfA = new Dataframe.Dataframe([1, 1], [["a"]]);
|
|
||||||
expect(() => {
|
|
||||||
dfA.withCol(1, []);
|
|
||||||
}).toThrow(RangeError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("dropCol", () => {
|
|
||||||
test("KeyIndex", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 3],
|
|
||||||
[["red", "blue"], [true, false], [1, 0]],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex(["colors", "bools", "numbers"])
|
|
||||||
);
|
|
||||||
const dfA = df.dropCol("colors");
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 2]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col("numbers").asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(["bools", "numbers"]);
|
|
||||||
expect(df.colIndex.keys()).toEqual(["colors", "bools", "numbers"]);
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("IdentityInt32Index drop first", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 3],
|
|
||||||
[["red", "blue"], [true, false], [1, 0]],
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const dfA = df.dropCol(0);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 2]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual([true, false]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([1, 0]);
|
|
||||||
expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray());
|
|
||||||
expect(df.col(2).asArray()).toEqual(dfA.col(2).asArray());
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(new Int32Array([1, 2]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1, 2]));
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("IdentityInt32Index drop last", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 3],
|
|
||||||
[["red", "blue"], [true, false], [1, 0]],
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const dfA = df.dropCol(2);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 2]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([true, false]);
|
|
||||||
expect(df.col(0).asArray()).toEqual(dfA.col(0).asArray());
|
|
||||||
expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray());
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1, 2]));
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
|
|
||||||
test("DenseInt32Index", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[2, 3],
|
|
||||||
[["red", "blue"], [true, false], [1, 0]],
|
|
||||||
null,
|
|
||||||
new Dataframe.DenseInt32Index([102, 101, 100])
|
|
||||||
);
|
|
||||||
const dfA = df.dropCol(101);
|
|
||||||
|
|
||||||
expect(dfA).toBeDefined();
|
|
||||||
expect(dfA.dims).toEqual([2, 2]);
|
|
||||||
expect(dfA.icol(0).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.icol(1).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col(100).asArray()).toEqual([1, 0]);
|
|
||||||
expect(dfA.col(102).asArray()).toEqual(["red", "blue"]);
|
|
||||||
expect(dfA.colIndex.keys()).toEqual(new Int32Array([102, 100]));
|
|
||||||
expect(df.colIndex.keys()).toEqual(new Int32Array([102, 101, 100]));
|
|
||||||
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("mapColumns", () => {
|
|
||||||
test("identity", () => {
|
|
||||||
const dfA = Dataframe.Dataframe.create(
|
|
||||||
[3, 3],
|
|
||||||
[
|
|
||||||
new Array(3).fill(0),
|
|
||||||
new Int16Array(3).fill(99),
|
|
||||||
new Float64Array(3).fill(1.1)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
const dfB = dfA.mapColumns((col, idx) => {
|
|
||||||
expect(dfA.icol(idx).asArray()).toBe(col);
|
|
||||||
return col;
|
|
||||||
});
|
|
||||||
expect(dfA).not.toBe(dfB);
|
|
||||||
expect(dfA.dims).toEqual(dfB.dims);
|
|
||||||
for (let c = 0; c < dfA.dims[1]; c += 1) {
|
|
||||||
expect(dfA.icol(c).asArray()).toBe(dfB.icol(c).asArray());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("transform", () => {
|
|
||||||
const dfA = Dataframe.Dataframe.create(
|
|
||||||
[3, 3],
|
|
||||||
[new Array(3).fill(0), new Array(3).fill(0), new Array(3).fill(0)]
|
|
||||||
);
|
|
||||||
const dfB = dfA.mapColumns(() => {
|
|
||||||
return new Array(3).fill(1);
|
|
||||||
});
|
|
||||||
expect(dfA).not.toBe(dfB);
|
|
||||||
expect(dfB.iat(0, 0)).toEqual(1);
|
|
||||||
expect(dfB.iat(0, 1)).toEqual(1);
|
|
||||||
expect(dfB.iat(0, 2)).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("dataframe col", () => {
|
|
||||||
let df = null;
|
|
||||||
beforeEach(() => {
|
|
||||||
df = new Dataframe.Dataframe(
|
|
||||||
[2, 2],
|
|
||||||
[[true, false], [1, 0]],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex(["A", "B"])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("col", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.col("A")).toBe(df.icol(0));
|
|
||||||
expect(df.col("B")).toBe(df.icol(1));
|
|
||||||
expect(df.col("undefined")).toBeUndefined();
|
|
||||||
expect(df.icol("undefined")).toBeUndefined();
|
|
||||||
|
|
||||||
const colA = df.col("A");
|
|
||||||
expect(colA).toBeInstanceOf(Function);
|
|
||||||
expect(colA.asArray).toBeInstanceOf(Function);
|
|
||||||
expect(colA.has).toBeInstanceOf(Function);
|
|
||||||
expect(colA.ihas).toBeInstanceOf(Function);
|
|
||||||
expect(colA.indexOf).toBeInstanceOf(Function);
|
|
||||||
expect(colA.iget).toBeInstanceOf(Function);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("col.asArray", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.col("A").asArray()).toEqual([true, false]);
|
|
||||||
expect(df.icol(0).asArray()).toEqual([true, false]);
|
|
||||||
expect(df.col("B").asArray()).toEqual([1, 0]);
|
|
||||||
expect(df.icol(1).asArray()).toEqual([1, 0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("col.has", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.col("A").has(-1)).toBe(false);
|
|
||||||
expect(df.col("A").has(0)).toBe(true);
|
|
||||||
expect(df.col("A").has(1)).toBe(true);
|
|
||||||
expect(df.col("A").has(2)).toBe(false);
|
|
||||||
expect(df.col("B").has(-1)).toBe(false);
|
|
||||||
expect(df.col("B").has(0)).toBe(true);
|
|
||||||
expect(df.col("B").has(1)).toBe(true);
|
|
||||||
expect(df.col("B").has(2)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("col.ihas", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.col("A").ihas(-1)).toBe(false);
|
|
||||||
expect(df.col("A").ihas(0)).toBe(true);
|
|
||||||
expect(df.col("A").ihas(1)).toBe(true);
|
|
||||||
expect(df.col("A").ihas(2)).toBe(false);
|
|
||||||
expect(df.col("B").ihas(-1)).toBe(false);
|
|
||||||
expect(df.col("B").ihas(0)).toBe(true);
|
|
||||||
expect(df.col("B").ihas(1)).toBe(true);
|
|
||||||
expect(df.col("B").ihas(2)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("col.iget", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.col("A").iget(0)).toEqual(df.iat(0, 0));
|
|
||||||
expect(df.col("B").iget(1)).toEqual(df.iat(1, 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("col.indexOf", () => {
|
|
||||||
expect(df).toBeDefined();
|
|
||||||
expect(df.col("A").indexOf(true)).toEqual(0);
|
|
||||||
expect(df.col("A").indexOf(false)).toEqual(1);
|
|
||||||
expect(df.col("A").indexOf(99)).toBeUndefined();
|
|
||||||
expect(df.col("A").indexOf(undefined)).toBeUndefined();
|
|
||||||
expect(df.col("A").indexOf(1)).toBeUndefined();
|
|
||||||
|
|
||||||
expect(df.col("B").indexOf(1)).toEqual(0);
|
|
||||||
expect(df.col("B").indexOf(0)).toEqual(1);
|
|
||||||
expect(df.col("B").indexOf(99)).toBeUndefined();
|
|
||||||
expect(df.col("B").indexOf(undefined)).toBeUndefined();
|
|
||||||
expect(df.col("B").indexOf(true)).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
import * as Dataframe from "../../../src/util/dataframe";
|
|
||||||
|
|
||||||
function float32Conversion(f) {
|
|
||||||
return new Float32Array([f])[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Dataframe column summary", () => {
|
|
||||||
test("empty column test", () => {
|
|
||||||
const df = Dataframe.Dataframe.create([0, 1], [[]]);
|
|
||||||
const summary = df.icol(0).summarize();
|
|
||||||
expect(summary).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: [],
|
|
||||||
categoryCounts: new Map(),
|
|
||||||
numCategories: 0
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple test", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[1, 6],
|
|
||||||
[
|
|
||||||
["n1"],
|
|
||||||
["hi"],
|
|
||||||
[true],
|
|
||||||
new Float32Array([39.3]),
|
|
||||||
new Int32Array([99]),
|
|
||||||
[1]
|
|
||||||
],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex([
|
|
||||||
"name",
|
|
||||||
"nameString",
|
|
||||||
"nameBoolean",
|
|
||||||
"nameFloat32",
|
|
||||||
"nameInt32",
|
|
||||||
"nameCategorical"
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(df.icol(0).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: ["n1"],
|
|
||||||
categoryCounts: new Map([["n1", 1]]),
|
|
||||||
numCategories: 1
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(1).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: ["hi"],
|
|
||||||
categoryCounts: new Map([["hi", 1]]),
|
|
||||||
numCategories: 1
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(2).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: [true],
|
|
||||||
categoryCounts: new Map([[true, 1]]),
|
|
||||||
numCategories: 1
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(3).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: false,
|
|
||||||
min: float32Conversion(39.3),
|
|
||||||
max: float32Conversion(39.3),
|
|
||||||
nan: 0,
|
|
||||||
ninf: 0,
|
|
||||||
pinf: 0
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(4).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: false,
|
|
||||||
min: 99,
|
|
||||||
max: 99,
|
|
||||||
nan: 0,
|
|
||||||
ninf: 0,
|
|
||||||
pinf: 0
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(5).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: [1],
|
|
||||||
categoryCounts: new Map([[1, 1]]),
|
|
||||||
numCategories: 1
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("multi test", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[3, 6],
|
|
||||||
[
|
|
||||||
["n0", "n1", "n2"],
|
|
||||||
["hi", "hi", "bye"],
|
|
||||||
[false, true, true],
|
|
||||||
new Float32Array([39.3, 39.3, 0]),
|
|
||||||
new Int32Array([99, 99, 99]),
|
|
||||||
[1, false, "0"]
|
|
||||||
],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex([
|
|
||||||
"name",
|
|
||||||
"nameString",
|
|
||||||
"nameBoolean",
|
|
||||||
"nameFloat32",
|
|
||||||
"nameInt32",
|
|
||||||
"nameCategorical"
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(df.icol(0).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining(["n0", "n1", "n2"]),
|
|
||||||
categoryCounts: new Map([["n0", 1], ["n1", 1], ["n2", 1]]),
|
|
||||||
numCategories: 3
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(1).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining(["hi", "bye"]),
|
|
||||||
categoryCounts: new Map([["hi", 2], ["bye", 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(2).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([true, false]),
|
|
||||||
categoryCounts: new Map([[true, 2], [false, 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(3).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: false,
|
|
||||||
min: 0,
|
|
||||||
max: float32Conversion(39.3),
|
|
||||||
nan: 0,
|
|
||||||
ninf: 0,
|
|
||||||
pinf: 0
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(4).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: false,
|
|
||||||
min: 99,
|
|
||||||
max: 99,
|
|
||||||
nan: 0,
|
|
||||||
ninf: 0,
|
|
||||||
pinf: 0
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(5).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([1, false, "0"]),
|
|
||||||
categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]),
|
|
||||||
numCategories: 3
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("non-finite numbers", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[4, 6],
|
|
||||||
[
|
|
||||||
["n0", "n1", "n2", "n2"],
|
|
||||||
["hi", "hi", "bye", "bye"],
|
|
||||||
[false, true, true, true],
|
|
||||||
new Float32Array([
|
|
||||||
39.3,
|
|
||||||
Number.NEGATIVE_INFINITY,
|
|
||||||
Number.NaN,
|
|
||||||
Number.POSITIVE_INFINITY
|
|
||||||
]),
|
|
||||||
new Int32Array([99, 99, 99, 99]),
|
|
||||||
[1, false, "0", "0"]
|
|
||||||
],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex([
|
|
||||||
"name",
|
|
||||||
"nameString",
|
|
||||||
"nameBoolean",
|
|
||||||
"nameFloat32",
|
|
||||||
"nameInt32",
|
|
||||||
"nameCategorical"
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(df.icol(0).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining(["n0", "n1", "n2"]),
|
|
||||||
categoryCounts: new Map([["n0", 1], ["n1", 1], ["n2", 2]]),
|
|
||||||
numCategories: 3
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(1).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining(["hi", "bye"]),
|
|
||||||
categoryCounts: new Map([["hi", 2], ["bye", 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(2).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([true, false]),
|
|
||||||
categoryCounts: new Map([[true, 2], [false, 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(3).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: false,
|
|
||||||
min: float32Conversion(39.3),
|
|
||||||
max: float32Conversion(39.3),
|
|
||||||
nan: 1,
|
|
||||||
ninf: 1,
|
|
||||||
pinf: 1
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(4).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: false,
|
|
||||||
min: 99,
|
|
||||||
max: 99,
|
|
||||||
nan: 0,
|
|
||||||
ninf: 0,
|
|
||||||
pinf: 0
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(df.icol(5).summarize()).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([1, false, "0"]),
|
|
||||||
categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]),
|
|
||||||
numCategories: 3
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
Test cases for nameCreators.js.
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
layoutDimensionName,
|
|
||||||
obsAnnoDimensionName,
|
|
||||||
diffexpDimensionName,
|
|
||||||
userDefinedDimensionName,
|
|
||||||
makeContinuousDimensionName
|
|
||||||
} from "../../src/util/nameCreators";
|
|
||||||
|
|
||||||
describe("nameCreators", () => {
|
|
||||||
const nameCreators = [
|
|
||||||
layoutDimensionName,
|
|
||||||
obsAnnoDimensionName,
|
|
||||||
diffexpDimensionName,
|
|
||||||
userDefinedDimensionName
|
|
||||||
];
|
|
||||||
|
|
||||||
test("check for namespace isolation", () => {
|
|
||||||
const foo = "foo";
|
|
||||||
nameCreators.forEach(fn => expect(fn(foo)).not.toBe(foo));
|
|
||||||
|
|
||||||
const bar = "bar";
|
|
||||||
nameCreators.forEach(fn => expect(fn(foo)).not.toBe(fn(bar)));
|
|
||||||
|
|
||||||
nameCreators.forEach(fn => {
|
|
||||||
const allOtherFn = nameCreators.filter(elmnt => elmnt !== fn);
|
|
||||||
allOtherFn.forEach(otherFn => expect(fn(foo)).not.toBe(otherFn(foo)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("check for legal keys", () => {
|
|
||||||
/* need namespace creators to return strings only */
|
|
||||||
nameCreators.forEach(fn => expect(fn("X")).toMatch(/X/));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("makeContinuousDimensionName", () => {
|
|
||||||
test("namespaces", () => {
|
|
||||||
expect(makeContinuousDimensionName({ isObs: true }, "OK")).toMatch(/OK/);
|
|
||||||
expect(makeContinuousDimensionName({ isDiffExp: true }, "OK")).toMatch(
|
|
||||||
/OK/
|
|
||||||
);
|
|
||||||
expect(makeContinuousDimensionName({ isUserDefined: true }, "OK")).toMatch(
|
|
||||||
/OK/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("error handling", () => {
|
|
||||||
expect(() => makeContinuousDimensionName({}, "oops")).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
makeContinuousDimensionName(
|
|
||||||
{ isObs: false, isDiffExp: false, isUserDefined: false },
|
|
||||||
"oops"
|
|
||||||
)
|
|
||||||
).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
makeContinuousDimensionName({ isObs: true }, "OK")
|
|
||||||
).not.toThrow();
|
|
||||||
expect(() =>
|
|
||||||
makeContinuousDimensionName({ isDiffExp: true }, "OK")
|
|
||||||
).not.toThrow();
|
|
||||||
expect(() =>
|
|
||||||
makeContinuousDimensionName({ isUserDefined: true }, "OK")
|
|
||||||
).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import quantile from "../../src/util/quantile";
|
|
||||||
|
|
||||||
describe("quantile", () => {
|
|
||||||
test("single q", () => {
|
|
||||||
const arr = new Float32Array([9, 3, 5, 6, 0]);
|
|
||||||
expect(quantile([1.0], arr)).toMatchObject([9]);
|
|
||||||
expect(quantile([0.9], arr)).toMatchObject([9]);
|
|
||||||
expect(quantile([0.8], arr)).toMatchObject([9]);
|
|
||||||
expect(quantile([0.7], arr)).toMatchObject([6]);
|
|
||||||
expect(quantile([0.6], arr)).toMatchObject([6]);
|
|
||||||
expect(quantile([0.5], arr)).toMatchObject([5]);
|
|
||||||
expect(quantile([0.4], arr)).toMatchObject([5]);
|
|
||||||
expect(quantile([0.3], arr)).toMatchObject([3]);
|
|
||||||
expect(quantile([0.2], arr)).toMatchObject([3]);
|
|
||||||
expect(quantile([0.1], arr)).toMatchObject([0]);
|
|
||||||
expect(quantile([0], arr)).toMatchObject([0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("multi q", () => {
|
|
||||||
const arr = new Float32Array([9, 3, 5, 6, 0]);
|
|
||||||
expect(quantile([0, 0.25, 0.5, 0.75, 1.0], arr)).toMatchObject([
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
9
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { range, rangeFill } from "../../src/util/range";
|
|
||||||
|
|
||||||
describe("range", () => {
|
|
||||||
test("no defaults", () => {
|
|
||||||
expect(range(0, 3, 1)).toMatchObject([0, 1, 2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("range(stop)", () => {
|
|
||||||
expect(range(3)).toMatchObject([0, 1, 2]);
|
|
||||||
expect(range(0)).toMatchObject([]);
|
|
||||||
expect(range(1)).toMatchObject([0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("range(start,stop)", () => {
|
|
||||||
expect(range(0, 0)).toMatchObject([]);
|
|
||||||
expect(range(0, 2)).toMatchObject([0, 1]);
|
|
||||||
expect(range(4, 8)).toMatchObject([4, 5, 6, 7]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("range(start, stop, step", () => {
|
|
||||||
expect(range(4, 0, -1)).toMatchObject([4, 3, 2, 1]);
|
|
||||||
expect(range(0, 4, 2)).toMatchObject([0, 2]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("rangefill", () => {
|
|
||||||
test("rangeFill(arr)", () => {
|
|
||||||
expect(rangeFill(new Int32Array(3))).toMatchObject(
|
|
||||||
new Int32Array([0, 1, 2])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test("rangeFill(arr, start)", () => {
|
|
||||||
expect(rangeFill(new Int32Array(2), 1)).toMatchObject(
|
|
||||||
new Int32Array([1, 2])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test("rangeFill(arr, start, step)", () => {
|
|
||||||
expect(rangeFill(new Int32Array(3), 2, -1)).toMatchObject(
|
|
||||||
new Int32Array([2, 1, 0])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
/* eslint no-bitwise: "off" */
|
|
||||||
import _ from "lodash";
|
|
||||||
import { flatbuffers } from "flatbuffers";
|
|
||||||
import { NetEncoding } from "../../../src/util/stateManager/matrix_generated";
|
|
||||||
|
|
||||||
/*
|
|
||||||
test data mocking REST 0.2 API responses. Used in several tests.
|
|
||||||
*/
|
|
||||||
const nObs = 10;
|
|
||||||
const nVar = 32;
|
|
||||||
const field4Categories = [83, true, "foo", 2.222222];
|
|
||||||
const fieldDCategories = [99, false, "mumble", 3.1415];
|
|
||||||
|
|
||||||
const aConfigResponse = {
|
|
||||||
config: {
|
|
||||||
features: [
|
|
||||||
{ method: "POST", path: "/cluster/", available: false },
|
|
||||||
{ method: "POST", path: "/layout/", available: false },
|
|
||||||
{ method: "POST", path: "/diffexp/", available: false },
|
|
||||||
{ method: "POST", path: "/saveLocal/", available: false }
|
|
||||||
],
|
|
||||||
displayNames: {
|
|
||||||
engine: "the little engine that could",
|
|
||||||
dataset: "all your zeros are mine"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const aSchemaResponse = {
|
|
||||||
schema: {
|
|
||||||
dataframe: {
|
|
||||||
nObs,
|
|
||||||
nVar,
|
|
||||||
type: "float32"
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
obs: {
|
|
||||||
index: "name",
|
|
||||||
columns: [
|
|
||||||
{ name: "name", type: "string" },
|
|
||||||
{ name: "field1", type: "int32" },
|
|
||||||
{ name: "field2", type: "float32" },
|
|
||||||
{ name: "field3", type: "boolean" },
|
|
||||||
{
|
|
||||||
name: "field4",
|
|
||||||
type: "categorical",
|
|
||||||
categories: field4Categories
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
var: {
|
|
||||||
index: "name",
|
|
||||||
columns: [
|
|
||||||
{ name: "name", type: "string" },
|
|
||||||
{ name: "fieldA", type: "int32" },
|
|
||||||
{ name: "fieldB", type: "float32" },
|
|
||||||
{ name: "fieldC", type: "boolean" },
|
|
||||||
{
|
|
||||||
name: "fieldD",
|
|
||||||
type: "categorical",
|
|
||||||
categories: fieldDCategories
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
obs: [{ name: "umap", type: "float32", dims: ["umap_0", "umap_1"] }],
|
|
||||||
var: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const anAnnotationsObsJSONResponse = {
|
|
||||||
names: ["name", "field1", "field2", "field3", "field4"],
|
|
||||||
data: _()
|
|
||||||
.range(nObs)
|
|
||||||
.map(idx => [
|
|
||||||
idx,
|
|
||||||
`obs${idx}`,
|
|
||||||
2 * idx,
|
|
||||||
idx + 0.0133,
|
|
||||||
!!(idx & 1),
|
|
||||||
field4Categories[idx % field4Categories.length]
|
|
||||||
])
|
|
||||||
.value()
|
|
||||||
};
|
|
||||||
|
|
||||||
const anAnnotationsVarJSONResponse = {
|
|
||||||
names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"],
|
|
||||||
data: _()
|
|
||||||
.range(nVar)
|
|
||||||
.map(idx => [
|
|
||||||
idx,
|
|
||||||
10 * idx,
|
|
||||||
idx + 2.90143,
|
|
||||||
!!(idx & 1),
|
|
||||||
fieldDCategories[idx % fieldDCategories.length],
|
|
||||||
`var${idx}`
|
|
||||||
])
|
|
||||||
.value()
|
|
||||||
};
|
|
||||||
|
|
||||||
function encodeTypedArray(builder, uType, uData) {
|
|
||||||
const uTypeName = NetEncoding.TypedArray[uType];
|
|
||||||
const ArrayType = NetEncoding[uTypeName];
|
|
||||||
const dv = ArrayType.createDataVector(builder, uData);
|
|
||||||
builder.startObject(1);
|
|
||||||
builder.addFieldOffset(0, dv, 0);
|
|
||||||
return builder.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeMatrix(columns, colIndex = undefined) {
|
|
||||||
const utf8Encoder = new TextEncoder("utf-8");
|
|
||||||
const builder = new flatbuffers.Builder(1024);
|
|
||||||
const cols = _.map(columns, carr => {
|
|
||||||
let uType;
|
|
||||||
let tarr;
|
|
||||||
if (_.every(carr, _.isNumber)) {
|
|
||||||
uType = NetEncoding.TypedArray.Float32Array;
|
|
||||||
tarr = encodeTypedArray(builder, uType, new Float32Array(carr));
|
|
||||||
} else {
|
|
||||||
uType = NetEncoding.TypedArray.JSONEncodedArray;
|
|
||||||
const json = JSON.stringify(carr);
|
|
||||||
const jsonUTF8 = utf8Encoder.encode(json);
|
|
||||||
tarr = encodeTypedArray(builder, uType, jsonUTF8);
|
|
||||||
}
|
|
||||||
NetEncoding.Column.startColumn(builder);
|
|
||||||
NetEncoding.Column.addUType(builder, uType);
|
|
||||||
NetEncoding.Column.addU(builder, tarr);
|
|
||||||
return NetEncoding.Column.endColumn(builder);
|
|
||||||
});
|
|
||||||
|
|
||||||
const encColumns = NetEncoding.Matrix.createColumnsVector(builder, cols);
|
|
||||||
|
|
||||||
let encColIndex;
|
|
||||||
if (colIndex) {
|
|
||||||
encColIndex = encodeTypedArray(
|
|
||||||
builder,
|
|
||||||
NetEncoding.TypedArray.JSONEncodedArray,
|
|
||||||
utf8Encoder.encode(JSON.stringify(colIndex))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NetEncoding.Matrix.startMatrix(builder);
|
|
||||||
NetEncoding.Matrix.addNRows(builder, columns[0].length);
|
|
||||||
NetEncoding.Matrix.addNCols(builder, columns.length);
|
|
||||||
NetEncoding.Matrix.addColumns(builder, encColumns);
|
|
||||||
if (colIndex) {
|
|
||||||
NetEncoding.Matrix.addColIndexType(
|
|
||||||
builder,
|
|
||||||
NetEncoding.TypedArray.JSONEncodedArray
|
|
||||||
);
|
|
||||||
NetEncoding.Matrix.addColIndex(builder, encColIndex);
|
|
||||||
}
|
|
||||||
const root = NetEncoding.Matrix.endMatrix(builder);
|
|
||||||
builder.finish(root);
|
|
||||||
return builder.asUint8Array();
|
|
||||||
}
|
|
||||||
|
|
||||||
const anAnnotationsObsFBSResponse = (() => {
|
|
||||||
const columns = _.zip(...anAnnotationsObsJSONResponse.data).slice(1);
|
|
||||||
return encodeMatrix(columns, anAnnotationsObsJSONResponse.names);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const anAnnotationsVarFBSResponse = (() => {
|
|
||||||
const columns = _.zip(...anAnnotationsVarJSONResponse.data).slice(1);
|
|
||||||
return encodeMatrix(columns, anAnnotationsVarJSONResponse.names);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const aLayoutFBSResponse = (() => {
|
|
||||||
const coords = [
|
|
||||||
new Float32Array(nObs).fill(Math.random()),
|
|
||||||
new Float32Array(nObs).fill(Math.random())
|
|
||||||
];
|
|
||||||
return encodeMatrix(coords, ["umap_0", "umap_1"]);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const aDataObsResponse = {
|
|
||||||
var: [2, 4, 29],
|
|
||||||
obs: _()
|
|
||||||
.range(nObs)
|
|
||||||
.map(idx => [idx, Math.random(), Math.random(), Math.random()])
|
|
||||||
.value()
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
aLayoutFBSResponse as layoutObs,
|
|
||||||
aDataObsResponse as dataObs,
|
|
||||||
anAnnotationsVarFBSResponse as annotationsVar,
|
|
||||||
anAnnotationsObsFBSResponse as annotationsObs,
|
|
||||||
aSchemaResponse as schema,
|
|
||||||
aConfigResponse as config
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import * as Universe from "../../../src/util/stateManager/universe";
|
|
||||||
import * as Dataframe from "../../../src/util/dataframe";
|
|
||||||
import * as REST from "./sampleResponses";
|
|
||||||
|
|
||||||
describe("createUniverseFromResponse", () => {
|
|
||||||
/*
|
|
||||||
test createUniverseFromResponse - this function converts
|
|
||||||
a set of REST 0.2 responses into a "new" Universe.
|
|
||||||
|
|
||||||
createUniverseFromResponse(
|
|
||||||
configResponse,
|
|
||||||
schemaResponse,
|
|
||||||
annotationsObsResponse,
|
|
||||||
annotationsVarResponse,
|
|
||||||
layoutObsResponse
|
|
||||||
) --> Universe
|
|
||||||
|
|
||||||
where:
|
|
||||||
configResponse: GET /.../config
|
|
||||||
schemaResponse: GET /.../schema
|
|
||||||
annotationsObsResponse: GET /.../annotations/obs
|
|
||||||
annotationsVarResponse: GET /.../annotations/var
|
|
||||||
layoutObsResponse: GET /.../layout/obs
|
|
||||||
|
|
||||||
See spec in docs/REST_API.md.
|
|
||||||
*/
|
|
||||||
|
|
||||||
test("create from test data", () => {
|
|
||||||
/*
|
|
||||||
create a universe from sample data nad validate its shape & contents
|
|
||||||
*/
|
|
||||||
const { nObs, nVar } = REST.schema.schema.dataframe;
|
|
||||||
const universe = Universe.createUniverseFromResponse(
|
|
||||||
REST.config,
|
|
||||||
REST.schema,
|
|
||||||
REST.annotationsObs,
|
|
||||||
REST.annotationsVar,
|
|
||||||
REST.layoutObs
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(universe).toBeDefined();
|
|
||||||
expect(universe).toMatchObject(
|
|
||||||
expect.objectContaining({
|
|
||||||
nObs,
|
|
||||||
nVar,
|
|
||||||
schema: REST.schema.schema,
|
|
||||||
obsAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
varAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
obsLayout: expect.any(Dataframe.Dataframe),
|
|
||||||
varData: expect.any(Dataframe.Dataframe)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(universe.obsAnnotations.dims).toEqual([
|
|
||||||
nObs,
|
|
||||||
REST.schema.schema.annotations.obs.columns.length
|
|
||||||
]);
|
|
||||||
expect(universe.obsLayout.dims).toEqual([nObs, 2]);
|
|
||||||
expect(universe.obsLayout.colIndex.keys()).toEqual(
|
|
||||||
universe.schema.layout.obs[0].dims
|
|
||||||
);
|
|
||||||
expect(universe.varAnnotations.dims).toEqual([
|
|
||||||
nVar,
|
|
||||||
REST.schema.schema.annotations.var.columns.length
|
|
||||||
]);
|
|
||||||
expect(universe.varData.isEmpty()).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import * as Universe from "../../../src/util/stateManager/universe";
|
|
||||||
import * as World from "../../../src/util/stateManager/world";
|
|
||||||
import * as Dataframe from "../../../src/util/dataframe";
|
|
||||||
import Crossfilter from "../../../src/util/typedCrossfilter";
|
|
||||||
import { DimTypes } from "../../../src/util/typedCrossfilter/crossfilter";
|
|
||||||
import * as REST from "./sampleResponses";
|
|
||||||
import {
|
|
||||||
obsAnnoDimensionName,
|
|
||||||
layoutDimensionName
|
|
||||||
} from "../../../src/util/nameCreators";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Helper - creates universe, world, corssfilter and dimensionMap from
|
|
||||||
the default REST test response.
|
|
||||||
*/
|
|
||||||
const defaultBigBang = () => {
|
|
||||||
/* create unverse, world, crossfilter and dimensionMap */
|
|
||||||
/* create universe */
|
|
||||||
const universe = Universe.createUniverseFromResponse(
|
|
||||||
REST.config,
|
|
||||||
REST.schema,
|
|
||||||
REST.annotationsObs,
|
|
||||||
REST.annotationsVar,
|
|
||||||
REST.layoutObs
|
|
||||||
);
|
|
||||||
/* create world */
|
|
||||||
const world = World.createWorldFromEntireUniverse(universe);
|
|
||||||
/* create crossfilter */
|
|
||||||
const crossfilter = World.createObsDimensions(
|
|
||||||
new Crossfilter(world.obsAnnotations),
|
|
||||||
world,
|
|
||||||
REST.schema.schema.layout.obs[0].dims
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
universe,
|
|
||||||
world,
|
|
||||||
crossfilter
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("createWorldFromEntireUniverse", () => {
|
|
||||||
test("create from REST sample", () => {
|
|
||||||
const universe = Universe.createUniverseFromResponse(
|
|
||||||
REST.config,
|
|
||||||
REST.schema,
|
|
||||||
REST.annotationsObs,
|
|
||||||
REST.annotationsVar,
|
|
||||||
REST.layoutObs
|
|
||||||
);
|
|
||||||
expect(universe).toBeDefined();
|
|
||||||
|
|
||||||
const world = World.createWorldFromEntireUniverse(universe);
|
|
||||||
expect(world).toBeDefined();
|
|
||||||
|
|
||||||
expect(world).toMatchObject(
|
|
||||||
expect.objectContaining({
|
|
||||||
nObs: universe.nObs,
|
|
||||||
nVar: universe.nVar,
|
|
||||||
schema: universe.schema,
|
|
||||||
obsAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
varAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
obsLayout: expect.any(Dataframe.Dataframe),
|
|
||||||
varData: expect.any(Dataframe.Dataframe),
|
|
||||||
clipQuantiles: { min: 0, max: 1 },
|
|
||||||
unclipped: {
|
|
||||||
obsAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
varData: expect.any(Dataframe.Dataframe)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("createWorldFromCurrentSelection", () => {
|
|
||||||
test("create from REST sample", () => {
|
|
||||||
const {
|
|
||||||
universe,
|
|
||||||
world: originalWorld,
|
|
||||||
crossfilter: originalCrossfilter
|
|
||||||
} = defaultBigBang();
|
|
||||||
|
|
||||||
/* mock a selection */
|
|
||||||
const crossfilter = originalCrossfilter
|
|
||||||
.select(obsAnnoDimensionName("field1"), { mode: "range", lo: 0, hi: 5 })
|
|
||||||
.select(obsAnnoDimensionName("field3"), {
|
|
||||||
mode: "exact",
|
|
||||||
values: [false]
|
|
||||||
});
|
|
||||||
|
|
||||||
/* create the world from the selection */
|
|
||||||
const world = World.createWorldBySelection(
|
|
||||||
universe,
|
|
||||||
originalWorld,
|
|
||||||
crossfilter
|
|
||||||
);
|
|
||||||
expect(world).toBeDefined();
|
|
||||||
expect(world.nObs).toEqual(crossfilter.countSelected());
|
|
||||||
|
|
||||||
/*
|
|
||||||
calculate expected values and match against result
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* matchFilter must match the dimension filters above */
|
|
||||||
const matchFilter = (df, row) => {
|
|
||||||
const field1 = df.at(row, "field1");
|
|
||||||
const field3 = df.at(row, "field3");
|
|
||||||
return field1 >= 0 && field1 < 5 && !field3;
|
|
||||||
};
|
|
||||||
const matchingIndices = _()
|
|
||||||
.range(universe.nObs)
|
|
||||||
.filter(idx => matchFilter(universe.obsAnnotations, idx))
|
|
||||||
.value();
|
|
||||||
|
|
||||||
expect(world).toMatchObject(
|
|
||||||
expect.objectContaining({
|
|
||||||
nObs: matchingIndices.length,
|
|
||||||
nVar: universe.nVar,
|
|
||||||
schema: universe.schema,
|
|
||||||
clipQuantiles: { min: 0, max: 1 },
|
|
||||||
obsAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
varAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
obsLayout: expect.any(Dataframe.Dataframe),
|
|
||||||
varData: expect.any(Dataframe.Dataframe),
|
|
||||||
unclipped: {
|
|
||||||
obsAnnotations: expect.any(Dataframe.Dataframe),
|
|
||||||
varData: expect.any(Dataframe.Dataframe)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(world.obsAnnotations.rowIndex.keys()).toEqual(
|
|
||||||
new Int32Array(matchingIndices)
|
|
||||||
);
|
|
||||||
expect(world.obsAnnotations.colIndex.keys()).toEqual(
|
|
||||||
universe.obsAnnotations.colIndex.keys()
|
|
||||||
);
|
|
||||||
expect(world.obsLayout.rowIndex.keys()).toEqual(
|
|
||||||
new Int32Array(matchingIndices)
|
|
||||||
);
|
|
||||||
expect(world.obsLayout.colIndex.keys()).toEqual(
|
|
||||||
world.schema.layout.obs[0].dims
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("createObsDimensionMap", () => {
|
|
||||||
test("when universe eq world", () => {
|
|
||||||
/*
|
|
||||||
check for:
|
|
||||||
- creates a dimension for all obsAnnotations, PLUS X/Y layout
|
|
||||||
- check that dimension typing is sane
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { crossfilter } = defaultBigBang();
|
|
||||||
const annotationNames = _.map(
|
|
||||||
REST.schema.schema.annotations.obs.columns,
|
|
||||||
c => c.name
|
|
||||||
);
|
|
||||||
const obsIndexColName = REST.schema.schema.annotations.obs.index;
|
|
||||||
const schemaByObsName = _.keyBy(
|
|
||||||
REST.schema.schema.annotations.obs.columns,
|
|
||||||
"name"
|
|
||||||
);
|
|
||||||
expect(crossfilter).toBeDefined();
|
|
||||||
annotationNames.forEach(name => {
|
|
||||||
const dim = crossfilter.dimensions[obsAnnoDimensionName(name)];
|
|
||||||
if (name === obsIndexColName) {
|
|
||||||
expect(dim).toBeUndefined();
|
|
||||||
} else {
|
|
||||||
const { type } = schemaByObsName[name];
|
|
||||||
if (type === "string" || type === "boolean" || type === "categorical") {
|
|
||||||
expect(dim.dim).toBeInstanceOf(DimTypes.enum);
|
|
||||||
} else {
|
|
||||||
expect(dim.dim).toBeInstanceOf(DimTypes.scalar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
crossfilter.dimensions[layoutDimensionName("XY")].dim
|
|
||||||
).toBeInstanceOf(DimTypes.spatial);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("worldEqUniverse", () => {
|
|
||||||
const { universe, world } = defaultBigBang();
|
|
||||||
const result = World.worldEqUniverse(world, universe);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import {
|
|
||||||
countCategoryValues2D,
|
|
||||||
clearCaches
|
|
||||||
} from "../../../src/util/stateManager/worldUtil";
|
|
||||||
import * as Dataframe from "../../../src/util/dataframe";
|
|
||||||
|
|
||||||
describe("WorldUtil cache management", () => {
|
|
||||||
test("empty", () => {
|
|
||||||
const count = countCategoryValues2D(
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
new Dataframe.Dataframe([0, 0], [])
|
|
||||||
);
|
|
||||||
expect(count).toMatchObject(new Map());
|
|
||||||
expect(count.size).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple couts", () => {
|
|
||||||
const df = new Dataframe.Dataframe(
|
|
||||||
[3, 2],
|
|
||||||
[[0, 0, 1], [false, true, false]],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex(["a", "b"])
|
|
||||||
);
|
|
||||||
const count = countCategoryValues2D("a", "b", df);
|
|
||||||
expect(count).toMatchObject(
|
|
||||||
new Map([
|
|
||||||
[0, new Map([[true, 1], [false, 1]])],
|
|
||||||
[1, new Map([[false, 1]])]
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("memo cache clear", () => {
|
|
||||||
clearCaches();
|
|
||||||
const df1 = new Dataframe.Dataframe([0, 0], []);
|
|
||||||
const df2 = new Dataframe.Dataframe(
|
|
||||||
[3, 2],
|
|
||||||
[[0, 0, 1], [false, true, false]],
|
|
||||||
null,
|
|
||||||
new Dataframe.KeyIndex(["a", "b"])
|
|
||||||
);
|
|
||||||
|
|
||||||
const count1 = countCategoryValues2D("a", "b", df1);
|
|
||||||
const count2 = countCategoryValues2D("a", "b", df1);
|
|
||||||
const count3 = countCategoryValues2D("a", "b", df1.clone());
|
|
||||||
const count4 = countCategoryValues2D("a", "b", df2);
|
|
||||||
|
|
||||||
clearCaches();
|
|
||||||
const count10 = countCategoryValues2D("a", "b", df1);
|
|
||||||
const count11 = countCategoryValues2D("a", "b", df2);
|
|
||||||
|
|
||||||
expect(count1).toEqual(count2);
|
|
||||||
expect(count1).toEqual(count3);
|
|
||||||
expect(count1).toEqual(count10);
|
|
||||||
expect(count1).not.toBe(count3);
|
|
||||||
expect(count1).not.toBe(count10);
|
|
||||||
|
|
||||||
expect(count4).toEqual(count11);
|
|
||||||
expect(count4).not.toBe(count11);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
|
|
||||||
import BitArray from "../../../src/util/typedCrossfilter/bitArray";
|
|
||||||
|
|
||||||
const defaultTestLength = 8;
|
|
||||||
|
|
||||||
describe("default select state", () => {
|
|
||||||
test("newly created Bitarray should be deselected", () => {
|
|
||||||
const ba = new BitArray(defaultTestLength);
|
|
||||||
expect(ba).toBeDefined();
|
|
||||||
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dim = ba.allocDimension();
|
|
||||||
expect(dim).toBeDefined();
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.freeDimension(dim);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("select and deselect", () => {
|
|
||||||
test("selectAll and deselectAll", () => {
|
|
||||||
const ba = new BitArray(defaultTestLength);
|
|
||||||
expect(ba).toBeDefined();
|
|
||||||
const dim1 = ba.allocDimension();
|
|
||||||
expect(dim1).toBeDefined();
|
|
||||||
ba.selectAll(dim1);
|
|
||||||
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dim2 = ba.allocDimension();
|
|
||||||
expect(dim2).toBeDefined();
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.selectAll(dim2);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.deselectAll(dim1);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.deselectAll(dim2);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.selectAll(dim1);
|
|
||||||
ba.selectAll(dim2);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.freeDimension(dim1);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.freeDimension(dim2);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("selectOne and deselectOne", () => {
|
|
||||||
const ba = new BitArray(defaultTestLength);
|
|
||||||
expect(ba).toBeDefined();
|
|
||||||
const dim = ba.allocDimension();
|
|
||||||
expect(dim).toBeDefined();
|
|
||||||
|
|
||||||
ba.selectOne(dim, 0);
|
|
||||||
expect(ba.isSelected(0)).toEqual(true);
|
|
||||||
for (let i = 1; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.deselectOne(dim, 0);
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.selectOne(dim, 1);
|
|
||||||
expect(ba.isSelected(1)).toEqual(true);
|
|
||||||
expect(ba.isSelected(0)).toEqual(false);
|
|
||||||
for (let i = 2; i < defaultTestLength; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.selectAll(dim);
|
|
||||||
ba.deselectOne(dim, defaultTestLength - 1);
|
|
||||||
expect(ba.isSelected(defaultTestLength - 1)).toEqual(false);
|
|
||||||
for (let i = 0; i < defaultTestLength - 1; i += 1) {
|
|
||||||
expect(ba.isSelected(i)).toEqual(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("selectionCount", () => {
|
|
||||||
test("simple", () => {
|
|
||||||
const ba = new BitArray(defaultTestLength);
|
|
||||||
expect(ba).toBeDefined();
|
|
||||||
const dim1 = ba.allocDimension();
|
|
||||||
expect(dim1).toBeDefined();
|
|
||||||
const dim2 = ba.allocDimension();
|
|
||||||
expect(dim2).toBeDefined();
|
|
||||||
|
|
||||||
expect(ba.selectionCount()).toEqual(0);
|
|
||||||
ba.selectAll(dim1);
|
|
||||||
expect(ba.selectionCount()).toEqual(0);
|
|
||||||
ba.selectAll(dim2);
|
|
||||||
expect(ba.selectionCount()).toEqual(defaultTestLength);
|
|
||||||
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
|
||||||
ba.deselectOne(dim1, i);
|
|
||||||
expect(ba.selectionCount()).toEqual(defaultTestLength - i - 1);
|
|
||||||
expect(ba.selectionCount()).toEqual(ba.countAllOnes());
|
|
||||||
}
|
|
||||||
|
|
||||||
ba.freeDimension(dim1);
|
|
||||||
ba.freeDimension(dim2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fillBySelection", () => {
|
|
||||||
test("sets values correctly", () => {
|
|
||||||
const ba = new BitArray(defaultTestLength);
|
|
||||||
expect(ba).toBeDefined();
|
|
||||||
const dim = ba.allocDimension();
|
|
||||||
expect(dim).toBeDefined();
|
|
||||||
|
|
||||||
const arr = new Int32Array(defaultTestLength);
|
|
||||||
arr.fill(0);
|
|
||||||
const truth = new Int32Array(defaultTestLength);
|
|
||||||
truth.fill(0);
|
|
||||||
|
|
||||||
// initial state should be deselected
|
|
||||||
ba.fillBySelection(arr, 1, 0);
|
|
||||||
expect(arr).toEqual(expect.not.arrayContaining([1]));
|
|
||||||
|
|
||||||
// selectAll
|
|
||||||
ba.selectAll(dim);
|
|
||||||
ba.fillBySelection(arr, 1, 0);
|
|
||||||
expect(arr).toEqual(expect.not.arrayContaining([0]));
|
|
||||||
|
|
||||||
// deselectOne
|
|
||||||
ba.deselectOne(dim, 3);
|
|
||||||
ba.fillBySelection(arr, 1, 0);
|
|
||||||
truth.fill(1);
|
|
||||||
truth[3] = 0;
|
|
||||||
expect(arr).toEqual(truth);
|
|
||||||
|
|
||||||
// deselectAll
|
|
||||||
ba.deselectAll(dim);
|
|
||||||
ba.fillBySelection(arr, 1, 0);
|
|
||||||
truth.fill(0);
|
|
||||||
expect(arr).toEqual(truth);
|
|
||||||
|
|
||||||
// selectOne
|
|
||||||
ba.selectOne(dim, 5);
|
|
||||||
ba.fillBySelection(arr, 6, 1);
|
|
||||||
truth.fill(1);
|
|
||||||
truth[5] = 6;
|
|
||||||
expect(arr).toEqual(truth);
|
|
||||||
|
|
||||||
// should be deselected after dimension disposal
|
|
||||||
ba.freeDimension(dim);
|
|
||||||
ba.fillBySelection(arr, 3, 9);
|
|
||||||
truth.fill(9);
|
|
||||||
expect(arr).toEqual(truth);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,434 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import { polygonContains } from "d3";
|
|
||||||
|
|
||||||
import Crossfilter from "../../../src/util/typedCrossfilter";
|
|
||||||
|
|
||||||
const someData = [
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:17:54Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 190,
|
|
||||||
tip: 100,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["001"],
|
|
||||||
coords: [0, 0],
|
|
||||||
nonFinite: 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:20:19Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 190,
|
|
||||||
tip: 100,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["001", "005"],
|
|
||||||
coords: [0.4, 0.4],
|
|
||||||
nonFinite: Number.NaN
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:28:54Z",
|
|
||||||
quantity: 1,
|
|
||||||
total: 300,
|
|
||||||
tip: 200,
|
|
||||||
type: "visa",
|
|
||||||
productIDs: ["004", "005"],
|
|
||||||
coords: [0.3, 0.1],
|
|
||||||
nonFinite: Number.POSITIVE_INFINITY
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:30:43Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 90,
|
|
||||||
tip: 0,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["001", "002"],
|
|
||||||
coords: [0.392, 0.1],
|
|
||||||
nonFinite: Number.NEGATIVE_INFINITY
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:48:46Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 90,
|
|
||||||
tip: 0,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["005"],
|
|
||||||
coords: [0.7, 0.0482],
|
|
||||||
nonFinite: 1.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:53:41Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 90,
|
|
||||||
tip: 0,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["001", "004", "005"],
|
|
||||||
coords: [0.9999, 1.0],
|
|
||||||
nonFinite: Number.NaN
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:54:06Z",
|
|
||||||
quantity: 1,
|
|
||||||
total: 100,
|
|
||||||
tip: 0,
|
|
||||||
type: "cash",
|
|
||||||
productIDs: ["001", "002", "003", "004", "005"],
|
|
||||||
coords: [0.384, 0.6938],
|
|
||||||
nonFinite: 99.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T16:58:03Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 90,
|
|
||||||
tip: 0,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["001"],
|
|
||||||
coords: [0.4822, 0.482],
|
|
||||||
nonFinite: Number.NaN
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T17:07:21Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 90,
|
|
||||||
tip: 0,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["004", "005"],
|
|
||||||
coords: [0.2234, 0],
|
|
||||||
nonFinite: Number.NaN
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T17:22:59Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 90,
|
|
||||||
tip: 0,
|
|
||||||
type: "tab",
|
|
||||||
productIDs: ["001", "002", "004", "005"],
|
|
||||||
coords: [0.382, 0.38485],
|
|
||||||
nonFinite: -1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T17:25:45Z",
|
|
||||||
quantity: 2,
|
|
||||||
total: 200,
|
|
||||||
tip: 0,
|
|
||||||
type: "cash",
|
|
||||||
productIDs: ["002"],
|
|
||||||
coords: [0.998, 0.8472],
|
|
||||||
nonFinite: 0.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "2011-11-14T17:29:52Z",
|
|
||||||
quantity: 1,
|
|
||||||
total: 200,
|
|
||||||
tip: 100,
|
|
||||||
type: "visa",
|
|
||||||
productIDs: ["004"],
|
|
||||||
coords: [0.8273, 0.3384],
|
|
||||||
nonFinite: 0.0
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let payments = null;
|
|
||||||
beforeEach(() => {
|
|
||||||
payments = new Crossfilter(someData);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ImmutableTypedCrossfilter", () => {
|
|
||||||
test("create crossfilter", () => {
|
|
||||||
expect(payments).toBeDefined();
|
|
||||||
expect(payments.size()).toEqual(someData.length);
|
|
||||||
expect(payments.all()).toEqual(someData);
|
|
||||||
|
|
||||||
const p = payments
|
|
||||||
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
|
||||||
.select("quantity", { mode: "all" });
|
|
||||||
expect(p).toBeDefined();
|
|
||||||
expect(p.all()).toEqual(someData);
|
|
||||||
expect(p.size()).toEqual(someData.length);
|
|
||||||
expect(p.isElementSelected(0)).toBeTruthy();
|
|
||||||
expect(p.countSelected()).toEqual(someData.length);
|
|
||||||
expect(p.allSelected()).toEqual(someData);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("immutability", () => {
|
|
||||||
/*
|
|
||||||
the following should return a new crossfilter:
|
|
||||||
- addDimension()
|
|
||||||
- delDimension()
|
|
||||||
- select
|
|
||||||
*/
|
|
||||||
const p2 = payments.addDimension(
|
|
||||||
"quantity",
|
|
||||||
"scalar",
|
|
||||||
(i, data) => data[i].quantity,
|
|
||||||
Int32Array
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(payments).not.toBe(p2);
|
|
||||||
const p3 = p2.select("quantity", { mode: "all" });
|
|
||||||
expect(p3).not.toBe(p2);
|
|
||||||
|
|
||||||
const p4 = p3.delDimension("quantity");
|
|
||||||
expect(p4).not.toBe(p3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("select all and none", () => {
|
|
||||||
let p = payments
|
|
||||||
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
|
||||||
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
|
|
||||||
.addDimension("total", "scalar", (i, d) => d[i].total, Float32Array)
|
|
||||||
.addDimension("type", "enum", (i, d) => d[i].type);
|
|
||||||
expect(p).toBeDefined();
|
|
||||||
|
|
||||||
/* expect all records to be selected - default init state */
|
|
||||||
expect(p.allSelected()).toEqual(someData);
|
|
||||||
expect(p.countSelected()).toEqual(someData.length);
|
|
||||||
expect(p.allSelectedMask()).toEqual(
|
|
||||||
new Uint8Array(someData.length).fill(1)
|
|
||||||
);
|
|
||||||
expect(p.fillByIsSelected(new Uint8Array(someData.length), 99, 0)).toEqual(
|
|
||||||
new Uint8Array(someData.length).fill(99)
|
|
||||||
);
|
|
||||||
for (let i = 0; i < someData.length; i += 1) {
|
|
||||||
expect(p.isElementSelected(i)).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* expect a selectAll on one dimension to change nothing */
|
|
||||||
p = p.select("tip", { mode: "all" });
|
|
||||||
expect(p.allSelected()).toEqual(someData);
|
|
||||||
|
|
||||||
/* ditto */
|
|
||||||
p = p.select("quantity", { mode: "all" });
|
|
||||||
expect(p.allSelected()).toEqual(someData);
|
|
||||||
|
|
||||||
/* select none on one dimension */
|
|
||||||
p = p.select("type", { mode: "none" });
|
|
||||||
expect(p.allSelected()).toEqual([]);
|
|
||||||
expect(p.countSelected()).toEqual(0);
|
|
||||||
expect(p.allSelectedMask()).toEqual(
|
|
||||||
new Uint8Array(someData.length).fill(0)
|
|
||||||
);
|
|
||||||
expect(p.fillByIsSelected(new Uint8Array(someData.length), 99, 0)).toEqual(
|
|
||||||
new Uint8Array(someData.length).fill(0)
|
|
||||||
);
|
|
||||||
for (let i = 0; i < someData.length; i += 1) {
|
|
||||||
expect(p.isElementSelected(i)).toBeFalsy();
|
|
||||||
}
|
|
||||||
|
|
||||||
p = p.select("quantity", { mode: "none" });
|
|
||||||
expect(p.allSelected()).toEqual([]);
|
|
||||||
|
|
||||||
// invert the first none; should have no effect because type is
|
|
||||||
// still not filtered.
|
|
||||||
p = p.select("quantity", { mode: "all" });
|
|
||||||
expect(p.allSelected()).toEqual([]);
|
|
||||||
|
|
||||||
/* select all of type; should select all records */
|
|
||||||
p = p.select("type", { mode: "all" });
|
|
||||||
expect(p.allSelected()).toEqual(someData);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("scalar dimension", () => {
|
|
||||||
let p;
|
|
||||||
beforeEach(() => {
|
|
||||||
p = payments
|
|
||||||
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
|
||||||
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
|
|
||||||
.select("tip", { mode: "all" });
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
select modes: all, none, exact, range
|
|
||||||
*/
|
|
||||||
test("all", () => {
|
|
||||||
expect(p.select("quantity", { mode: "all" }).countSelected()).toEqual(
|
|
||||||
someData.length
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test("none", () => {
|
|
||||||
expect(p.select("quantity", { mode: "none" }).countSelected()).toEqual(0);
|
|
||||||
});
|
|
||||||
test.each([[[]], [[2]], [[2, 1]], [[9, 82]], [[0, 1]]])("exact: %p", v =>
|
|
||||||
expect(
|
|
||||||
p.select("quantity", { mode: "exact", values: v }).countSelected()
|
|
||||||
).toEqual(_.filter(someData, d => v.includes(d.quantity)).length)
|
|
||||||
);
|
|
||||||
test.each([[0, 1], [1, 2], [0, 99], [99, 100000]])("range %p", (lo, hi) =>
|
|
||||||
expect(
|
|
||||||
p.select("quantity", { mode: "range", lo, hi }).countSelected()
|
|
||||||
).toEqual(
|
|
||||||
_.filter(someData, d => d.quantity >= lo && d.quantity < hi).length
|
|
||||||
)
|
|
||||||
);
|
|
||||||
test("bad mode", () => {
|
|
||||||
expect(() => p.select("type", { mode: "bad mode" })).toThrow(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("enum dimension", () => {
|
|
||||||
let p;
|
|
||||||
beforeEach(() => {
|
|
||||||
p = payments.addDimension("type", "enum", (i, d) => d[i].type);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("all", () => {
|
|
||||||
expect(p.select("type", { mode: "all" }).countSelected()).toEqual(
|
|
||||||
someData.length
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test("none", () => {
|
|
||||||
expect(p.select("type", { mode: "none" }).countSelected()).toEqual(0);
|
|
||||||
});
|
|
||||||
test.each([
|
|
||||||
[[]],
|
|
||||||
[["tab"]],
|
|
||||||
[["visa"]],
|
|
||||||
[["visa", "tab"]],
|
|
||||||
[["cash", "tab", "visa"]]
|
|
||||||
])("exact: %p", v =>
|
|
||||||
expect(
|
|
||||||
p.select("type", { mode: "exact", values: v }).countSelected()
|
|
||||||
).toEqual(_.filter(someData, d => v.includes(d.type)).length)
|
|
||||||
);
|
|
||||||
test("range", () => {
|
|
||||||
expect(() => p.select("type", { mode: "range", lo: 0, hi: 9 })).toThrow(
|
|
||||||
Error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test("bad mode", () => {
|
|
||||||
expect(() => p.select("type", { mode: "bad mode" })).toThrow(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("spatial dimension", () => {
|
|
||||||
let p;
|
|
||||||
beforeEach(() => {
|
|
||||||
const X = someData.map(r => r.coords[0]);
|
|
||||||
const Y = someData.map(r => r.coords[1]);
|
|
||||||
p = payments.addDimension("coords", "spatial", X, Y);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("all", () => {
|
|
||||||
expect(p.select("coords", { mode: "all" }).countSelected()).toEqual(
|
|
||||||
someData.length
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test("none", () => {
|
|
||||||
expect(p.select("coords", { mode: "none" }).countSelected()).toEqual(0);
|
|
||||||
});
|
|
||||||
test.each([[0, 0, 1, 1], [0, 0, 0.5, 0.5], [0.5, 0.5, 1, 1]])(
|
|
||||||
"within-rect %d %d %d %d",
|
|
||||||
(minX, minY, maxX, maxY) => {
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("coords", { mode: "within-rect", minX, minY, maxX, maxY })
|
|
||||||
.allSelected()
|
|
||||||
).toEqual(
|
|
||||||
_.filter(someData, d => {
|
|
||||||
const [x, y] = d.coords;
|
|
||||||
return minX <= x && x < maxX && minY <= y && y < maxY;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
test.each([
|
|
||||||
[[[0, 0], [0, 1], [1, 1], [1, 0]]],
|
|
||||||
[[[0, 0], [0, 0.5], [0.5, 0.5], [0.5, 0]]]
|
|
||||||
])("within-polygon %p", polygon => {
|
|
||||||
expect(
|
|
||||||
p.select("coords", { mode: "within-polygon", polygon }).allSelected()
|
|
||||||
).toEqual(_.filter(someData, d => polygonContains(polygon, d.coords)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("non-finite scalars", () => {
|
|
||||||
let p;
|
|
||||||
beforeEach(() => {
|
|
||||||
p = payments
|
|
||||||
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
|
|
||||||
.addDimension(
|
|
||||||
"nonFinite",
|
|
||||||
"scalar",
|
|
||||||
(i, d) => d[i].nonFinite,
|
|
||||||
Float32Array
|
|
||||||
)
|
|
||||||
.select("quantity", { mode: "all" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("all or none", () => {
|
|
||||||
expect(p.select("nonFinite", { mode: "all" }).countSelected()).toEqual(
|
|
||||||
someData.length
|
|
||||||
);
|
|
||||||
expect(p.select("nonFinite", { mode: "none" }).countSelected()).toEqual(
|
|
||||||
0
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("exact", () => {
|
|
||||||
expect(
|
|
||||||
p.select("nonFinite", { mode: "exact", values: [0] }).countSelected()
|
|
||||||
).toEqual(3);
|
|
||||||
expect(
|
|
||||||
p.select("nonFinite", { mode: "exact", values: [1] }).countSelected()
|
|
||||||
).toEqual(1);
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("nonFinite", {
|
|
||||||
mode: "exact",
|
|
||||||
values: [Number.POSITIVE_INFINITY]
|
|
||||||
})
|
|
||||||
.countSelected()
|
|
||||||
).toEqual(1);
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("nonFinite", {
|
|
||||||
mode: "exact",
|
|
||||||
values: [Number.NEGATIVE_INFINITY]
|
|
||||||
})
|
|
||||||
.countSelected()
|
|
||||||
).toEqual(1);
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("nonFinite", { mode: "exact", values: [Number.NaN] })
|
|
||||||
.countSelected()
|
|
||||||
).toEqual(4);
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("nonFinite", {
|
|
||||||
mode: "exact",
|
|
||||||
values: [Number.POSITIVE_INFINITY, 0, 1, 99]
|
|
||||||
})
|
|
||||||
.countSelected()
|
|
||||||
).toEqual(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("range", () => {
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("nonFinite", {
|
|
||||||
mode: "range",
|
|
||||||
lo: 0,
|
|
||||||
hi: Number.POSITIVE_INFINITY
|
|
||||||
})
|
|
||||||
.countSelected()
|
|
||||||
).toEqual(5);
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("nonFinite", {
|
|
||||||
mode: "range",
|
|
||||||
lo: 0,
|
|
||||||
hi: Number.NaN
|
|
||||||
})
|
|
||||||
.countSelected()
|
|
||||||
).toEqual(6);
|
|
||||||
expect(
|
|
||||||
p
|
|
||||||
.select("nonFinite", {
|
|
||||||
mode: "range",
|
|
||||||
lo: Number.NEGATIVE_INFINITY,
|
|
||||||
hi: Number.POSITIVE_INFINITY
|
|
||||||
})
|
|
||||||
.countSelected()
|
|
||||||
).toEqual(7);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
|
|
||||||
// const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals");
|
|
||||||
import PositiveIntervals from "../../../src/util/typedCrossfilter/positiveIntervals";
|
|
||||||
|
|
||||||
describe("canonicalize", () => {
|
|
||||||
test("empty", () => {
|
|
||||||
expect(PositiveIntervals.canonicalize([])).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple, already correct", () => {
|
|
||||||
expect(PositiveIntervals.canonicalize([[0, 1]])).toEqual([[0, 1]]);
|
|
||||||
expect(PositiveIntervals.canonicalize([[0, 1], [2, 3]])).toEqual([
|
|
||||||
[0, 1],
|
|
||||||
[2, 3]
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("non-canonical, need to be canonicalized", () => {
|
|
||||||
expect(PositiveIntervals.canonicalize([[0, 1], [1, 2]])).toEqual([[0, 2]]);
|
|
||||||
expect(PositiveIntervals.canonicalize([[1, 2], [2, 3]])).toEqual([[1, 3]]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("union", () => {
|
|
||||||
test("empty range", () => {
|
|
||||||
expect(PositiveIntervals.union([], [])).toEqual([]);
|
|
||||||
expect(PositiveIntervals.union([], [[1, 2]])).toEqual([[1, 2]]);
|
|
||||||
expect(PositiveIntervals.union([], [[1, 2], [3, 4]])).toEqual([
|
|
||||||
[1, 2],
|
|
||||||
[3, 4]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.union([[3, 4]], [])).toEqual([[3, 4]]);
|
|
||||||
expect(PositiveIntervals.union([[1, 2], [3, 4]], [])).toEqual([
|
|
||||||
[1, 2],
|
|
||||||
[3, 4]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.union([[3, 3]], [])).toEqual([[3, 3]]);
|
|
||||||
expect(PositiveIntervals.union([], [[3, 3]])).toEqual([[3, 3]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple ranges", () => {
|
|
||||||
expect(PositiveIntervals.union([[1, 2]], [[2, 3]])).toEqual([[1, 3]]);
|
|
||||||
expect(PositiveIntervals.union([[2, 3]], [[1, 2]])).toEqual([[1, 3]]);
|
|
||||||
expect(PositiveIntervals.union([[1, 2]], [[3, 4]])).toEqual([
|
|
||||||
[1, 2],
|
|
||||||
[3, 4]
|
|
||||||
]);
|
|
||||||
expect(
|
|
||||||
PositiveIntervals.union([[1, 2], [3, 4]], [[6, 7], [19, 40]])
|
|
||||||
).toEqual([[1, 2], [3, 4], [6, 7], [19, 40]]);
|
|
||||||
expect(PositiveIntervals.union([[1, 4]], [[1, 1], [3, 4]])).toEqual([
|
|
||||||
[1, 4]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.union([[3, 3]], [[4, 4]])).toEqual([
|
|
||||||
[3, 3],
|
|
||||||
[4, 4]
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("intersection", () => {
|
|
||||||
test("empty range", () => {
|
|
||||||
expect(PositiveIntervals.intersection([], [])).toEqual([]);
|
|
||||||
expect(PositiveIntervals.intersection([], [[1, 2]])).toEqual([]);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 2]], [])).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple", () => {
|
|
||||||
expect(PositiveIntervals.intersection([[1, 2]], [[2, 3]])).toEqual([]);
|
|
||||||
expect(PositiveIntervals.intersection([[2, 3]], [[1, 2]])).toEqual([]);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 10]], [[1, 10]])).toEqual([
|
|
||||||
[1, 10]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 10]], [[2, 8]])).toEqual([
|
|
||||||
[2, 8]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.intersection([[2, 8]], [[1, 10]])).toEqual([
|
|
||||||
[2, 8]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 10]], [[2, 12]])).toEqual([
|
|
||||||
[2, 10]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.intersection([[2, 12]], [[1, 10]])).toEqual([
|
|
||||||
[2, 10]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 10]], [[1, 8]])).toEqual([
|
|
||||||
[1, 8]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 8]], [[1, 10]])).toEqual([
|
|
||||||
[1, 8]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 10]], [[1, 2], [6, 9]])).toEqual(
|
|
||||||
[[1, 2], [6, 9]]
|
|
||||||
);
|
|
||||||
expect(PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])).toEqual(
|
|
||||||
[[1363, 2638]]
|
|
||||||
);
|
|
||||||
expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([
|
|
||||||
[1, 2]
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("difference", () => {
|
|
||||||
test("empty", () => {
|
|
||||||
expect(PositiveIntervals.difference([], [])).toEqual([]);
|
|
||||||
expect(PositiveIntervals.difference([], [[1, 10]])).toEqual([]);
|
|
||||||
expect(PositiveIntervals.difference([[1, 10]], [])).toEqual([[1, 10]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple", () => {
|
|
||||||
expect(PositiveIntervals.difference([[1, 2], [3, 4]], [])).toEqual([
|
|
||||||
[1, 2],
|
|
||||||
[3, 4]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[5, 10]])).toEqual([
|
|
||||||
[1, 2],
|
|
||||||
[3, 5]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[0, 5]])).toEqual([
|
|
||||||
[5, 10]
|
|
||||||
]);
|
|
||||||
expect(
|
|
||||||
PositiveIntervals.difference([[0, 2638]], [[0, 1363], [2055, 2638]])
|
|
||||||
).toEqual([[1363, 2055]]);
|
|
||||||
expect(
|
|
||||||
PositiveIntervals.difference([[0, 1363], [2055, 2638]], [[0, 2638]])
|
|
||||||
).toEqual([]);
|
|
||||||
expect(PositiveIntervals.difference([[0, 10]], [[0, 1]])).toEqual([
|
|
||||||
[1, 10]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.difference([[0, 10]], [[1, 2]])).toEqual([
|
|
||||||
[0, 1],
|
|
||||||
[2, 10]
|
|
||||||
]);
|
|
||||||
expect(PositiveIntervals.difference([[0, 10]], [[9, 10]])).toEqual([
|
|
||||||
[0, 9]
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
import {
|
|
||||||
sortArray,
|
|
||||||
sortIndex,
|
|
||||||
lowerBound,
|
|
||||||
upperBound,
|
|
||||||
lowerBoundIndirect,
|
|
||||||
upperBoundIndirect
|
|
||||||
} from "../../../src/util/typedCrossfilter/sort";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Sort tests should keep in mind that there are separate code
|
|
||||||
paths for:
|
|
||||||
- small vs. large arrays (insertionsort only)
|
|
||||||
- float-only typed arrays vs. other array types (non-finite handling)
|
|
||||||
- indexed vs. direct sort
|
|
||||||
*/
|
|
||||||
|
|
||||||
const pInf = Number.POSITIVE_INFINITY;
|
|
||||||
const nInf = Number.NEGATIVE_INFINITY;
|
|
||||||
|
|
||||||
function fillRange(arr, start = 0) {
|
|
||||||
const larr = arr;
|
|
||||||
for (let i = 0, len = larr.length; i < len; i += 1) {
|
|
||||||
larr[i] = i + start;
|
|
||||||
}
|
|
||||||
return larr;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillRand(arr) {
|
|
||||||
for (let i = 0, len = arr.length; i < len; i += 1) {
|
|
||||||
arr[i] = Math.random();
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("sortArray", () => {
|
|
||||||
describe("JS vals", () => {
|
|
||||||
[
|
|
||||||
[true, false],
|
|
||||||
["a", "b", "0", "1"],
|
|
||||||
[0, "a", true, null, undefined, 3.1415],
|
|
||||||
fillRand(new Array(1000)),
|
|
||||||
["a", NaN, null, pInf]
|
|
||||||
].map((val, idx) =>
|
|
||||||
test(`JS vals ${idx}`, () => {
|
|
||||||
expect(sortArray(val)).toMatchObject(val.sort());
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("finite numbers", () => {
|
|
||||||
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map(Type =>
|
|
||||||
test(Type.name, () => {
|
|
||||||
expect(sortArray(Type.from([6, 5, 4, 3, 2, 1, 0]))).toMatchObject(
|
|
||||||
Type.from([0, 1, 2, 3, 4, 5, 6])
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(sortArray(Type.from([6, 5, 4, 3, 2, 1]))).toMatchObject(
|
|
||||||
Type.from([1, 2, 3, 4, 5, 6])
|
|
||||||
);
|
|
||||||
|
|
||||||
const source = fillRand(new Type(1000));
|
|
||||||
expect(sortArray(Type.from(source))).toMatchObject(
|
|
||||||
Type.from(source).sort()
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("non-finite numbers", () => {
|
|
||||||
test("inifinity", () => {
|
|
||||||
expect(sortArray(new Float32Array([pInf, nInf, 0, 1, 2]))).toMatchObject(
|
|
||||||
new Float32Array([nInf, 0, 1, 2, pInf])
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
sortArray(new Float32Array([pInf, nInf, pInf, nInf]))
|
|
||||||
).toMatchObject(new Float32Array([nInf, nInf, pInf, pInf]));
|
|
||||||
expect(
|
|
||||||
sortArray(new Float32Array([pInf, nInf, pInf, nInf, pInf]))
|
|
||||||
).toMatchObject(new Float32Array([nInf, nInf, pInf, pInf, pInf]));
|
|
||||||
expect(
|
|
||||||
sortArray(
|
|
||||||
new Float32Array(100).fill(Infinity, 0, 50).fill(-Infinity, 50, 100)
|
|
||||||
)
|
|
||||||
).toMatchObject(
|
|
||||||
new Float32Array(100).fill(-Infinity, 0, 50).fill(Infinity, 50, 100)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test("NaN", () => {
|
|
||||||
expect(sortArray(new Float64Array([NaN, 2, 1, 0]))).toMatchObject(
|
|
||||||
new Float64Array([0, 1, 2, NaN])
|
|
||||||
);
|
|
||||||
expect(sortArray(new Float32Array([NaN, 2, 1, 0]))).toMatchObject(
|
|
||||||
new Float32Array([0, 1, 2, NaN])
|
|
||||||
);
|
|
||||||
expect(sortArray(new Float32Array([NaN, 2, NaN, 1, 0]))).toMatchObject(
|
|
||||||
new Float32Array([0, 1, 2, NaN, NaN])
|
|
||||||
);
|
|
||||||
expect(sortArray(new Float32Array([NaN, 2, 1, NaN, 0]))).toMatchObject(
|
|
||||||
new Float32Array([0, 1, 2, NaN, NaN])
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
sortArray(fillRange(new Float32Array(100)).fill(NaN, 0, 10))
|
|
||||||
).toMatchObject(fillRange(new Float32Array(100), 10).fill(NaN, 90, 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("mixed numbers", () => {
|
|
||||||
expect(
|
|
||||||
sortArray(new Float32Array([NaN, pInf, nInf, NaN, NaN]))
|
|
||||||
).toMatchObject(new Float32Array([nInf, pInf, NaN, NaN, NaN]));
|
|
||||||
expect(
|
|
||||||
sortArray(new Float32Array([NaN, pInf, nInf, NaN, 1, NaN, 2]))
|
|
||||||
).toMatchObject(new Float32Array([nInf, 1, 2, pInf, NaN, NaN, NaN]));
|
|
||||||
expect(
|
|
||||||
sortArray(new Float32Array([NaN, pInf, nInf, 0, 1, NaN, 2]))
|
|
||||||
).toMatchObject(new Float32Array([nInf, 0, 1, 2, pInf, NaN, NaN]));
|
|
||||||
expect(
|
|
||||||
sortArray(
|
|
||||||
fillRange(new Float32Array(100))
|
|
||||||
.fill(NaN, 0, 10)
|
|
||||||
.fill(Infinity, 10, 20)
|
|
||||||
)
|
|
||||||
).toMatchObject(
|
|
||||||
fillRange(new Float32Array(100), 20)
|
|
||||||
.fill(Infinity, 80, 90)
|
|
||||||
.fill(NaN, 90, 100)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sortIndex", () => {
|
|
||||||
describe("finite numbers", () => {
|
|
||||||
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map(Type =>
|
|
||||||
test(Type.name, () => {
|
|
||||||
const source1 = Type.from([6, 5, 4, 3, 2, 1, 0]);
|
|
||||||
const index1 = fillRange(new Uint32Array(source1.length));
|
|
||||||
expect(sortIndex(index1, source1)).toMatchObject(
|
|
||||||
index1.sort((a, b) => source1[a] - source1[b])
|
|
||||||
);
|
|
||||||
|
|
||||||
const source2 = Type.from([6, 5, 4, 3, 2, 1]);
|
|
||||||
const index2 = fillRange(new Uint32Array(source2.length));
|
|
||||||
expect(sortIndex(index2, source2)).toMatchObject(
|
|
||||||
index2.sort((a, b) => source1[a] - source1[b])
|
|
||||||
);
|
|
||||||
|
|
||||||
const source3 = fillRand(new Type(1000));
|
|
||||||
const index3 = fillRange(new Uint32Array(source3.length));
|
|
||||||
expect(sortIndex(index3, source3)).toMatchObject(
|
|
||||||
index3.sort((a, b) => source1[a] - source1[b])
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("non-finite numbers", () => {
|
|
||||||
test("mixed numbers", () => {
|
|
||||||
const source1 = new Float32Array([NaN, pInf, nInf, NaN, 1, NaN, 2]);
|
|
||||||
const index1 = fillRange(new Uint32Array(source1.length));
|
|
||||||
expect(sortIndex(index1, source1)).toMatchObject(
|
|
||||||
new Uint32Array([2, 4, 6, 1, 0, 3, 5])
|
|
||||||
);
|
|
||||||
|
|
||||||
const source2 = new Float32Array([NaN, pInf, nInf, 0, 1, NaN, 2]);
|
|
||||||
const index2 = fillRange(new Uint32Array(source2.length));
|
|
||||||
expect(sortIndex(index2, source2)).toMatchObject(
|
|
||||||
new Uint32Array([2, 3, 4, 6, 1, 0, 5])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("lowerBound", () => {
|
|
||||||
test("non-float path", () => {
|
|
||||||
expect(lowerBound([], 0, 0, 0)).toEqual(0);
|
|
||||||
|
|
||||||
expect(lowerBound([0, 1, 2, 3], -1, 0, 4)).toEqual(0);
|
|
||||||
expect(lowerBound([0, 1, 2, 3], 0, 0, 4)).toEqual(0);
|
|
||||||
expect(lowerBound([0, 1, 2, 3], 1, 0, 4)).toEqual(1);
|
|
||||||
expect(lowerBound([0, 1, 2, 3], 3, 0, 4)).toEqual(3);
|
|
||||||
expect(lowerBound([0, 1, 2, 3], 4, 0, 4)).toEqual(4);
|
|
||||||
|
|
||||||
expect(lowerBound([0, 1, 2, 3, 4], -1, 0, 5)).toEqual(0);
|
|
||||||
expect(lowerBound([0, 1, 2, 3, 4], 0, 0, 5)).toEqual(0);
|
|
||||||
expect(lowerBound([0, 1, 2, 3, 4], 2, 0, 5)).toEqual(2);
|
|
||||||
expect(lowerBound([0, 1, 2, 3, 4], 4, 0, 5)).toEqual(4);
|
|
||||||
expect(lowerBound([0, 1, 2, 3, 4], 5, 0, 5)).toEqual(5);
|
|
||||||
|
|
||||||
expect(lowerBound([0, 2, 4, 6, 8], 5, 0, 5)).toEqual(3);
|
|
||||||
expect(lowerBound([0, 2, 2, 2, 8], 5, 0, 5)).toEqual(4);
|
|
||||||
|
|
||||||
expect(lowerBound([0, 1, 2, 3, 4, 5, 6, 7, 8], 3, 2, 4)).toEqual(3);
|
|
||||||
expect(lowerBound([0, 1, 2, 3, 4, 5, 6, 7, 8], 99, 2, 4)).toEqual(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("float path, finites", () => {
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 1, 0, 4)).toEqual(1);
|
|
||||||
|
|
||||||
expect(lowerBound(new Float32Array([]), 0, 0, 0)).toEqual(0);
|
|
||||||
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3]), -1, 0, 4)).toEqual(0);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 0, 0, 4)).toEqual(0);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 1, 0, 4)).toEqual(1);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 3, 0, 4)).toEqual(3);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 4, 0, 4)).toEqual(4);
|
|
||||||
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), -1, 0, 5)).toEqual(0);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 0, 0, 5)).toEqual(0);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 2, 0, 5)).toEqual(2);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 4, 0, 5)).toEqual(4);
|
|
||||||
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 5, 0, 5)).toEqual(5);
|
|
||||||
|
|
||||||
expect(lowerBound(new Float32Array([0, 2, 4, 6, 8]), 5, 0, 5)).toEqual(3);
|
|
||||||
expect(lowerBound(new Float32Array([0, 2, 2, 2, 8]), 5, 0, 5)).toEqual(4);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
lowerBound(new Float32Array([0, 1, 2, 3, 4, 5, 6, 7, 8]), 3, 2, 4)
|
|
||||||
).toEqual(3);
|
|
||||||
expect(
|
|
||||||
lowerBound(new Float32Array([0, 1, 2, 3, 4, 5, 6, 7, 8]), 99, 2, 4)
|
|
||||||
).toEqual(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("float path, non-finite", () => {
|
|
||||||
expect(
|
|
||||||
lowerBound(
|
|
||||||
new Float32Array([-Infinity, 0, 1, Infinity, NaN]),
|
|
||||||
-Infinity,
|
|
||||||
0,
|
|
||||||
5
|
|
||||||
)
|
|
||||||
).toEqual(0);
|
|
||||||
expect(
|
|
||||||
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), 0, 0, 5)
|
|
||||||
).toEqual(1);
|
|
||||||
expect(
|
|
||||||
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), 1, 0, 5)
|
|
||||||
).toEqual(2);
|
|
||||||
expect(
|
|
||||||
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), 2, 0, 5)
|
|
||||||
).toEqual(3);
|
|
||||||
expect(
|
|
||||||
lowerBound(
|
|
||||||
new Float32Array([-Infinity, 0, 1, Infinity, NaN]),
|
|
||||||
Infinity,
|
|
||||||
0,
|
|
||||||
5
|
|
||||||
)
|
|
||||||
).toEqual(3);
|
|
||||||
expect(
|
|
||||||
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), NaN, 0, 5)
|
|
||||||
).toEqual(4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import {
|
|
||||||
sliceByIndex,
|
|
||||||
makeSortIndex
|
|
||||||
} from "../../../src/util/typedCrossfilter/util";
|
|
||||||
import { rangeFill as fillRange } from "../../../src/util/range";
|
|
||||||
|
|
||||||
describe("fillRange", () => {
|
|
||||||
test("Array", () => {
|
|
||||||
expect(fillRange(new Array(6))).toMatchObject([0, 1, 2, 3, 4, 5]);
|
|
||||||
expect(fillRange(new Array(4), 1)).toMatchObject([1, 2, 3, 4]);
|
|
||||||
expect(fillRange([])).toMatchObject([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Uint32Array", () => {
|
|
||||||
expect(fillRange(new Uint32Array(6))).toMatchObject(
|
|
||||||
new Uint32Array([0, 1, 2, 3, 4, 5])
|
|
||||||
);
|
|
||||||
expect(fillRange(new Array(4), 1)).toMatchObject(
|
|
||||||
new Uint32Array([1, 2, 3, 4])
|
|
||||||
);
|
|
||||||
expect(fillRange([])).toMatchObject(new Uint32Array([]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sliceByIndex", () => {
|
|
||||||
test("Array", () => {
|
|
||||||
expect(sliceByIndex([0, 1, 2, 3, 4], [0, 1, 2])).toMatchObject([0, 1, 2]);
|
|
||||||
expect(sliceByIndex([0, 1, 2, 3, 4], [2, 1, 0])).toMatchObject([2, 1, 0]);
|
|
||||||
expect(sliceByIndex([0, 1, 2, 3, 4], [])).toMatchObject([]);
|
|
||||||
expect(sliceByIndex([], [])).toMatchObject([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Uint32Array", () => {
|
|
||||||
expect(
|
|
||||||
sliceByIndex([0, 1, 2, 3, 4], new Uint32Array([0, 1, 2]))
|
|
||||||
).toMatchObject([0, 1, 2]);
|
|
||||||
expect(
|
|
||||||
sliceByIndex([0, 1, 2, 3, 4], new Uint32Array([2, 1, 0]))
|
|
||||||
).toMatchObject([2, 1, 0]);
|
|
||||||
expect(sliceByIndex([0, 1, 2, 3, 4], new Uint32Array([]))).toMatchObject(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
expect(sliceByIndex([], new Uint32Array([]))).toMatchObject([]);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
sliceByIndex(new Uint32Array([0, 1, 2, 3, 4]), new Uint32Array([0, 1, 2]))
|
|
||||||
).toMatchObject(new Uint32Array([0, 1, 2]));
|
|
||||||
expect(
|
|
||||||
sliceByIndex(new Uint32Array([0, 1, 2, 3, 4]), new Uint32Array([2, 1, 0]))
|
|
||||||
).toMatchObject(new Uint32Array([2, 1, 0]));
|
|
||||||
expect(
|
|
||||||
sliceByIndex(
|
|
||||||
new Uint32Array([0, 1, 2, 3, 4]),
|
|
||||||
new Uint32Array(new Uint32Array([]))
|
|
||||||
)
|
|
||||||
).toMatchObject(new Uint32Array([]));
|
|
||||||
expect(
|
|
||||||
sliceByIndex(new Uint32Array([]), new Uint32Array([]))
|
|
||||||
).toMatchObject(new Uint32Array([]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Float32Array", () => {
|
|
||||||
expect(
|
|
||||||
sliceByIndex(new Float32Array([0, 1, 2, 3, 4]), [0, 1, 2])
|
|
||||||
).toMatchObject(new Float32Array([0, 1, 2]));
|
|
||||||
expect(
|
|
||||||
sliceByIndex(new Float32Array([0, 1, 2, 3, 4]), [2, 1, 0])
|
|
||||||
).toMatchObject(new Float32Array([2, 1, 0]));
|
|
||||||
expect(sliceByIndex(new Float32Array([0, 1, 2, 3, 4]), [])).toMatchObject(
|
|
||||||
new Float32Array([])
|
|
||||||
);
|
|
||||||
expect(sliceByIndex(new Float32Array([]), [])).toMatchObject(
|
|
||||||
new Float32Array([])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("makeSortIndex", () => {
|
|
||||||
test("Array", () => {
|
|
||||||
expect(makeSortIndex([3, 2, 1, 0])).toMatchObject(
|
|
||||||
new Uint32Array([3, 2, 1, 0])
|
|
||||||
);
|
|
||||||
expect(makeSortIndex([3, 2, 1, 0, 4])).toMatchObject(
|
|
||||||
new Uint32Array([3, 2, 1, 0, 4])
|
|
||||||
);
|
|
||||||
expect(makeSortIndex([])).toMatchObject(new Uint32Array([]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Float32Array", () => {
|
|
||||||
expect(makeSortIndex(new Float32Array([3, 2, 1, 0]))).toMatchObject(
|
|
||||||
new Uint32Array([3, 2, 1, 0])
|
|
||||||
);
|
|
||||||
expect(makeSortIndex(new Float32Array([3, 2, 1, 0, 4]))).toMatchObject(
|
|
||||||
new Uint32Array([3, 2, 1, 0, 4])
|
|
||||||
);
|
|
||||||
expect(makeSortIndex(new Float32Array([]))).toMatchObject(
|
|
||||||
new Uint32Array([])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Int32Array", () => {
|
|
||||||
expect(makeSortIndex(new Int32Array([3, 2, 1, 0]))).toMatchObject(
|
|
||||||
new Uint32Array([3, 2, 1, 0])
|
|
||||||
);
|
|
||||||
expect(makeSortIndex(new Int32Array([3, 2, 1, 0, 4]))).toMatchObject(
|
|
||||||
new Uint32Array([3, 2, 1, 0, 4])
|
|
||||||
);
|
|
||||||
expect(makeSortIndex(new Int32Array([]))).toMatchObject(
|
|
||||||
new Uint32Array([])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
babelrc: false,
|
|
||||||
cacheDirectory: true,
|
|
||||||
presets: [
|
|
||||||
["modern-browsers", { loose: true, modules: false }],
|
|
||||||
"@babel/preset-react"
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
"@babel/plugin-proposal-function-bind",
|
|
||||||
["@babel/plugin-proposal-decorators", { legacy: true }],
|
|
||||||
["@babel/plugin-proposal-class-properties", { loose: true }],
|
|
||||||
"@babel/plugin-proposal-export-namespace-from",
|
|
||||||
"@babel/plugin-proposal-optional-chaining",
|
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
babelrc: false,
|
|
||||||
presets: [
|
|
||||||
["modern-browsers", { loose: true, modules: false }],
|
|
||||||
"@babel/preset-react"
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
"@babel/plugin-proposal-function-bind",
|
|
||||||
["@babel/plugin-proposal-decorators", { legacy: true }],
|
|
||||||
["@babel/plugin-proposal-class-properties", { loose: true }],
|
|
||||||
"@babel/plugin-proposal-export-namespace-from",
|
|
||||||
"@babel/plugin-transform-react-constant-elements",
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
"@babel/plugin-proposal-optional-chaining",
|
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: "babel-eslint",
|
|
||||||
extends: ["airbnb", "prettier", "prettier/react"],
|
|
||||||
env: { browser: true, commonjs: true, es6: true },
|
|
||||||
globals: { expect: true },
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2017,
|
|
||||||
sourceType: "module",
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
generators: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"no-magic-numbers": "off",
|
|
||||||
"no-nested-ternary": "off",
|
|
||||||
"func-style": "off",
|
|
||||||
"arrow-parens": "off",
|
|
||||||
"no-use-before-define": "off",
|
|
||||||
"react/jsx-filename-extension": "off",
|
|
||||||
"comma-dangle": "off",
|
|
||||||
"no-underscore-dangle": "off",
|
|
||||||
quotes: ["error", "double"],
|
|
||||||
"implicit-arrow-linebreak": "off",
|
|
||||||
"operator-linebreak": [
|
|
||||||
"error",
|
|
||||||
"after",
|
|
||||||
{ overrides: { "?": "before", ":": "before" } }
|
|
||||||
],
|
|
||||||
"no-console": "off",
|
|
||||||
"spaced-comment": ["error", "always", { exceptions: ["*"] }],
|
|
||||||
"no-param-reassign": "off",
|
|
||||||
"object-curly-newline": ["error", { consistent: true }],
|
|
||||||
"react/prop-types": [0],
|
|
||||||
"space-before-function-paren": "off",
|
|
||||||
"function-paren-newline": "off",
|
|
||||||
"prefer-destructuring": ["error", { object: true, array: false }]
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ["**/*.test.js"],
|
|
||||||
env: {
|
|
||||||
jest: true // now **/*.test.js files' env has both es6 *and* jest
|
|
||||||
},
|
|
||||||
// Can't extend in overrides: https://github.com/eslint/eslint/issues/8813
|
|
||||||
// "extends": ["plugin:jest/recommended"]
|
|
||||||
plugins: ["jest"],
|
|
||||||
rules: {
|
|
||||||
"jest/no-disabled-tests": "warn",
|
|
||||||
"jest/no-focused-tests": "error",
|
|
||||||
"jest/no-identical-title": "error",
|
|
||||||
"jest/prefer-to-have-length": "warn",
|
|
||||||
"jest/valid-expect": "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
const path = require("path");
|
|
||||||
const webpack = require("webpack");
|
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
|
||||||
|
|
||||||
const src = path.resolve("src");
|
|
||||||
const nodeModules = path.resolve("node_modules");
|
|
||||||
|
|
||||||
const babelOptions = require("../babel/babel.dev");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: "development",
|
|
||||||
devtool: "eval",
|
|
||||||
entry: ["./src/index"],
|
|
||||||
output: {
|
|
||||||
path: path.resolve("build"),
|
|
||||||
pathinfo: true,
|
|
||||||
filename: "static/js/bundle.js",
|
|
||||||
publicPath: "/"
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
include: src,
|
|
||||||
loader: "babel-loader",
|
|
||||||
options: babelOptions
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
include: src,
|
|
||||||
exclude: [path.resolve(src, "index.css")],
|
|
||||||
loader: [
|
|
||||||
{
|
|
||||||
loader: "style-loader"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: "css-loader",
|
|
||||||
options: {
|
|
||||||
modules: true,
|
|
||||||
localIdentName: "[name]__[local]___[hash:base64:5]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /index\.css$/,
|
|
||||||
include: [path.resolve(src, "index.css")],
|
|
||||||
loader: [
|
|
||||||
{
|
|
||||||
loader: "style-loader"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: "css-loader"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{ test: /\.json$/, include: [src, nodeModules], loader: "json-loader" },
|
|
||||||
{
|
|
||||||
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
|
|
||||||
include: [src, nodeModules],
|
|
||||||
loader: "file-loader",
|
|
||||||
query: { name: "static/media/[name].[ext]" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(mp4|webm)(\?.*)?$/,
|
|
||||||
include: [src, nodeModules],
|
|
||||||
loader: "url-loader",
|
|
||||||
query: { limit: 10000, name: "static/media/[name].[ext]" }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
inject: true,
|
|
||||||
template: path.resolve("index.html"),
|
|
||||||
favicon: path.resolve("favicon.png")
|
|
||||||
}),
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
__REACT_DEVTOOLS_GLOBAL_HOOK__: "({ isDisabled: true })"
|
|
||||||
})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
const path = require("path");
|
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
|
||||||
const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");
|
|
||||||
const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin");
|
|
||||||
|
|
||||||
const src = path.resolve("src");
|
|
||||||
const nodeModules = path.resolve("node_modules");
|
|
||||||
|
|
||||||
const babelOptions = require("../babel/babel.prod");
|
|
||||||
|
|
||||||
const publicPath = "";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: "production",
|
|
||||||
bail: true,
|
|
||||||
cache: false,
|
|
||||||
devtool: "cheap-source-map",
|
|
||||||
entry: ["./src/index.js"],
|
|
||||||
output: {
|
|
||||||
path: path.resolve("build"),
|
|
||||||
filename: "static/js/[name].[chunkhash:8].js",
|
|
||||||
publicPath
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
include: src,
|
|
||||||
loader: "babel-loader",
|
|
||||||
options: babelOptions
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
include: src,
|
|
||||||
exclude: [path.resolve(src, "index.css")],
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.loader,
|
|
||||||
{
|
|
||||||
loader: "css-loader",
|
|
||||||
options: {
|
|
||||||
modules: true,
|
|
||||||
importLoaders: 1,
|
|
||||||
localIdentName: "[name]__[local]___[hash:base64:5]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /index\.css$/,
|
|
||||||
include: [path.resolve(src, "index.css")],
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.loader,
|
|
||||||
{
|
|
||||||
loader: "css-loader",
|
|
||||||
options: {
|
|
||||||
importLoaders: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.json$/,
|
|
||||||
include: [src, nodeModules],
|
|
||||||
loader: "json-loader",
|
|
||||||
exclude: /manifest.json$/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
|
|
||||||
include: [src, nodeModules],
|
|
||||||
loader: "file-loader",
|
|
||||||
options: {
|
|
||||||
name: "[name].[hash:8].[ext]",
|
|
||||||
outputPath: "static/media/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(mp4|webm)(\?.*)?$/,
|
|
||||||
include: [src, nodeModules],
|
|
||||||
loader: "url-loader",
|
|
||||||
query: { limit: 10000, name: "static/media/[name].[hash:8].[ext]" }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
inject: "body",
|
|
||||||
filename: "index.html",
|
|
||||||
template: path.resolve("index_template.html"),
|
|
||||||
favicon: path.resolve("favicon.png"),
|
|
||||||
inlineSource: ".(js|css)$",
|
|
||||||
minify: {
|
|
||||||
removeComments: true,
|
|
||||||
collapseWhitespace: true,
|
|
||||||
removeRedundantAttributes: true,
|
|
||||||
useShortDoctype: true,
|
|
||||||
removeEmptyAttributes: true,
|
|
||||||
removeStyleLinkTypeAttributes: true,
|
|
||||||
keepClosingSlash: true,
|
|
||||||
minifyJS: true,
|
|
||||||
minifyCSS: true,
|
|
||||||
minifyURLs: true
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new HtmlWebpackInlineSourcePlugin(),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: "static/css/[name].[contenthash:8].css"
|
|
||||||
}),
|
|
||||||
new SWPrecacheWebpackPlugin({
|
|
||||||
cacheId: "cellxgene",
|
|
||||||
filename: "service-worker.js"
|
|
||||||
})
|
|
||||||
],
|
|
||||||
performance: {
|
|
||||||
maxEntrypointSize: 2000000,
|
|
||||||
maxAssetSize: 2000000
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 312 B |
@@ -1,29 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>cell×gene</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
html, body, p, h1, h2, h3, h4, h5, h6, span, button, input, label, text, div {
|
|
||||||
font-family: 'Roboto Condensed','Helvetica Neue','Helvetica','Arial',sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript>
|
|
||||||
|
|
||||||
<div id="root"></div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>cell×gene</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
html, body, p, h1, h2, h3, h4, h5, h6, span, button, input, label, text, div {
|
|
||||||
font-family: 'Roboto Condensed', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.CELLXGENE = {};
|
|
||||||
window.CELLXGENE.API = {
|
|
||||||
prefix: window.location.href + "api/",
|
|
||||||
version: "v0.2/"
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>,
|
|
||||||
please <strong>enable JS</strong> to make this app work.
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id="root"></div>
|
|
||||||
{% for script in SCRIPTS %}
|
|
||||||
<script type="text/javascript" src="{{script | safe}}"></script>
|
|
||||||
{% endfor %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Generated
-16961
File diff suppressed because it is too large
Load Diff
@@ -1,155 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cellxgene",
|
|
||||||
"version": "0.10.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
|
||||||
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
|
||||||
"scripts": {
|
|
||||||
"backend-dev": "python3.6 -m venv cellxgene && source cellxgene/bin/activate && yes | pip uninstall cellxgene || true && pip install -e .. && cellxgene launch ",
|
|
||||||
"build": "npm run clean && webpack --config configuration/webpack/webpack.config.prod.js",
|
|
||||||
"clean": "rimraf build",
|
|
||||||
"dev": "npm run clean && webpack --config configuration/webpack/webpack.config.dev.js",
|
|
||||||
"e2e": "node node_modules/jest/bin/jest.js --verbose false --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.js",
|
|
||||||
"lint": "eslint src",
|
|
||||||
"smoke-test": "start-server-and-test start-server-for-test :5000 e2e",
|
|
||||||
"start": "node server/development.js",
|
|
||||||
"start-server-for-test": "cellxgene launch -p 5000 ../example-dataset/pbmc3k.h5ad",
|
|
||||||
"test": "node node_modules/jest/bin/jest.js",
|
|
||||||
"unit-test": "node node_modules/jest/bin/jest.js --testPathIgnorePatterns e2e"
|
|
||||||
},
|
|
||||||
"engineStrict": true,
|
|
||||||
"engines": {
|
|
||||||
"npm": ">=3.0.0"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "./configuration/eslint/eslint.js"
|
|
||||||
},
|
|
||||||
"eslintIgnore": [
|
|
||||||
"src/util/stateManager/matrix_generated.js"
|
|
||||||
],
|
|
||||||
"resolutions": {
|
|
||||||
"eslint-scope": "3.7.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@blueprintjs/core": "^3.15.1",
|
|
||||||
"@blueprintjs/icons": "^3.8.0",
|
|
||||||
"@blueprintjs/select": "^3.8.0",
|
|
||||||
"canvas-fit": "^1.5.0",
|
|
||||||
"d3": "^4.10.0",
|
|
||||||
"d3-scale-chromatic": "^1.3.0",
|
|
||||||
"flatbuffers": "^1.10.2",
|
|
||||||
"font-color-contrast": "^1.0.3",
|
|
||||||
"fuzzysort": "^1.1.4",
|
|
||||||
"gl-mat4": "^1.1.4",
|
|
||||||
"gl-vec3": "^1.1.3",
|
|
||||||
"gl-matrix": "^3.0.0",
|
|
||||||
"is-number": "^7.0.0",
|
|
||||||
"key-pressed": "0.0.1",
|
|
||||||
"lodash": "^4.17.4",
|
|
||||||
"memoize-one": "^5.0.4",
|
|
||||||
"mouse-position": "^2.0.1",
|
|
||||||
"mouse-pressed": "^1.0.0",
|
|
||||||
"orbit-camera": "^1.0.0",
|
|
||||||
"query-string": "^6.5.0",
|
|
||||||
"react": "^16.8.6",
|
|
||||||
"react-autocomplete": "^1.7.2",
|
|
||||||
"react-dom": "^16.8.6",
|
|
||||||
"react-helmet": "^5.2.1",
|
|
||||||
"react-icons": "^3.7.0",
|
|
||||||
"react-redux": "^7.0.3",
|
|
||||||
"redux": "^4.0.1",
|
|
||||||
"redux-thunk": "^2.2.0",
|
|
||||||
"regl": "^1.3.11",
|
|
||||||
"scroll-speed": "^1.0.0",
|
|
||||||
"urijs": "^1.19.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.4.4",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
|
||||||
"@babel/plugin-proposal-decorators": "^7.4.4",
|
|
||||||
"@babel/plugin-proposal-export-namespace-from": "^7.2.0",
|
|
||||||
"@babel/plugin-proposal-function-bind": "^7.2.0",
|
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
|
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
|
||||||
"@babel/plugin-transform-react-constant-elements": "^7.2.0",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
|
||||||
"@babel/preset-env": "^7.4.4",
|
|
||||||
"@babel/preset-react": "^7.0.0",
|
|
||||||
"@babel/register": "^7.4.4",
|
|
||||||
"@babel/runtime": "^7.4.4",
|
|
||||||
"babel-eslint": "^10.0.1",
|
|
||||||
"babel-jest": "^24.8.0",
|
|
||||||
"babel-loader": "^8.0.6",
|
|
||||||
"babel-preset-modern-browsers": "^14.0.0",
|
|
||||||
"chalk": "^2.4.2",
|
|
||||||
"connect-history-api-fallback": "^1.6.0",
|
|
||||||
"copy-webpack-plugin": "^5.0.3",
|
|
||||||
"css-loader": "^2.1.1",
|
|
||||||
"eslint": "^5.16.0",
|
|
||||||
"eslint-config-airbnb": "^17.1.0",
|
|
||||||
"eslint-config-prettier": "^4.2.0",
|
|
||||||
"eslint-loader": "^2.1.2",
|
|
||||||
"eslint-plugin-filenames": "^1.3.2",
|
|
||||||
"eslint-plugin-import": "^2.17.2",
|
|
||||||
"eslint-plugin-jest": "^22.5.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.1",
|
|
||||||
"eslint-plugin-react": "^7.13.0",
|
|
||||||
"express": "^4.14.0",
|
|
||||||
"file-loader": "^3.0.1",
|
|
||||||
"html-webpack-inline-source-plugin": "0.0.10",
|
|
||||||
"html-webpack-plugin": "^3.2.0",
|
|
||||||
"jest": "^24.8.0",
|
|
||||||
"jest-puppeteer": "^4.1.1",
|
|
||||||
"json-loader": "^0.5.4",
|
|
||||||
"mini-css-extract-plugin": "^0.6.0",
|
|
||||||
"puppeteer": "^1.16.0",
|
|
||||||
"rimraf": "^2.6.3",
|
|
||||||
"serve-favicon": "^2.3.0",
|
|
||||||
"start-server-and-test": "^1.9.0",
|
|
||||||
"style-loader": "^0.23.1",
|
|
||||||
"sw-precache-webpack-plugin": "^0.11.5",
|
|
||||||
"url-loader": "^1.1.0",
|
|
||||||
"webpack": "^4.31.0",
|
|
||||||
"webpack-cli": "^3.3.2",
|
|
||||||
"webpack-dev-middleware": "^3.6.2"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"testMatch": [
|
|
||||||
"**/__tests__/**/?(*.)(spec|test).js?(x)"
|
|
||||||
],
|
|
||||||
"testURL": "http://localhost/",
|
|
||||||
"setupFiles": [
|
|
||||||
"./__tests__/setupMissingGlobals.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"babel": {
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env",
|
|
||||||
"@babel/preset-react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-proposal-function-bind",
|
|
||||||
[
|
|
||||||
"@babel/plugin-proposal-decorators",
|
|
||||||
{
|
|
||||||
"legacy": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
{
|
|
||||||
"loose": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@babel/plugin-proposal-export-namespace-from",
|
|
||||||
"@babel/plugin-transform-react-constant-elements",
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
"@babel/plugin-proposal-optional-chaining",
|
|
||||||
"@babel/plugin-proposal-nullish-coalescing-operator"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
// jshint esversion: 6
|
|
||||||
var path = require("path");
|
|
||||||
var historyApiFallback = require("connect-history-api-fallback");
|
|
||||||
var chalk = require("chalk");
|
|
||||||
var express = require("express");
|
|
||||||
var favicon = require("serve-favicon");
|
|
||||||
var webpack = require("webpack");
|
|
||||||
var config = require("../configuration/webpack/webpack.config.dev");
|
|
||||||
var utils = require("./utils");
|
|
||||||
|
|
||||||
process.env.NODE_ENV = "development";
|
|
||||||
|
|
||||||
var PORT = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
// Set up compiler
|
|
||||||
var compiler = webpack(config);
|
|
||||||
|
|
||||||
compiler.plugin("invalid", () => {
|
|
||||||
utils.clearConsole();
|
|
||||||
console.log("Compiling...");
|
|
||||||
});
|
|
||||||
|
|
||||||
compiler.plugin("done", stats => {
|
|
||||||
utils.formatStats(stats, PORT);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Launch server
|
|
||||||
var app = express();
|
|
||||||
|
|
||||||
app.use(historyApiFallback({ verbose: false }));
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
require("webpack-dev-middleware")(compiler, {
|
|
||||||
logLevel: "warn",
|
|
||||||
publicPath: config.output.publicPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(favicon("./favicon.png"));
|
|
||||||
|
|
||||||
app.get("*", (req, res) => {
|
|
||||||
res.sendFile(path.resolve("index.html"));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(PORT, err => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.clearConsole();
|
|
||||||
console.log(chalk.cyan("Starting the development server..."));
|
|
||||||
console.log();
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
// jshint esversion: 6
|
|
||||||
var chalk = require("chalk");
|
|
||||||
|
|
||||||
var friendlySyntaxErrorLabel = "Syntax error:";
|
|
||||||
|
|
||||||
function isLikelyASyntaxError(message) {
|
|
||||||
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatMessage(message) {
|
|
||||||
return message
|
|
||||||
.replace("Module build failed: SyntaxError:", friendlySyntaxErrorLabel)
|
|
||||||
.replace(
|
|
||||||
/Module not found: Error: Cannot resolve 'file' or 'directory'/,
|
|
||||||
"Module not found:"
|
|
||||||
)
|
|
||||||
.replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, "")
|
|
||||||
.replace("./~/css-loader!./~/postcss-loader!", "");
|
|
||||||
}
|
|
||||||
var clearConsole = () => {
|
|
||||||
process.stdout.write("\x1bc");
|
|
||||||
};
|
|
||||||
|
|
||||||
var formatStats = (stats, port) => {
|
|
||||||
clearConsole();
|
|
||||||
var hasErrors = stats.hasErrors();
|
|
||||||
var hasWarnings = stats.hasWarnings();
|
|
||||||
if (!hasErrors && !hasWarnings) {
|
|
||||||
console.log(chalk.green("Compiled successfully!"));
|
|
||||||
console.log();
|
|
||||||
console.log("The app is running at http://localhost:" + port + "/");
|
|
||||||
console.log();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = stats.toJson();
|
|
||||||
var formattedErrors = json.errors.map(
|
|
||||||
message => "Error in " + formatMessage(message)
|
|
||||||
);
|
|
||||||
var formattedWarnings = json.warnings.map(
|
|
||||||
message => "Warning in " + formatMessage(message)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasErrors) {
|
|
||||||
console.log(chalk.red("Failed to compile."));
|
|
||||||
console.log();
|
|
||||||
if (formattedErrors.some(isLikelyASyntaxError)) {
|
|
||||||
formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
|
|
||||||
}
|
|
||||||
formattedErrors.forEach(message => {
|
|
||||||
console.log(message);
|
|
||||||
console.log();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasWarnings) {
|
|
||||||
console.log(chalk.yellow("Compiled with warnings."));
|
|
||||||
console.log();
|
|
||||||
formattedWarnings.forEach(message => {
|
|
||||||
console.log(message);
|
|
||||||
console.log();
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("You may use special comments to disable some warnings.");
|
|
||||||
console.log(
|
|
||||||
"Use " +
|
|
||||||
chalk.yellow("// eslint-disable-next-line") +
|
|
||||||
" to ignore the next line."
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"Use " +
|
|
||||||
chalk.yellow("/* eslint-disable */") +
|
|
||||||
" to ignore all warnings in a file."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { formatStats: formatStats, clearConsole: clearConsole };
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import _ from "lodash";
|
|
||||||
import * as globals from "../globals";
|
|
||||||
import { Universe } from "../util/stateManager";
|
|
||||||
import {
|
|
||||||
catchErrorsWrap,
|
|
||||||
doJsonRequest,
|
|
||||||
doBinaryRequest,
|
|
||||||
dispatchNetworkErrorMessageToUser
|
|
||||||
} from "../util/actionHelpers";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Bootstrap application with the initial data loading.
|
|
||||||
* /config - application configuration
|
|
||||||
* /schema - schema of dataframe
|
|
||||||
* /annotations - all metadata annotation
|
|
||||||
* /layout - all default layout
|
|
||||||
*/
|
|
||||||
const doInitialDataLoad = () =>
|
|
||||||
catchErrorsWrap(async dispatch => {
|
|
||||||
dispatch({ type: "initial data load start" });
|
|
||||||
|
|
||||||
try {
|
|
||||||
/*
|
|
||||||
Step 1 - config & schema, all JSON
|
|
||||||
*/
|
|
||||||
const requestJson = ["config", "schema"]
|
|
||||||
.map(r => `${globals.API.prefix}${globals.API.version}${r}`)
|
|
||||||
.map(url => doJsonRequest(url));
|
|
||||||
const stepOneResults = await Promise.all(requestJson);
|
|
||||||
/* set config defaults */
|
|
||||||
const config = { ...globals.configDefaults, ...stepOneResults[0].config };
|
|
||||||
const schema = stepOneResults[1];
|
|
||||||
|
|
||||||
/*
|
|
||||||
Step 2 - dataframes, all binary. NOTE: uses results of step 1.
|
|
||||||
*/
|
|
||||||
/* only load names for var annotations, if possible*/
|
|
||||||
const varIndexName = schema?.schema?.annotations?.var?.index;
|
|
||||||
const varAnnotationsQuery = varIndexName
|
|
||||||
? `?annotation-name=${varIndexName}`
|
|
||||||
: "";
|
|
||||||
const varAnnotationsURL = `annotations/var${varAnnotationsQuery}`;
|
|
||||||
const requestBinary = ["annotations/obs", varAnnotationsURL, "layout/obs"]
|
|
||||||
.map(r => `${globals.API.prefix}${globals.API.version}${r}`)
|
|
||||||
.map(url => doBinaryRequest(url));
|
|
||||||
const stepTwoResults = await Promise.all(requestBinary);
|
|
||||||
const [obsAnno, varAnno, obsLayout] = [...stepTwoResults];
|
|
||||||
|
|
||||||
const universe = Universe.createUniverseFromResponse(
|
|
||||||
config,
|
|
||||||
schema,
|
|
||||||
obsAnno,
|
|
||||||
varAnno,
|
|
||||||
obsLayout
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "configuration load complete",
|
|
||||||
config
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "initial data load complete (universe exists)",
|
|
||||||
universe
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({ type: "initial data load error", error });
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Set the view (world) to current selection. Placeholder for an async action
|
|
||||||
which also does re-layout.
|
|
||||||
*/
|
|
||||||
const regraph = () => (dispatch, getState) => {
|
|
||||||
const { universe, world, crossfilter } = getState();
|
|
||||||
dispatch({
|
|
||||||
type: "set World to current selection",
|
|
||||||
universe,
|
|
||||||
world,
|
|
||||||
crossfilter
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Throws
|
|
||||||
const dispatchExpressionErrors = (dispatch, res) => {
|
|
||||||
const msg = `Unexpected HTTP response while fetching expression data ${
|
|
||||||
res.status
|
|
||||||
}, ${res.statusText}`;
|
|
||||||
dispatchNetworkErrorMessageToUser(msg);
|
|
||||||
throw new Error(msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Fetch expression vectors for each gene in genes. This is NOT an action
|
|
||||||
function, but rather a helper to be called from an action helper that
|
|
||||||
needs expression data.
|
|
||||||
|
|
||||||
Transparently utilizes cached data if it is already present.
|
|
||||||
*/
|
|
||||||
async function _doRequestExpressionData(dispatch, getState, genes) {
|
|
||||||
const state = getState();
|
|
||||||
const { universe } = state;
|
|
||||||
const varIndexName = universe.schema.annotations.var.index;
|
|
||||||
|
|
||||||
/* helper for this function only */
|
|
||||||
const fetchData = async geneNames => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${globals.API.prefix}${globals.API.version}data/var`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify({
|
|
||||||
filter: {
|
|
||||||
var: {
|
|
||||||
annotation_value: [{ name: varIndexName, values: geneNames }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
headers: new Headers({
|
|
||||||
accept: "application/octet-stream",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
res.headers.get("Content-Type") !== "application/octet-stream"
|
|
||||||
) {
|
|
||||||
// WILL throw
|
|
||||||
return dispatchExpressionErrors(dispatch, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.arrayBuffer();
|
|
||||||
return Universe.convertDataFBStoObject(universe, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* preload data already in cache */
|
|
||||||
let expressionData = _.transform(
|
|
||||||
genes,
|
|
||||||
(expData, g) => {
|
|
||||||
const data = universe.varData.col(g);
|
|
||||||
if (data) {
|
|
||||||
expData[g] = data.asArray();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
); // --> { gene: data }
|
|
||||||
/* make a list of genes for which we do not have data */
|
|
||||||
const genesToFetch = _.filter(genes, g => expressionData[g] === undefined);
|
|
||||||
|
|
||||||
dispatch({ type: "expression load start" });
|
|
||||||
|
|
||||||
/* Fetch data for any genes not in cache */
|
|
||||||
if (genesToFetch.length) {
|
|
||||||
try {
|
|
||||||
const newExpressionData = await fetchData(genesToFetch);
|
|
||||||
expressionData = {
|
|
||||||
...expressionData,
|
|
||||||
...newExpressionData
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({ type: "expression load error", error });
|
|
||||||
throw error; // rethrow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: "expression load success", expressionData });
|
|
||||||
return expressionData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestSingleGeneExpressionCountsForColoringPOST(gene) {
|
|
||||||
return async (dispatch, getState) => {
|
|
||||||
dispatch({ type: "get single gene expression for coloring started" });
|
|
||||||
try {
|
|
||||||
await _doRequestExpressionData(dispatch, getState, [gene]);
|
|
||||||
const { world } = getState();
|
|
||||||
dispatch({
|
|
||||||
type: "color by expression",
|
|
||||||
gene,
|
|
||||||
data: {
|
|
||||||
[gene]: world.varData.col(gene).asArray()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: "get single gene expression for coloring error",
|
|
||||||
error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestUserDefinedGene = gene => async (dispatch, getState) => {
|
|
||||||
dispatch({ type: "request user defined gene started" });
|
|
||||||
try {
|
|
||||||
await await _doRequestExpressionData(dispatch, getState, [gene]);
|
|
||||||
const { world } = getState();
|
|
||||||
|
|
||||||
/* then send the success case action through */
|
|
||||||
return dispatch({
|
|
||||||
type: "request user defined gene success",
|
|
||||||
data: {
|
|
||||||
genes: [gene],
|
|
||||||
expression: world.varData.col(gene).asArray()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return dispatch({
|
|
||||||
type: "request user defined gene error",
|
|
||||||
error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const dispatchDiffExpErrors = (dispatch, response) => {
|
|
||||||
switch (response.status) {
|
|
||||||
case 403:
|
|
||||||
dispatchNetworkErrorMessageToUser(
|
|
||||||
"Too many cells selected for differential experesion calculation - please make a smaller selection."
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 501:
|
|
||||||
dispatchNetworkErrorMessageToUser(
|
|
||||||
"Differential expression is not implemented."
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
const msg = `Unexpected differential expression HTTP response ${
|
|
||||||
response.status
|
|
||||||
}, ${response.statusText}`;
|
|
||||||
dispatchNetworkErrorMessageToUser(msg);
|
|
||||||
dispatch({
|
|
||||||
type: "request differential expression error",
|
|
||||||
error: new Error(msg)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|
||||||
dispatch,
|
|
||||||
getState
|
|
||||||
) => {
|
|
||||||
dispatch({ type: "request differential expression started" });
|
|
||||||
try {
|
|
||||||
/*
|
|
||||||
Steps:
|
|
||||||
1. get the most differentially expressed genes
|
|
||||||
2. get expression data for each
|
|
||||||
*/
|
|
||||||
const state = getState();
|
|
||||||
const { universe } = state;
|
|
||||||
const varIndexName = universe.schema.annotations.var.index;
|
|
||||||
|
|
||||||
// Legal values are null, Array or TypedArray. Null is initial state.
|
|
||||||
if (!set1) set1 = [];
|
|
||||||
if (!set2) set2 = [];
|
|
||||||
|
|
||||||
// These lines ensure that we convert any TypedArray to an Array.
|
|
||||||
// This is necessary because JSON.stringify() does some very strange
|
|
||||||
// things with TypedArrays (they are marshalled to JSON objects, rather
|
|
||||||
// than being marshalled as a JSON array).
|
|
||||||
set1 = Array.isArray(set1) ? set1 : Array.from(set1);
|
|
||||||
set2 = Array.isArray(set2) ? set2 : Array.from(set2);
|
|
||||||
|
|
||||||
const res = await fetch(
|
|
||||||
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: new Headers({
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}),
|
|
||||||
body: JSON.stringify({
|
|
||||||
mode: "topN",
|
|
||||||
count: num_genes,
|
|
||||||
set1: { filter: { obs: { index: set1 } } },
|
|
||||||
set2: { filter: { obs: { index: set2 } } }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
|
|
||||||
return dispatchDiffExpErrors(dispatch, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
// result is [ [varIdx, ...], ... ]
|
|
||||||
const topNGenes = _.map(data, r =>
|
|
||||||
universe.varAnnotations.at(r[0], varIndexName)
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Kick off secondary action to fetch all of the expression data for the
|
|
||||||
topN expressed genes.
|
|
||||||
*/
|
|
||||||
await _doRequestExpressionData(dispatch, getState, topNGenes);
|
|
||||||
|
|
||||||
/* then send the success case action through */
|
|
||||||
return dispatch({
|
|
||||||
type: "request differential expression success",
|
|
||||||
data
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return dispatch({
|
|
||||||
type: "request differential expression error",
|
|
||||||
error
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetInterface = () => (dispatch, getState) => {
|
|
||||||
const { universe } = getState();
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "user reset start"
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "clear all user defined genes"
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "clear differential expression"
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "reset colorscale"
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "clear scatterplot"
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "reset World to eq Universe",
|
|
||||||
universe
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "increment graph render counter"
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "user reset end"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
regraph,
|
|
||||||
resetInterface,
|
|
||||||
requestSingleGeneExpressionCountsForColoringPOST,
|
|
||||||
requestDifferentialExpression,
|
|
||||||
requestUserDefinedGene,
|
|
||||||
doInitialDataLoad
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import Helmet from "react-helmet";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
|
|
||||||
import Container from "./framework/container";
|
|
||||||
import LeftSideBar from "./leftSidebar";
|
|
||||||
import Legend from "./continuousLegend";
|
|
||||||
import Graph from "./graph/graph";
|
|
||||||
import MenuBar from "./menubar";
|
|
||||||
|
|
||||||
import actions from "../actions";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
loading: state.controls.loading,
|
|
||||||
error: state.controls.error,
|
|
||||||
graphRenderCounter: state.controls.graphRenderCounter
|
|
||||||
}))
|
|
||||||
class App extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
/* listen for url changes, fire one when we start the app up */
|
|
||||||
window.addEventListener("popstate", this._onURLChanged);
|
|
||||||
this._onURLChanged();
|
|
||||||
|
|
||||||
dispatch(actions.doInitialDataLoad(window.location.search));
|
|
||||||
|
|
||||||
/* listen for resize events */
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
dispatch({
|
|
||||||
type: "window resize",
|
|
||||||
data: {
|
|
||||||
height: window.innerHeight,
|
|
||||||
width: window.innerWidth
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "window resize",
|
|
||||||
data: {
|
|
||||||
height: window.innerHeight,
|
|
||||||
width: window.innerWidth
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onURLChanged() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
dispatch({ type: "url changed", url: document.location.href });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loading, error, graphRenderCounter } = this.props;
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Helmet title="cellxgene" />
|
|
||||||
{loading ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
fontWeight: 500,
|
|
||||||
top: window.innerHeight / 2,
|
|
||||||
left: window.innerWidth / 2 - 50
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
loading cellxgene
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{error ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
fontWeight: 500,
|
|
||||||
top: window.innerHeight / 2,
|
|
||||||
left: window.innerWidth / 2 - 50
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
error loading
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div>
|
|
||||||
{loading ? null : <LeftSideBar />}
|
|
||||||
{loading ? null : <MenuBar />}
|
|
||||||
{loading ? null : <Graph key={graphRenderCounter} />}
|
|
||||||
<Legend />
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,533 +0,0 @@
|
|||||||
/*
|
|
||||||
https://bl.ocks.org/mbostock/4341954
|
|
||||||
https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
|
|
||||||
https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
|
|
||||||
*/
|
|
||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import memoize from "memoize-one";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import actions from "../../actions";
|
|
||||||
import { makeContinuousDimensionName } from "../../util/nameCreators";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
world: state.world,
|
|
||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
|
||||||
continuousSelection: state.continuousSelection,
|
|
||||||
differential: state.differential,
|
|
||||||
colorAccessor: state.colors.colorAccessor
|
|
||||||
}))
|
|
||||||
class HistogramBrush extends React.Component {
|
|
||||||
static getColumn(world, field, clipped = true) {
|
|
||||||
/*
|
|
||||||
Return the underlying Dataframe column for our field. By default,
|
|
||||||
returns the clipped column. If clipped===false, will return the
|
|
||||||
unclipped column.
|
|
||||||
*/
|
|
||||||
const obsAnnotations = clipped
|
|
||||||
? world.obsAnnotations
|
|
||||||
: world.unclipped.obsAnnotations;
|
|
||||||
const varData = clipped ? world.varData : world.unclipped.varData;
|
|
||||||
if (obsAnnotations.hasCol(field)) {
|
|
||||||
return obsAnnotations.col(field);
|
|
||||||
}
|
|
||||||
return varData.col(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
calcHistogramCache = memoize((col, field) => {
|
|
||||||
/*
|
|
||||||
recalculate expensive stuff, notably bins, summaries, etc.
|
|
||||||
*/
|
|
||||||
const histogramCache = {};
|
|
||||||
const values = col.asArray();
|
|
||||||
const summary = col.summarize();
|
|
||||||
const { min: domainMin, max: domainMax } = summary;
|
|
||||||
histogramCache.x = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain([domainMin, domainMax])
|
|
||||||
.range([0, this.width - this.marginRight]);
|
|
||||||
|
|
||||||
histogramCache.bins = d3
|
|
||||||
.histogram()
|
|
||||||
.domain(histogramCache.x.domain())
|
|
||||||
.thresholds(40)(values);
|
|
||||||
|
|
||||||
const yMax = histogramCache.bins
|
|
||||||
.map(b => b.length)
|
|
||||||
.reduce((a, b) => Math.max(a, b));
|
|
||||||
histogramCache.y = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain([0, yMax])
|
|
||||||
.range([this.height - this.marginBottom, 0]);
|
|
||||||
|
|
||||||
return histogramCache;
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.width = 340;
|
|
||||||
this.height = 100;
|
|
||||||
this.marginBottom = 20; // space for X axis & labels
|
|
||||||
this.marginRight = 40; // space for Y axis & labels
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { field } = this.props;
|
|
||||||
const { x, y, bins, svgRef } = this._histogram;
|
|
||||||
|
|
||||||
this.renderAxesBrushBins(x, y, bins, svgRef, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { field, world, continuousSelection } = this.props;
|
|
||||||
const { x, y, bins, svgRef } = this._histogram;
|
|
||||||
let { brushXselection, brushX } = this.state;
|
|
||||||
let forceBrushUpdate = false;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Update our axis if the underlying dataframe column has changed
|
|
||||||
*/
|
|
||||||
const dfColumn = HistogramBrush.getColumn(world, field);
|
|
||||||
const oldDfColumn = HistogramBrush.getColumn(
|
|
||||||
prevProps.world,
|
|
||||||
prevProps.field
|
|
||||||
);
|
|
||||||
if (dfColumn !== oldDfColumn) {
|
|
||||||
({ brushXselection, brushX } = this.renderAxesBrushBins(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
bins,
|
|
||||||
svgRef,
|
|
||||||
field
|
|
||||||
));
|
|
||||||
forceBrushUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
if the selection has changed, ensure that the brush correctly reflects
|
|
||||||
the underlying selection.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
forceBrushUpdate ||
|
|
||||||
continuousSelection !== prevProps.continuousSelection
|
|
||||||
) {
|
|
||||||
const { isObs, isUserDefined, isDiffExp } = this.props;
|
|
||||||
const myName = makeContinuousDimensionName(
|
|
||||||
{ isObs, isUserDefined, isDiffExp },
|
|
||||||
field
|
|
||||||
);
|
|
||||||
const range = continuousSelection[myName];
|
|
||||||
if (brushXselection) {
|
|
||||||
const selection = d3.brushSelection(brushXselection.node());
|
|
||||||
if (!range && selection) {
|
|
||||||
/* no active selection - clear brush */
|
|
||||||
brushXselection.call(brushX.move, null);
|
|
||||||
} else if (range && !selection) {
|
|
||||||
/* there is an active selection, but no brush - set the brush */
|
|
||||||
const x0 = x(range[0]);
|
|
||||||
const x1 = x(range[1]);
|
|
||||||
brushXselection.call(brushX.move, [x0, x1]);
|
|
||||||
} else if (range && selection) {
|
|
||||||
/* there is an active selection and a brush - make sure they match */
|
|
||||||
const moveDeltaThreshold = 1;
|
|
||||||
const x0 = x(range[0]);
|
|
||||||
const x1 = x(range[1]);
|
|
||||||
const dX0 = Math.abs(x0 - selection[0]);
|
|
||||||
const dX1 = Math.abs(x1 - selection[1]);
|
|
||||||
/*
|
|
||||||
only update the brush if it is grossly incorrect,
|
|
||||||
as defined by the moveDeltaThreshold
|
|
||||||
*/
|
|
||||||
if (dX0 > moveDeltaThreshold || dX1 > moveDeltaThreshold) {
|
|
||||||
brushXselection.call(brushX.move, [x0, x1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBrush(selection, x, eventType) {
|
|
||||||
const type = `continuous metadata histogram ${eventType}`;
|
|
||||||
return () => {
|
|
||||||
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
|
|
||||||
|
|
||||||
// ignore programmatically generated events
|
|
||||||
if (!d3.event.sourceEvent) return;
|
|
||||||
// ignore cascading events, which are programmatically generated
|
|
||||||
if (d3.event.sourceEvent.sourceEvent) return;
|
|
||||||
|
|
||||||
if (d3.event.selection) {
|
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
selection: field,
|
|
||||||
continuousNamespace: {
|
|
||||||
isObs,
|
|
||||||
isUserDefined,
|
|
||||||
isDiffExp
|
|
||||||
},
|
|
||||||
range: [x(d3.event.selection[0]), x(d3.event.selection[1])]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type,
|
|
||||||
selection: field,
|
|
||||||
continuousNamespace: {
|
|
||||||
isObs,
|
|
||||||
isUserDefined,
|
|
||||||
isDiffExp
|
|
||||||
},
|
|
||||||
range: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onBrushEnd(selection, x) {
|
|
||||||
return () => {
|
|
||||||
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
|
|
||||||
const minAllowedBrushSize = 10;
|
|
||||||
const smallAmountToAvoidInfiniteLoop = 0.1;
|
|
||||||
|
|
||||||
// ignore programmatically generated events
|
|
||||||
if (!d3.event.sourceEvent) return;
|
|
||||||
// ignore cascading events, which are programmatically generated
|
|
||||||
if (d3.event.sourceEvent.sourceEvent) return;
|
|
||||||
|
|
||||||
if (d3.event.selection) {
|
|
||||||
let _range;
|
|
||||||
|
|
||||||
if (
|
|
||||||
d3.event.selection[1] - d3.event.selection[0] >
|
|
||||||
minAllowedBrushSize
|
|
||||||
) {
|
|
||||||
_range = [x(d3.event.selection[0]), x(d3.event.selection[1])];
|
|
||||||
} else {
|
|
||||||
/* the user selected range is too small and will be hidden #587, so take control of it procedurally */
|
|
||||||
/* https://stackoverflow.com/questions/12354729/d3-js-limit-size-of-brush */
|
|
||||||
|
|
||||||
const procedurallyResizedBrushWidth =
|
|
||||||
d3.event.selection[0] +
|
|
||||||
minAllowedBrushSize +
|
|
||||||
smallAmountToAvoidInfiniteLoop; //
|
|
||||||
|
|
||||||
_range = [x(d3.event.selection[0]), x(procedurallyResizedBrushWidth)];
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "continuous metadata histogram end",
|
|
||||||
selection: field,
|
|
||||||
continuousNamespace: {
|
|
||||||
isObs,
|
|
||||||
isUserDefined,
|
|
||||||
isDiffExp
|
|
||||||
},
|
|
||||||
range: _range
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type: "continuous metadata histogram cancel",
|
|
||||||
selection: field,
|
|
||||||
continuousNamespace: {
|
|
||||||
isObs,
|
|
||||||
isUserDefined,
|
|
||||||
isDiffExp
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
drawHistogram(svgRef) {
|
|
||||||
const { field, world } = this.props;
|
|
||||||
const col = HistogramBrush.getColumn(world, field);
|
|
||||||
const histogramCache = this.calcHistogramCache(col, field);
|
|
||||||
const { x, y, bins } = histogramCache;
|
|
||||||
this._histogram = { x, y, bins, svgRef };
|
|
||||||
}
|
|
||||||
|
|
||||||
handleColorAction() {
|
|
||||||
const { dispatch, field, world, ranges } = this.props;
|
|
||||||
|
|
||||||
if (world.obsAnnotations.hasCol(field)) {
|
|
||||||
dispatch({
|
|
||||||
type: "color by continuous metadata",
|
|
||||||
colorAccessor: field,
|
|
||||||
rangeForColorAccessor: ranges
|
|
||||||
});
|
|
||||||
} else if (world.varData.hasCol(field)) {
|
|
||||||
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeHistogram() {
|
|
||||||
const {
|
|
||||||
dispatch,
|
|
||||||
field,
|
|
||||||
colorAccessor,
|
|
||||||
scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor
|
|
||||||
} = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "clear user defined gene",
|
|
||||||
data: field
|
|
||||||
});
|
|
||||||
if (field === colorAccessor) {
|
|
||||||
dispatch({
|
|
||||||
type: "reset colorscale"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (field === scatterplotXXaccessor) {
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot x",
|
|
||||||
data: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (field === scatterplotYYaccessor) {
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot y",
|
|
||||||
data: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSetGeneAsScatterplotX() {
|
|
||||||
return () => {
|
|
||||||
const { dispatch, field } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot x",
|
|
||||||
data: field
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSetGeneAsScatterplotY() {
|
|
||||||
return () => {
|
|
||||||
const { dispatch, field } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "set scatterplot y",
|
|
||||||
data: field
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAxesBrushBins(x, y, bins, svgRef, field) {
|
|
||||||
const svg = d3.select(svgRef);
|
|
||||||
|
|
||||||
/* Remove everything */
|
|
||||||
svg.selectAll("*").remove();
|
|
||||||
|
|
||||||
/* BINS */
|
|
||||||
svg
|
|
||||||
.insert("g", "*")
|
|
||||||
.attr("fill", "#bbb")
|
|
||||||
.selectAll("rect")
|
|
||||||
.data(bins)
|
|
||||||
.enter()
|
|
||||||
.append("rect")
|
|
||||||
.attr("x", d => x(d.x0) + 1)
|
|
||||||
.attr("y", d => y(d.length))
|
|
||||||
.attr("width", d => Math.abs(x(d.x1) - x(d.x0) - 1))
|
|
||||||
.attr("height", d => y(0) - y(d.length));
|
|
||||||
|
|
||||||
/* BRUSH */
|
|
||||||
const brushX = d3
|
|
||||||
.brushX()
|
|
||||||
.extent([[0, 0], [this.width - this.marginRight, this.height]])
|
|
||||||
/*
|
|
||||||
emit start so that the Undoable history can save an undo point
|
|
||||||
upon drag start, and ignore the subsequent intermediate drag events.
|
|
||||||
*/
|
|
||||||
.on("start", this.onBrush(field, x.invert, "start").bind(this))
|
|
||||||
.on("brush", this.onBrush(field, x.invert, "brush").bind(this))
|
|
||||||
.on("end", this.onBrushEnd(field, x.invert).bind(this));
|
|
||||||
const brushXselection = d3
|
|
||||||
.select(svgRef)
|
|
||||||
.append("g")
|
|
||||||
.attr("class", "brush")
|
|
||||||
.attr("data-testid", `${svgRef.dataset.testid}-brush`)
|
|
||||||
.call(brushX);
|
|
||||||
|
|
||||||
/* X AXIS */
|
|
||||||
svg
|
|
||||||
.append("g")
|
|
||||||
.attr("class", "axis axis--x")
|
|
||||||
.attr("transform", `translate(0,${this.height - this.marginBottom})`)
|
|
||||||
.call(d3.axisBottom(x).ticks(5));
|
|
||||||
|
|
||||||
/* Y AXIS */
|
|
||||||
svg
|
|
||||||
.append("g")
|
|
||||||
.attr("class", "axis axis--y")
|
|
||||||
.attr("transform", `translate(${this.width - this.marginRight},0)`)
|
|
||||||
.call(d3.axisRight(y).ticks(3));
|
|
||||||
|
|
||||||
/* axis style */
|
|
||||||
svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
|
|
||||||
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
|
|
||||||
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
|
|
||||||
|
|
||||||
const newState = { brushX, brushXselection };
|
|
||||||
this.setState(newState);
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
field,
|
|
||||||
world,
|
|
||||||
colorAccessor,
|
|
||||||
isUserDefined,
|
|
||||||
isDiffExp,
|
|
||||||
logFoldChange,
|
|
||||||
pvalAdj,
|
|
||||||
scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor,
|
|
||||||
zebra
|
|
||||||
} = this.props;
|
|
||||||
const fieldForId = field.replace(/\s/g, "_");
|
|
||||||
const {
|
|
||||||
min: unclippedRangeMin,
|
|
||||||
max: unclippedRangeMax
|
|
||||||
} = HistogramBrush.getColumn(world, field, false).summarize();
|
|
||||||
const unclippedRangeMinColor =
|
|
||||||
world.clipQuantiles.min === 0 ? "#bbb" : globals.blue;
|
|
||||||
const unclippedRangeMaxColor =
|
|
||||||
world.clipQuantiles.max === 1 ? "#bbb" : globals.blue;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id={`histogram_${fieldForId}`}
|
|
||||||
data-testid={`histogram-${field}`}
|
|
||||||
data-testclass={
|
|
||||||
isDiffExp
|
|
||||||
? "histogram-diffexp"
|
|
||||||
: isUserDefined
|
|
||||||
? "histogram-user-gene"
|
|
||||||
: "histogram-continuous-metadata"
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
padding: globals.leftSidebarSectionPadding,
|
|
||||||
backgroundColor: zebra ? globals.lightestGrey : "white"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
paddingBottom: "8px"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isDiffExp || isUserDefined ? (
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
style={{ marginRight: 7 }}
|
|
||||||
className="bp3-icon-standard bp3-icon-scatter-plot"
|
|
||||||
/>
|
|
||||||
<ButtonGroup style={{ marginRight: 7 }}>
|
|
||||||
<Button
|
|
||||||
data-testid={`plot-x-${field}`}
|
|
||||||
onClick={this.handleSetGeneAsScatterplotX(field).bind(this)}
|
|
||||||
active={scatterplotXXaccessor === field}
|
|
||||||
intent={scatterplotXXaccessor === field ? "primary" : "none"}
|
|
||||||
>
|
|
||||||
plot x
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
data-testid={`plot-y-${field}`}
|
|
||||||
onClick={this.handleSetGeneAsScatterplotY(field).bind(this)}
|
|
||||||
active={scatterplotYYaccessor === field}
|
|
||||||
intent={scatterplotYYaccessor === field ? "primary" : "none"}
|
|
||||||
>
|
|
||||||
plot y
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{isUserDefined ? (
|
|
||||||
<Button
|
|
||||||
minimal
|
|
||||||
onClick={this.removeHistogram.bind(this)}
|
|
||||||
style={{
|
|
||||||
color: globals.blue,
|
|
||||||
cursor: "pointer",
|
|
||||||
marginLeft: 7
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
remove
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
<Tooltip content="Use as color scale" position="bottom">
|
|
||||||
<Button
|
|
||||||
onClick={this.handleColorAction.bind(this)}
|
|
||||||
active={colorAccessor === field}
|
|
||||||
intent={colorAccessor === field ? "primary" : "none"}
|
|
||||||
data-testclass="colorby"
|
|
||||||
data-testid={`colorby-${field}`}
|
|
||||||
icon="tint"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
width={this.width}
|
|
||||||
height={this.height}
|
|
||||||
id={`histogram_${fieldForId}_svg`}
|
|
||||||
data-testclass="histogram-plot"
|
|
||||||
data-testid={`histogram-${field}-plot`}
|
|
||||||
ref={svgRef => {
|
|
||||||
this.drawHistogram(svgRef);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ color: unclippedRangeMinColor }}>
|
|
||||||
min {unclippedRangeMin.toPrecision(4)}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-testclass="brushable-histogram-field-name"
|
|
||||||
style={{ fontStyle: "italic" }}
|
|
||||||
>
|
|
||||||
{field}
|
|
||||||
</span>
|
|
||||||
<span style={{ color: unclippedRangeMaxColor }}>
|
|
||||||
max {unclippedRangeMax.toPrecision(4)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isDiffExp ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "baseline"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<strong>log fold change:</strong>
|
|
||||||
{` ${logFoldChange.toPrecision(4)}`}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
marginLeft: 7,
|
|
||||||
padding: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>p-value (adj):</strong>
|
|
||||||
{pvalAdj < 0.0001 ? " < 0.0001" : ` ${pvalAdj.toFixed(4)}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HistogramBrush;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import Category from "./category";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
categoricalSelection: state.categoricalSelection
|
|
||||||
}))
|
|
||||||
class Categories extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { categoricalSelection } = this.props;
|
|
||||||
if (!categoricalSelection) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: globals.leftSidebarSectionPadding
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{_.map(categoricalSelection, (catState, catName) => (
|
|
||||||
<Category key={catName} metadataField={catName} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Categories;
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
|
||||||
import { Button, Tooltip } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import Value from "./value";
|
|
||||||
import sortedCategoryValues from "./util";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
categoricalSelection: state.categoricalSelection
|
|
||||||
}))
|
|
||||||
class Category extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isChecked: true,
|
|
||||||
isExpanded: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { categoricalSelection, metadataField } = this.props;
|
|
||||||
if (categoricalSelection !== prevProps.categoricalSelection) {
|
|
||||||
const cat = categoricalSelection[metadataField];
|
|
||||||
const categoryCount = {
|
|
||||||
// total number of categories in this dimension
|
|
||||||
totalCatCount: cat.numCategoryValues,
|
|
||||||
// number of selected options in this category
|
|
||||||
selectedCatCount: _.reduce(
|
|
||||||
cat.categoryValueSelected,
|
|
||||||
(res, cond) => (cond ? res + 1 : res),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if (categoryCount.selectedCatCount === categoryCount.totalCatCount) {
|
|
||||||
/* everything is on, so not indeterminate */
|
|
||||||
this.checkbox.indeterminate = false;
|
|
||||||
this.setState({ isChecked: true }); // eslint-disable-line react/no-did-update-set-state
|
|
||||||
} else if (categoryCount.selectedCatCount === 0) {
|
|
||||||
/* nothing is on, so no */
|
|
||||||
this.checkbox.indeterminate = false;
|
|
||||||
this.setState({ isChecked: false }); // eslint-disable-line react/no-did-update-set-state
|
|
||||||
} else if (categoryCount.selectedCatCount < categoryCount.totalCatCount) {
|
|
||||||
/* to be explicit... */
|
|
||||||
this.checkbox.indeterminate = true;
|
|
||||||
this.setState({ isChecked: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleColorChange = () => {
|
|
||||||
const { dispatch, metadataField } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "color by categorical metadata",
|
|
||||||
colorAccessor: metadataField
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleAll() {
|
|
||||||
const { dispatch, metadataField } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "categorical metadata filter all of these",
|
|
||||||
metadataField
|
|
||||||
});
|
|
||||||
this.setState({ isChecked: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleNone() {
|
|
||||||
const { dispatch, metadataField } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "categorical metadata filter none of these",
|
|
||||||
metadataField
|
|
||||||
});
|
|
||||||
this.setState({ isChecked: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleAllClick() {
|
|
||||||
const { isChecked } = this.state;
|
|
||||||
// || this.checkbox.indeterminate === false
|
|
||||||
if (isChecked) {
|
|
||||||
this.toggleNone();
|
|
||||||
} else if (!isChecked) {
|
|
||||||
this.toggleAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCategoryItems() {
|
|
||||||
const { categoricalSelection, metadataField } = this.props;
|
|
||||||
|
|
||||||
const cat = categoricalSelection[metadataField];
|
|
||||||
const optTuples = sortedCategoryValues([...cat.categoryValueIndices]);
|
|
||||||
return _.map(optTuples, (tuple, i) => (
|
|
||||||
<Value
|
|
||||||
optTuples={optTuples}
|
|
||||||
key={tuple[1]}
|
|
||||||
metadataField={metadataField}
|
|
||||||
categoryIndex={tuple[1]}
|
|
||||||
i={i}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { isExpanded, isChecked } = this.state;
|
|
||||||
const { metadataField, colorAccessor, categoricalSelection } = this.props;
|
|
||||||
const { isTruncated } = categoricalSelection[metadataField];
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
maxWidth: globals.maxControlsWidth
|
|
||||||
}}
|
|
||||||
data-testclass="category"
|
|
||||||
data-testid={`category-${metadataField}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "baseline"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "baseline"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label className="bp3-control bp3-checkbox">
|
|
||||||
<input
|
|
||||||
data-testclass="category-select"
|
|
||||||
data-testid={`category-select-${metadataField}`}
|
|
||||||
onChange={this.handleToggleAllClick.bind(this)}
|
|
||||||
ref={el => {
|
|
||||||
this.checkbox = el;
|
|
||||||
return el;
|
|
||||||
}}
|
|
||||||
checked={isChecked}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span className="bp3-control-indicator" />
|
|
||||||
{""}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<span
|
|
||||||
data-testid={`category-expand-${metadataField}`}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "inline-block"
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
this.setState({ isExpanded: !isExpanded });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{metadataField}
|
|
||||||
{isExpanded ? (
|
|
||||||
<FaChevronDown
|
|
||||||
data-testclass="category-expand-is-expanded"
|
|
||||||
style={{ fontSize: 10, marginLeft: 5 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FaChevronRight
|
|
||||||
data-testclass="category-expand-is-not-expanded"
|
|
||||||
style={{ fontSize: 10, marginLeft: 5 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Tooltip content="Use as color scale" position="bottom">
|
|
||||||
<Button
|
|
||||||
data-testclass="colorby"
|
|
||||||
data-testid={`colorby-${metadataField}`}
|
|
||||||
onClick={this.handleColorChange}
|
|
||||||
active={colorAccessor === metadataField}
|
|
||||||
intent={colorAccessor === metadataField ? "primary" : "none"}
|
|
||||||
icon="tint"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginLeft: 26 }}>
|
|
||||||
{isExpanded ? this.renderCategoryItems() : null}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{isExpanded && isTruncated ? (
|
|
||||||
<p style={{ paddingLeft: 15 }}>... truncated list ...</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Category;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
@connect()
|
|
||||||
class Occupancy extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { occupancy, colorScale, colorAccessor, schema, world } = this.props;
|
|
||||||
const width = 100;
|
|
||||||
const height = 11;
|
|
||||||
|
|
||||||
const categories = schema.annotations.obsByName[colorAccessor]?.categories;
|
|
||||||
|
|
||||||
const x = d3
|
|
||||||
.scaleLinear()
|
|
||||||
/* get all the keys d[1] as an array, then find the sum */
|
|
||||||
.domain([0, d3.sum(Array.from(occupancy, d => d[1]))])
|
|
||||||
.range([0, width]);
|
|
||||||
|
|
||||||
let currentOffset = 0;
|
|
||||||
const dfColumn = world.obsAnnotations.col(colorAccessor);
|
|
||||||
const categoryValues = dfColumn.summarize().categories;
|
|
||||||
const stacks = categoryValues.map(d => {
|
|
||||||
const o = occupancy.get(d);
|
|
||||||
|
|
||||||
const scaledValue = x(o);
|
|
||||||
|
|
||||||
const stackItem = {
|
|
||||||
key: d,
|
|
||||||
value: o || 0,
|
|
||||||
rectWidth: o ? scaledValue : 0,
|
|
||||||
offset: currentOffset,
|
|
||||||
fill: o ? colorScale(categories.indexOf(d)) : "rgb(255,255,255)"
|
|
||||||
};
|
|
||||||
currentOffset += o ? scaledValue : 0;
|
|
||||||
return stackItem;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
style={{
|
|
||||||
marginRight: 5,
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{stacks.map(d => (
|
|
||||||
<rect
|
|
||||||
key={d.key}
|
|
||||||
width={d.rectWidth}
|
|
||||||
height={height}
|
|
||||||
x={d.offset}
|
|
||||||
title={d.metadataField}
|
|
||||||
fill={d.fill}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Occupancy;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
|
|
||||||
// values is [ [optVal, optIdx], ...]
|
|
||||||
// index is range array
|
|
||||||
// return sorted index
|
|
||||||
|
|
||||||
import isNumber from "is-number";
|
|
||||||
|
|
||||||
const sortedCategoryValues = values => {
|
|
||||||
/* this sort could be memoized for perf */
|
|
||||||
|
|
||||||
const strings = [];
|
|
||||||
const ints = [];
|
|
||||||
|
|
||||||
values.forEach(v => {
|
|
||||||
if (isNumber(v[0])) {
|
|
||||||
ints.push(v);
|
|
||||||
} else {
|
|
||||||
strings.push(v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
strings.sort((a, b) => {
|
|
||||||
const textA = String(a[0]).toUpperCase();
|
|
||||||
const textB = String(b[0]).toUpperCase();
|
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
ints.sort((a, b) => +a[0] - +b[0]);
|
|
||||||
|
|
||||||
return ints.concat(strings);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default sortedCategoryValues;
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import React from "react";
|
|
||||||
import Occupancy from "./occupancy";
|
|
||||||
import { countCategoryValues2D } from "../../util/stateManager/worldUtil";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
categoricalSelection: state.categoricalSelection,
|
|
||||||
colorScale: state.colors.scale,
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
schema: state.world?.schema,
|
|
||||||
world: state.world
|
|
||||||
}))
|
|
||||||
class CategoryValue extends React.Component {
|
|
||||||
toggleOff = () => {
|
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "categorical metadata filter deselect",
|
|
||||||
metadataField,
|
|
||||||
categoryIndex
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleOn = () => {
|
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "categorical metadata filter select",
|
|
||||||
metadataField,
|
|
||||||
categoryIndex
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseEnter = () => {
|
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "category value mouse hover start",
|
|
||||||
metadataField,
|
|
||||||
categoryIndex
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseExit = () => {
|
|
||||||
const { dispatch, metadataField, categoryIndex } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "category value mouse hover end",
|
|
||||||
metadataField,
|
|
||||||
categoryIndex
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
categoricalSelection,
|
|
||||||
metadataField,
|
|
||||||
categoryIndex,
|
|
||||||
colorAccessor,
|
|
||||||
colorScale,
|
|
||||||
i,
|
|
||||||
schema,
|
|
||||||
world
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!categoricalSelection) return null;
|
|
||||||
|
|
||||||
const category = categoricalSelection[metadataField];
|
|
||||||
const selected = category.categoryValueSelected[categoryIndex];
|
|
||||||
const count = category.categoryValueCounts[categoryIndex];
|
|
||||||
const value = category.categoryValues[categoryIndex];
|
|
||||||
const displayString = String(
|
|
||||||
category.categoryValues[categoryIndex]
|
|
||||||
).valueOf();
|
|
||||||
|
|
||||||
/* this is the color scale, so add swatches below */
|
|
||||||
const isColorBy = metadataField === colorAccessor;
|
|
||||||
let categories = null;
|
|
||||||
let occupancy = null;
|
|
||||||
|
|
||||||
if (isColorBy && schema) {
|
|
||||||
categories = schema.annotations.obsByName[colorAccessor]?.categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colorAccessor && !isColorBy && categoricalSelection[colorAccessor]) {
|
|
||||||
occupancy = countCategoryValues2D(
|
|
||||||
metadataField,
|
|
||||||
colorAccessor,
|
|
||||||
world.obsAnnotations
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "baseline",
|
|
||||||
justifyContent: "space-between"
|
|
||||||
}}
|
|
||||||
data-testclass="categorical-row"
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseExit}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
userSelect: "none",
|
|
||||||
width: globals.leftSidebarWidth - 130,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label className="bp3-control bp3-checkbox">
|
|
||||||
<input
|
|
||||||
onChange={selected ? this.toggleOff : this.toggleOn}
|
|
||||||
data-testclass="categorical-value-select"
|
|
||||||
data-testid={`categorical-value-select-${metadataField}-${displayString}`}
|
|
||||||
checked={selected}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span className="bp3-control-indicator" />
|
|
||||||
<span
|
|
||||||
data-testid={`categorical-value-${metadataField}-${displayString}`}
|
|
||||||
data-testclass="categorical-value"
|
|
||||||
>
|
|
||||||
{displayString}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<span style={{ flexShrink: 0 }}>
|
|
||||||
{colorAccessor &&
|
|
||||||
!isColorBy &&
|
|
||||||
categoricalSelection[colorAccessor] ? (
|
|
||||||
<Occupancy
|
|
||||||
occupancy={occupancy.get(
|
|
||||||
category.categoryValues[categoryIndex]
|
|
||||||
)}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
<span
|
|
||||||
data-testclass="categorical-value-count"
|
|
||||||
data-testid={`categorical-value-count-${metadataField}-${displayString}`}
|
|
||||||
>
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
style={{
|
|
||||||
marginLeft: 5,
|
|
||||||
width: 11,
|
|
||||||
height: 11,
|
|
||||||
backgroundColor:
|
|
||||||
isColorBy && categories
|
|
||||||
? colorScale(categories.indexOf(value))
|
|
||||||
: "inherit"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CategoryValue;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
/* rc slider https://www.npmjs.com/package/rc-slider */
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
|
|
||||||
import HistogramBrush from "../brushableHistogram";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
obsAnnotations: state.world?.obsAnnotations,
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
colorScale: state.colors.scale,
|
|
||||||
schema: state.world?.schema
|
|
||||||
}))
|
|
||||||
class Continuous extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.hasContinuous = false;
|
|
||||||
this.continuousChecked = false;
|
|
||||||
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {}
|
|
||||||
|
|
||||||
handleColorAction(key) {
|
|
||||||
return () => {
|
|
||||||
const { dispatch, obsAnnotations } = this.props;
|
|
||||||
const summary = obsAnnotations.col(key).summarize();
|
|
||||||
dispatch({
|
|
||||||
type: "color by continuous metadata",
|
|
||||||
colorAccessor: key,
|
|
||||||
rangeForColorAccessor: summary
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { obsAnnotations, schema } = this.props;
|
|
||||||
if (schema && !this.continuousChecked) {
|
|
||||||
this.hasContinuous = _.some(
|
|
||||||
schema.annotations.obs,
|
|
||||||
d => d.type === "int32" || d.type === "float32"
|
|
||||||
);
|
|
||||||
this.continuousChecked = true; /* only do this once */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* initial value for iterator to simulate index, ranges is an object */
|
|
||||||
let zebra = 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{this.hasContinuous ? (
|
|
||||||
<p
|
|
||||||
style={Object.assign({}, globals.leftSidebarSectionHeading, {
|
|
||||||
marginTop: 40,
|
|
||||||
paddingLeft: globals.leftSidebarSectionPadding
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Continuous metadata
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
{obsAnnotations
|
|
||||||
? _.map(obsAnnotations.colIndex.keys(), key => {
|
|
||||||
const isColorField =
|
|
||||||
key.includes("color") || key.includes("Color");
|
|
||||||
if (key === schema.annotations.obs.index || isColorField)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const summary = obsAnnotations.col(key).summarize();
|
|
||||||
const nonFiniteExtent =
|
|
||||||
summary.min === undefined || summary.max === undefined;
|
|
||||||
if (!summary.categorical && !nonFiniteExtent) {
|
|
||||||
zebra += 1;
|
|
||||||
return (
|
|
||||||
<HistogramBrush
|
|
||||||
key={key}
|
|
||||||
field={key}
|
|
||||||
isObs
|
|
||||||
zebra={zebra % 2 === 0}
|
|
||||||
ranges={summary}
|
|
||||||
handleColorAction={this.handleColorAction(key).bind(this)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Continuous;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import styles from "./parallelCoordinates.css";
|
|
||||||
import { yAxis, brushstart } from "./util";
|
|
||||||
|
|
||||||
const drawAxes = (
|
|
||||||
svg,
|
|
||||||
ctx,
|
|
||||||
dimensions,
|
|
||||||
xscale,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
handleBrushAction,
|
|
||||||
handleColorAction
|
|
||||||
) => {
|
|
||||||
/*****************************************
|
|
||||||
******************************************
|
|
||||||
Handles a brush event, toggling the display of foreground lines.
|
|
||||||
******************************************
|
|
||||||
******************************************/
|
|
||||||
|
|
||||||
function brush() {
|
|
||||||
const actives = [];
|
|
||||||
svg
|
|
||||||
.selectAll(".parcoords_axis .parcoords_brush")
|
|
||||||
.filter(() => d3.brushSelection(this))
|
|
||||||
.each(d => {
|
|
||||||
actives.push({
|
|
||||||
dimension: d,
|
|
||||||
extent: d3.brushSelection(this)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
/* fire action, with selected dimensions & their values */
|
|
||||||
handleBrushAction(actives);
|
|
||||||
}
|
|
||||||
|
|
||||||
const axes = svg
|
|
||||||
.selectAll(".parcoords_axis")
|
|
||||||
.data(dimensions)
|
|
||||||
.enter()
|
|
||||||
.append("g")
|
|
||||||
.attr("class", `${styles.axis} parcoords_axis`)
|
|
||||||
.attr("transform", (d, i) => `translate(${xscale(i)})`);
|
|
||||||
|
|
||||||
axes
|
|
||||||
.append("g")
|
|
||||||
.each(d => {
|
|
||||||
const renderAxis =
|
|
||||||
"axis" in d
|
|
||||||
? d.axis.scale(d.scale) // custom axis
|
|
||||||
: yAxis.scale(d.scale); // default axis
|
|
||||||
d3.select(this).call(renderAxis);
|
|
||||||
})
|
|
||||||
.append("text")
|
|
||||||
.on("click", d => {
|
|
||||||
handleColorAction(d.key);
|
|
||||||
})
|
|
||||||
.attr("class", styles.title)
|
|
||||||
.attr("text-anchor", "start")
|
|
||||||
.text(d => ("description" in d ? `${d.description} 🖌️` : `${d.key} 🖌️`));
|
|
||||||
|
|
||||||
// Add and store a brush for each axis.
|
|
||||||
axes
|
|
||||||
.append("g")
|
|
||||||
.attr("class", `${styles.brush} parcoords_brush`)
|
|
||||||
.each(d => {
|
|
||||||
d3.select(this).call(
|
|
||||||
(d.brush = d3
|
|
||||||
.brushY()
|
|
||||||
.extent([[-10, 0], [10, height]])
|
|
||||||
.on("start", brushstart)
|
|
||||||
.on("brush", brush)
|
|
||||||
.on("end", brush))
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.selectAll("rect")
|
|
||||||
.attr("x", -8)
|
|
||||||
.attr("width", 16);
|
|
||||||
|
|
||||||
return axes;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default drawAxes;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
This code can be found: https://bl.ocks.org/syntagmatic/05a5b0897a48890133beb59c815bd953
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-width: 760px;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
:local(.parcoords) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.parcoords) svg,
|
|
||||||
:local(.parcoords) canvas {
|
|
||||||
font: 10px sans-serif;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.parcoords) canvas {
|
|
||||||
opacity: 0.9;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.axis .title) {
|
|
||||||
font-size: 10px;
|
|
||||||
transform: rotate(-21deg) translate(-5px, -6px);
|
|
||||||
fill: #222;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -webkit-filter: grayscale(100%);
|
|
||||||
filter: grayscale(100%); */
|
|
||||||
|
|
||||||
:local(.axis) line,
|
|
||||||
:local(.axis) path {
|
|
||||||
fill: none;
|
|
||||||
stroke: #ccc;
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.axis .tick) text {
|
|
||||||
fill: #222;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
old, from reference bl.ocks
|
|
||||||
.axis.manufac_name .tick text,
|
|
||||||
.axis.food_group .tick text {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
:local(.axis:hover) line,
|
|
||||||
:local(.axis:hover) path,
|
|
||||||
:local(.axis.active) line,
|
|
||||||
:local(.axis.active) path {
|
|
||||||
fill: none;
|
|
||||||
stroke: #222;
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.axis:hover .title) {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.axis:hover .tick) text {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.axis.active .title) {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.axis.active .tick) text {
|
|
||||||
opacity: 1;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.brush .extent) {
|
|
||||||
fill-opacity: 0.3;
|
|
||||||
stroke: #fff;
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.pre) {
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
margin: 6px 12px;
|
|
||||||
tab-size: 40;
|
|
||||||
font-size: 10px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*****************************************
|
|
||||||
******************************************
|
|
||||||
Setup SVG & Canvas elements
|
|
||||||
******************************************
|
|
||||||
******************************************/
|
|
||||||
// jshint esversion: 6
|
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
const setupParallelCoordinates = (width, height, margin) => {
|
|
||||||
const container = d3.select("#parcoords");
|
|
||||||
|
|
||||||
const svg = container
|
|
||||||
.append("svg")
|
|
||||||
.attr("width", width + margin.left + margin.right)
|
|
||||||
.attr("height", height + margin.top + margin.bottom)
|
|
||||||
.append("g")
|
|
||||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
||||||
|
|
||||||
const canvas = container
|
|
||||||
.append("canvas")
|
|
||||||
.attr("width", width * devicePixelRatio)
|
|
||||||
.attr("height", height * devicePixelRatio)
|
|
||||||
.style("width", `${width}px`)
|
|
||||||
.style("height", `${height}px`)
|
|
||||||
.style("margin-top", `${margin.top}px`)
|
|
||||||
.style("margin-left", `${margin.left}px`);
|
|
||||||
|
|
||||||
const ctx = canvas.node().getContext("2d");
|
|
||||||
ctx.globalCompositeOperation = "darken";
|
|
||||||
ctx.globalAlpha = 0.15;
|
|
||||||
ctx.lineWidth = 1.5;
|
|
||||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
|
||||||
|
|
||||||
return {
|
|
||||||
svg,
|
|
||||||
ctx
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default setupParallelCoordinates;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import _ from "lodash";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
const paddingRight = 120;
|
|
||||||
const continuousChartWidth = 1200;
|
|
||||||
|
|
||||||
export const margin = { top: 66, right: 110, bottom: 20, left: 60 };
|
|
||||||
export const width =
|
|
||||||
continuousChartWidth - margin.left - margin.right - paddingRight;
|
|
||||||
export const height = 340 - margin.top - margin.bottom;
|
|
||||||
export const innerHeight = height - 2;
|
|
||||||
|
|
||||||
export const devicePixelRatio = window.devicePixelRatio || 1;
|
|
||||||
|
|
||||||
export const createDimensions = data => {
|
|
||||||
const newArr = [];
|
|
||||||
_.each(data, (value, key) => {
|
|
||||||
if (value.range) {
|
|
||||||
newArr.push({
|
|
||||||
key /* room for confusion: lodash calls this key, it's also the name of the property parallel coords code is looking for */,
|
|
||||||
type: {
|
|
||||||
within: (d, extent, dim) =>
|
|
||||||
extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1]
|
|
||||||
},
|
|
||||||
scale: d3
|
|
||||||
.scaleSqrt()
|
|
||||||
.range([innerHeight, 0])
|
|
||||||
.domain([0, value.range.max])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return newArr;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const yAxis = d3.axisLeft();
|
|
||||||
|
|
||||||
export const brushstart = () => {
|
|
||||||
d3.event.sourceEvent.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Unused.
|
|
||||||
// export const d3_functor = v => (typeof v === "function" ? v : () => v);
|
|
||||||
export const project = (d, dimensions, xscale) =>
|
|
||||||
dimensions.map((p, i) => {
|
|
||||||
// check if data element has property and contains a value
|
|
||||||
if (!(p.key in d) || d[p.key] === null) return null;
|
|
||||||
|
|
||||||
return [xscale(i), p.scale(d[p.key])];
|
|
||||||
});
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import { interpolateCool } from "d3-scale-chromatic";
|
|
||||||
|
|
||||||
// create continuous color legend
|
|
||||||
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
|
|
||||||
const continuous = (selectorId, colorscale, colorAccessor) => {
|
|
||||||
const legendheight = 200;
|
|
||||||
const legendwidth = 80;
|
|
||||||
const margin = { top: 10, right: 60, bottom: 10, left: 2 };
|
|
||||||
|
|
||||||
const canvas = d3
|
|
||||||
.select(selectorId)
|
|
||||||
.style("height", `${legendheight}px`)
|
|
||||||
.style("width", `${legendwidth}px`)
|
|
||||||
.append("canvas")
|
|
||||||
.attr("height", legendheight - margin.top - margin.bottom)
|
|
||||||
.attr("width", 1)
|
|
||||||
.style("height", `${legendheight - margin.top - margin.bottom}px`)
|
|
||||||
.style("width", `${legendwidth - margin.left - margin.right}px`)
|
|
||||||
.style("position", "absolute")
|
|
||||||
.style("top", `${margin.top + 1}px`)
|
|
||||||
.style("left", `${margin.left + 1}px`)
|
|
||||||
.style(
|
|
||||||
"transform",
|
|
||||||
"scale(1,-1)"
|
|
||||||
) /* flip it! dark is high value light is low.
|
|
||||||
we flip the color scale as well [1, 0] instead of [0, 1] */
|
|
||||||
.node();
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
|
|
||||||
const legendscale = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.range([1, legendheight - margin.top - margin.bottom])
|
|
||||||
.domain([
|
|
||||||
colorscale.domain()[1],
|
|
||||||
colorscale.domain()[0]
|
|
||||||
]); /* we flip this to make viridis colors dark if high in the color scale */
|
|
||||||
|
|
||||||
// image data hackery based on http://bl.ocks.org/mbostock/048d21cf747371b11884f75ad896e5a5
|
|
||||||
const image = ctx.createImageData(1, legendheight);
|
|
||||||
d3.range(legendheight).forEach(i => {
|
|
||||||
const c = d3.rgb(colorscale(legendscale.invert(i)));
|
|
||||||
image.data[4 * i] = c.r;
|
|
||||||
image.data[4 * i + 1] = c.g;
|
|
||||||
image.data[4 * i + 2] = c.b;
|
|
||||||
image.data[4 * i + 3] = 255;
|
|
||||||
});
|
|
||||||
ctx.putImageData(image, 0, 0);
|
|
||||||
|
|
||||||
// A simpler way to do the above, but possibly slower. keep in mind the legend
|
|
||||||
// width is stretched because the width attr of the canvas is 1
|
|
||||||
// See http://stackoverflow.com/questions/4899799/whats-the-best-way-to-set-a-single-pixel-in-an-html5-canvas
|
|
||||||
/*
|
|
||||||
d3.range(legendheight).forEach(function(i) {
|
|
||||||
ctx.fillStyle = colorscale(legendscale.invert(i));
|
|
||||||
ctx.fillRect(0,i,1,1);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
const legendaxis = d3
|
|
||||||
.axisRight()
|
|
||||||
.scale(legendscale)
|
|
||||||
.tickSize(6)
|
|
||||||
.ticks(8);
|
|
||||||
|
|
||||||
const svg = d3
|
|
||||||
.select(selectorId)
|
|
||||||
.append("svg")
|
|
||||||
.attr("height", `${legendheight}px`)
|
|
||||||
.attr("width", `${legendwidth}px`)
|
|
||||||
.style("position", "absolute")
|
|
||||||
.style("left", "0px")
|
|
||||||
.style("top", "0px");
|
|
||||||
|
|
||||||
svg
|
|
||||||
.append("g")
|
|
||||||
.attr("class", "axis")
|
|
||||||
.attr(
|
|
||||||
"transform",
|
|
||||||
`translate(${legendwidth - margin.left - margin.right + 3},${margin.top})`
|
|
||||||
)
|
|
||||||
.call(legendaxis);
|
|
||||||
|
|
||||||
// text label for the y axis
|
|
||||||
svg
|
|
||||||
.append("text")
|
|
||||||
.attr("transform", "rotate(-90)")
|
|
||||||
.attr("y", 2)
|
|
||||||
.attr("x", 0 - legendheight / 2)
|
|
||||||
.attr("dy", "1em")
|
|
||||||
.style("text-anchor", "middle")
|
|
||||||
.style("fill", "white")
|
|
||||||
.text(colorAccessor);
|
|
||||||
};
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
colorScale: state.colors.scale,
|
|
||||||
responsive: state.responsive
|
|
||||||
}))
|
|
||||||
class ContinuousLegend extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { colorAccessor, responsive, colorScale } = this.props;
|
|
||||||
if (
|
|
||||||
prevProps.colorAccessor !== colorAccessor ||
|
|
||||||
prevProps.colorScale !== colorScale ||
|
|
||||||
prevProps.responsive.height !== responsive.height ||
|
|
||||||
prevProps.responsive.width !== responsive.width
|
|
||||||
) {
|
|
||||||
/* always remove it, if it's not continuous we don't put it back. */
|
|
||||||
d3.select("#continuous_legend")
|
|
||||||
.selectAll("*")
|
|
||||||
.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colorAccessor && colorScale && colorScale.range) {
|
|
||||||
/* fragile! continuous range is 0 to 1, not [#fa4b2c, ...], make this a flag? */
|
|
||||||
if (colorScale.range()[0][0] !== "#") {
|
|
||||||
continuous(
|
|
||||||
"#continuous_legend",
|
|
||||||
d3.scaleSequential(interpolateCool).domain(colorScale.domain()),
|
|
||||||
colorAccessor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { colorAccessor, responsive } = this.props;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="continuous_legend"
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
display: colorAccessor ? "inherit" : "none",
|
|
||||||
right: 0,
|
|
||||||
top: responsive.height / 2
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContinuousLegend;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
:local(.btn) {
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: "#FFF";
|
|
||||||
border: "none";
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
:local(.container) {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import styles from "./container.css";
|
|
||||||
|
|
||||||
const Container = props => {
|
|
||||||
const { children } = props;
|
|
||||||
return <div className={styles.container}>{children}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Container;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
|
|
||||||
const Logo = props => {
|
|
||||||
const { size } = props;
|
|
||||||
return (
|
|
||||||
<svg width={size} height={size} viewBox="0 0 48 48" fill="none">
|
|
||||||
<rect width="48" height="48" fill="white" />
|
|
||||||
<rect width="48" height="48" fill={globals.logoColor} />
|
|
||||||
<rect x="19" y="19" width="22" height="22" fill="white" />
|
|
||||||
<rect x="24" y="24" width="12" height="12" fill={globals.logoColor} />
|
|
||||||
<rect x="7" y="19" width="7" height="22" fill="white" />
|
|
||||||
<rect x="19" y="7" width="22" height="7" fill="white" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Logo;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Position, Toaster, Intent } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
/** Singleton toaster instance. Create separate instances for different options. */
|
|
||||||
|
|
||||||
const ErrorToastTopCenter = Toaster.create({
|
|
||||||
className: "recipe-toaster",
|
|
||||||
position: Position.TOP
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
A "user" error - eg, bad input
|
|
||||||
*/
|
|
||||||
export const postUserErrorToast = message =>
|
|
||||||
ErrorToastTopCenter.show({ message, intent: Intent.WARNING });
|
|
||||||
|
|
||||||
/*
|
|
||||||
A toast the user must dismiss manually, because they need to act on its information,
|
|
||||||
ie., 8 bulk add genes out of 40 were bad. Manually see which ones and fix.
|
|
||||||
*/
|
|
||||||
export const keepAroundErrorToast = message =>
|
|
||||||
ErrorToastTopCenter.show({ message, timeout: 0, intent: Intent.WARNING });
|
|
||||||
|
|
||||||
/*
|
|
||||||
a hard network error
|
|
||||||
*/
|
|
||||||
export const postNetworkErrorToast = message =>
|
|
||||||
ErrorToastTopCenter.show({
|
|
||||||
message,
|
|
||||||
timeout: 30000,
|
|
||||||
intent: Intent.DANGER
|
|
||||||
});
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
/* rc slider https://www.npmjs.com/package/rc-slider */
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import _ from "lodash";
|
|
||||||
import fuzzysort from "fuzzysort";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import {
|
|
||||||
MenuItem,
|
|
||||||
Button,
|
|
||||||
FormGroup,
|
|
||||||
InputGroup,
|
|
||||||
ControlGroup
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
import { Suggest } from "@blueprintjs/select";
|
|
||||||
import HistogramBrush from "../brushableHistogram";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import actions from "../../actions";
|
|
||||||
import {
|
|
||||||
postUserErrorToast,
|
|
||||||
keepAroundErrorToast
|
|
||||||
} from "../framework/toasters";
|
|
||||||
|
|
||||||
const renderGene = (fuzzySortResult, { handleClick, modifiers, query }) => {
|
|
||||||
if (!modifiers.matchesPredicate) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
/* the fuzzysort wraps the object with other properties, like a score */
|
|
||||||
const geneName = fuzzySortResult.target;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
active={modifiers.active}
|
|
||||||
disabled={modifiers.disabled}
|
|
||||||
data-testid={`suggest-menu-item-${geneName}`}
|
|
||||||
// Use of annotations in this way is incorrect and dataset specific.
|
|
||||||
// See https://github.com/chanzuckerberg/cellxgene/issues/483
|
|
||||||
// label={gene.n_counts}
|
|
||||||
key={geneName}
|
|
||||||
onClick={g =>
|
|
||||||
/* this fires when user clicks a menu item */
|
|
||||||
handleClick(g)
|
|
||||||
}
|
|
||||||
text={geneName}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterGenes = (query, genes) =>
|
|
||||||
/* fires on load, once, and then for each character typed into the input */
|
|
||||||
fuzzysort.go(query, genes, {
|
|
||||||
limit: 5,
|
|
||||||
threshold: -10000 // don't return bad results
|
|
||||||
});
|
|
||||||
|
|
||||||
@connect(state => {
|
|
||||||
return {
|
|
||||||
obsAnnotations: state.world?.obsAnnotations,
|
|
||||||
userDefinedGenes: state.controls.userDefinedGenes,
|
|
||||||
userDefinedGenesLoading: state.controls.userDefinedGenesLoading,
|
|
||||||
world: state.world,
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
differential: state.differential
|
|
||||||
};
|
|
||||||
})
|
|
||||||
class GeneExpression extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
bulkAdd: "",
|
|
||||||
tab: "autosuggest"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
placeholderGeneNames() {
|
|
||||||
/*
|
|
||||||
return a string containing gene name suggestions for use as a user hint.
|
|
||||||
Eg., Apod, Cd74, ...
|
|
||||||
Will return a max of 3 genes, totalling 15 characters in length.
|
|
||||||
Randomly selects gene names.
|
|
||||||
|
|
||||||
NOTE: the random selection means it will re-render constantly.
|
|
||||||
*/
|
|
||||||
const { world } = this.props;
|
|
||||||
const { varAnnotations } = world;
|
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
|
||||||
const geneNames = varAnnotations.col(varIndexName).asArray();
|
|
||||||
if (geneNames.length > 0) {
|
|
||||||
const placeholder = [];
|
|
||||||
let len = geneNames.length;
|
|
||||||
const maxGeneNameCount = 3;
|
|
||||||
const maxStrLength = 15;
|
|
||||||
len = len < maxGeneNameCount ? len : maxGeneNameCount;
|
|
||||||
for (let i = 0, strLen = 0; i < len && strLen < maxStrLength; i += 1) {
|
|
||||||
const deal = Math.floor(Math.random() * geneNames.length);
|
|
||||||
const geneName = geneNames[deal];
|
|
||||||
placeholder.push(geneName);
|
|
||||||
strLen += geneName.length + 2; // '2' is the length of a comma and space
|
|
||||||
}
|
|
||||||
placeholder.push("...");
|
|
||||||
return placeholder.join(", ");
|
|
||||||
}
|
|
||||||
// default - should never happen.
|
|
||||||
return "Apod, Cd74, ...";
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(g) {
|
|
||||||
const { world, dispatch, userDefinedGenes } = this.props;
|
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
|
||||||
const gene = g.target;
|
|
||||||
if (userDefinedGenes.indexOf(gene) !== -1) {
|
|
||||||
postUserErrorToast("That gene already exists");
|
|
||||||
} else if (userDefinedGenes.length > 15) {
|
|
||||||
postUserErrorToast(
|
|
||||||
"That's too many genes, you can have at most 15 user defined genes"
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
world.varAnnotations.col(varIndexName).indexOf(gene) === undefined
|
|
||||||
) {
|
|
||||||
postUserErrorToast("That doesn't appear to be a valid gene name.");
|
|
||||||
} else {
|
|
||||||
dispatch({ type: "single user defined gene start" });
|
|
||||||
dispatch(actions.requestUserDefinedGene(gene)).then(
|
|
||||||
() => dispatch({ type: "single user defined gene complete" }),
|
|
||||||
() => dispatch({ type: "single user defined gene error" })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBulkAddClick() {
|
|
||||||
const { world, dispatch, userDefinedGenes } = this.props;
|
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
|
||||||
const { bulkAdd } = this.state;
|
|
||||||
|
|
||||||
/*
|
|
||||||
test:
|
|
||||||
Apod,,, Cd74,, ,,, Foo, Bar-2,,
|
|
||||||
*/
|
|
||||||
if (bulkAdd !== "") {
|
|
||||||
const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), "");
|
|
||||||
|
|
||||||
dispatch({ type: "bulk user defined gene start" });
|
|
||||||
Promise.all(
|
|
||||||
genes.map(gene => {
|
|
||||||
if (gene.length === 0) {
|
|
||||||
return keepAroundErrorToast("Must enter a gene name.");
|
|
||||||
}
|
|
||||||
if (userDefinedGenes.indexOf(gene) !== -1) {
|
|
||||||
return keepAroundErrorToast("That gene already exists");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
world.varAnnotations.col(varIndexName).indexOf(gene) === undefined
|
|
||||||
) {
|
|
||||||
return keepAroundErrorToast(
|
|
||||||
`${gene} doesn't appear to be a valid gene name.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return dispatch(actions.requestUserDefinedGene(gene));
|
|
||||||
})
|
|
||||||
).then(
|
|
||||||
() => dispatch({ type: "bulk user defined gene complete" }),
|
|
||||||
() => dispatch({ type: "bulk user defined gene error" })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ bulkAdd: "" });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
world,
|
|
||||||
userDefinedGenes,
|
|
||||||
userDefinedGenesLoading,
|
|
||||||
differential
|
|
||||||
} = this.props;
|
|
||||||
const varIndexName = world?.schema?.annotations?.var?.index;
|
|
||||||
const { tab, bulkAdd } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: globals.leftSidebarSectionPadding
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
active={tab === "autosuggest"}
|
|
||||||
style={{ marginRight: 5 }}
|
|
||||||
minimal
|
|
||||||
small
|
|
||||||
data-testid="tab-autosuggest"
|
|
||||||
onClick={() => {
|
|
||||||
this.setState({ tab: "autosuggest" });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Autosuggest genes
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
active={tab === "bulkadd"}
|
|
||||||
minimal
|
|
||||||
small
|
|
||||||
data-testid="section-bulk-add"
|
|
||||||
onClick={() => {
|
|
||||||
this.setState({ tab: "bulkadd" });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Bulk add genes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{tab === "autosuggest" ? (
|
|
||||||
<ControlGroup
|
|
||||||
style={{
|
|
||||||
paddingLeft: globals.leftSidebarSectionPadding,
|
|
||||||
paddingBottom: globals.leftSidebarSectionPadding
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Suggest
|
|
||||||
resetOnSelect
|
|
||||||
closeOnSelect
|
|
||||||
resetOnClose
|
|
||||||
itemDisabled={
|
|
||||||
userDefinedGenesLoading ? () => true : () => false
|
|
||||||
}
|
|
||||||
noResults={<MenuItem disabled text="No matching genes." />}
|
|
||||||
onItemSelect={g => {
|
|
||||||
/* this happens on 'enter' */
|
|
||||||
this.handleClick(g);
|
|
||||||
}}
|
|
||||||
inputProps={{ "data-testid": "gene-search" }}
|
|
||||||
inputValueRenderer={g => {
|
|
||||||
return "";
|
|
||||||
}}
|
|
||||||
itemListPredicate={filterGenes}
|
|
||||||
itemRenderer={renderGene.bind(this)}
|
|
||||||
items={
|
|
||||||
world && world.varAnnotations
|
|
||||||
? world.varAnnotations.col(varIndexName).asArray()
|
|
||||||
: ["No genes"]
|
|
||||||
}
|
|
||||||
popoverProps={{ minimal: true }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className="bp3-button bp3-intent-primary"
|
|
||||||
data-testid={"add-gene"}
|
|
||||||
loading={userDefinedGenesLoading}
|
|
||||||
>
|
|
||||||
Add gene
|
|
||||||
</Button>
|
|
||||||
</ControlGroup>
|
|
||||||
) : null}
|
|
||||||
{tab === "bulkadd" ? (
|
|
||||||
<div style={{ paddingLeft: globals.leftSidebarSectionPadding }}>
|
|
||||||
<form
|
|
||||||
onSubmit={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.handleBulkAddClick();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormGroup
|
|
||||||
helperText="Add a list of genes (comma delimited)"
|
|
||||||
labelFor="text-input-bulk-add"
|
|
||||||
>
|
|
||||||
<ControlGroup>
|
|
||||||
<InputGroup
|
|
||||||
onChange={e => {
|
|
||||||
this.setState({ bulkAdd: e.target.value });
|
|
||||||
}}
|
|
||||||
id="text-input-bulk-add"
|
|
||||||
data-testid="input-bulk-add"
|
|
||||||
placeholder={this.placeholderGeneNames()}
|
|
||||||
value={bulkAdd}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
intent="primary"
|
|
||||||
onClick={this.handleBulkAddClick.bind(this)}
|
|
||||||
loading={userDefinedGenesLoading}
|
|
||||||
>
|
|
||||||
Add genes
|
|
||||||
</Button>
|
|
||||||
</ControlGroup>
|
|
||||||
</FormGroup>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{world && userDefinedGenes.length > 0
|
|
||||||
? _.map(userDefinedGenes, (geneName, index) => {
|
|
||||||
const values = world.varData.col(geneName);
|
|
||||||
if (!values) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const summary = values.summarize();
|
|
||||||
return (
|
|
||||||
<HistogramBrush
|
|
||||||
key={geneName}
|
|
||||||
field={geneName}
|
|
||||||
zebra={index % 2 === 0}
|
|
||||||
ranges={summary}
|
|
||||||
isUserDefined
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{differential.diffExp
|
|
||||||
? _.map(differential.diffExp, (value, index) => {
|
|
||||||
const name = world.varAnnotations.at(value[0], varIndexName);
|
|
||||||
const values = world.varData.col(name);
|
|
||||||
if (!values) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const summary = values.summarize();
|
|
||||||
return (
|
|
||||||
<HistogramBrush
|
|
||||||
key={name}
|
|
||||||
field={name}
|
|
||||||
zebra={index % 2 === 0}
|
|
||||||
ranges={summary}
|
|
||||||
isDiffExp
|
|
||||||
logFoldChange={value[1]}
|
|
||||||
pval={value[2]}
|
|
||||||
pvalAdj={value[3]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GeneExpression;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
const mat4 = require("gl-mat4");
|
|
||||||
const vec3 = require("gl-vec3");
|
|
||||||
|
|
||||||
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
|
||||||
|
|
||||||
export default function(regl) {
|
|
||||||
return regl({
|
|
||||||
vert: `
|
|
||||||
precision mediump float;
|
|
||||||
attribute vec2 position;
|
|
||||||
attribute vec3 color;
|
|
||||||
attribute float size;
|
|
||||||
uniform float distance;
|
|
||||||
uniform mat4 projection, view;
|
|
||||||
varying vec3 fragColor;
|
|
||||||
void main() {
|
|
||||||
gl_PointSize = 7.0 / pow(distance, 2.5) + size;
|
|
||||||
gl_Position = projection * view * vec4(position.x, -position.y, 0, 1);
|
|
||||||
fragColor = color;
|
|
||||||
}`,
|
|
||||||
|
|
||||||
frag: `
|
|
||||||
precision mediump float;
|
|
||||||
varying vec3 fragColor;
|
|
||||||
void main() {
|
|
||||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
gl_FragColor = vec4(fragColor, 1);
|
|
||||||
}`,
|
|
||||||
|
|
||||||
attributes: {
|
|
||||||
position: regl.prop("position"),
|
|
||||||
color: regl.prop("color"),
|
|
||||||
size: regl.prop("size")
|
|
||||||
},
|
|
||||||
|
|
||||||
uniforms: {
|
|
||||||
distance: regl.prop("distance"),
|
|
||||||
view: regl.prop("view"),
|
|
||||||
projection: ({ viewportWidth, viewportHeight }) => {
|
|
||||||
const aspectRatio = viewportWidth / viewportHeight;
|
|
||||||
let m = mat4.perspective(
|
|
||||||
[],
|
|
||||||
Math.PI / 2,
|
|
||||||
viewportWidth / viewportHeight,
|
|
||||||
0.01,
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
if (aspectRatio < 1) {
|
|
||||||
m = mat4.scale(m, m, vec3.fromValues(1, 1, 1 / aspectRatio));
|
|
||||||
}
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
count: regl.prop("count"),
|
|
||||||
|
|
||||||
primitive: "points"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
:local(.graphCanvas) path {
|
|
||||||
shape-rendering: crispEdges;
|
|
||||||
}
|
|
||||||
|
|
||||||
#graphWrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#graphAttachPoint,
|
|
||||||
.graphSVG,
|
|
||||||
.graphCanvas {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
#graphSVG {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.axis) path,
|
|
||||||
:local(.axis) line {
|
|
||||||
fill: none;
|
|
||||||
stroke: #000;
|
|
||||||
stroke-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
circle {
|
|
||||||
stroke-width: 4px;
|
|
||||||
stroke: #000;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.hidden) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -1,656 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import mat4 from "gl-mat4";
|
|
||||||
import _regl from "regl";
|
|
||||||
import memoize from "memoize-one";
|
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import setupSVGandBrushElements from "./setupSVGandBrush";
|
|
||||||
import setupCentroidSVG from "./setupCentroidSVG";
|
|
||||||
import actions from "../../actions";
|
|
||||||
import _camera from "../../util/camera";
|
|
||||||
import _drawPoints from "./drawPointsRegl";
|
|
||||||
import scaleLinear from "../../util/scaleLinear";
|
|
||||||
|
|
||||||
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
world: state.world,
|
|
||||||
crossfilter: state.crossfilter,
|
|
||||||
responsive: state.responsive,
|
|
||||||
colorRGB: state.colors.rgb,
|
|
||||||
selectionTool: state.graphSelection.tool,
|
|
||||||
currentSelection: state.graphSelection.selection,
|
|
||||||
layoutChoice: state.layoutChoice,
|
|
||||||
centroidLabel: state.centroidLabel,
|
|
||||||
graphInteractionMode: state.controls.graphInteractionMode,
|
|
||||||
colorAccessor: state.colors.colorAccessor
|
|
||||||
}))
|
|
||||||
class Graph extends React.Component {
|
|
||||||
computePointPositions = memoize((X, Y, scaleX, scaleY) => {
|
|
||||||
/*
|
|
||||||
compute webgl coordinate buffer for each point
|
|
||||||
*/
|
|
||||||
const positions = new Float32Array(2 * X.length);
|
|
||||||
for (let i = 0, len = X.length; i < len; i += 1) {
|
|
||||||
positions[2 * i] = scaleX(X[i]);
|
|
||||||
positions[2 * i + 1] = scaleY(Y[i]);
|
|
||||||
}
|
|
||||||
return positions;
|
|
||||||
});
|
|
||||||
|
|
||||||
computePointColors = memoize(rgb => {
|
|
||||||
/*
|
|
||||||
compute webgl colors for each point
|
|
||||||
*/
|
|
||||||
const colors = new Float32Array(3 * rgb.length);
|
|
||||||
for (let i = 0, len = rgb.length; i < len; i += 1) {
|
|
||||||
colors.set(rgb[i], 3 * i);
|
|
||||||
}
|
|
||||||
return colors;
|
|
||||||
});
|
|
||||||
|
|
||||||
computePointSizes = memoize((len, crossfilter) => {
|
|
||||||
/*
|
|
||||||
compute webgl dot size for each point
|
|
||||||
*/
|
|
||||||
const sizes = new Float32Array(len);
|
|
||||||
crossfilter.fillByIsSelected(sizes, 4, 0.2);
|
|
||||||
return sizes;
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.count = 0;
|
|
||||||
this.graphPaddingTop = 0;
|
|
||||||
this.graphPaddingRight = globals.leftSidebarWidth;
|
|
||||||
this.renderCache = {
|
|
||||||
X: null,
|
|
||||||
Y: null,
|
|
||||||
positions: null,
|
|
||||||
colors: null,
|
|
||||||
sizes: null
|
|
||||||
};
|
|
||||||
this.state = {
|
|
||||||
toolSVG: null,
|
|
||||||
centroidSVG: null,
|
|
||||||
tool: null,
|
|
||||||
container: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// setup canvas and camera
|
|
||||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
|
||||||
const regl = _regl(this.reglCanvas);
|
|
||||||
|
|
||||||
const drawPoints = _drawPoints(regl);
|
|
||||||
|
|
||||||
// preallocate buffers
|
|
||||||
const pointBuffer = regl.buffer();
|
|
||||||
const colorBuffer = regl.buffer();
|
|
||||||
const sizeBuffer = regl.buffer();
|
|
||||||
|
|
||||||
// preallocate coordinate system transformation between data and gl
|
|
||||||
const fractionToUse = 0.93; // fraction of dimension to use
|
|
||||||
const shiftForMenuBar = 0.05;
|
|
||||||
const transform = {
|
|
||||||
glScaleX: scaleLinear([0, 1], [-1 * fractionToUse, 1 * fractionToUse]),
|
|
||||||
glScaleY: scaleLinear(
|
|
||||||
[0, 1],
|
|
||||||
[
|
|
||||||
(1 + shiftForMenuBar) * fractionToUse,
|
|
||||||
(-1 + shiftForMenuBar) * fractionToUse
|
|
||||||
]
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
/* first time, but this duplicates above function, should be possile to avoid this */
|
|
||||||
const reglRender = regl.frame(() => {
|
|
||||||
this.reglDraw(
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
);
|
|
||||||
camera.tick();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
pointBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
sizeBuffer,
|
|
||||||
camera,
|
|
||||||
reglRender,
|
|
||||||
transform
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { renderCache } = this;
|
|
||||||
const {
|
|
||||||
world,
|
|
||||||
crossfilter,
|
|
||||||
colorRGB,
|
|
||||||
responsive,
|
|
||||||
selectionTool,
|
|
||||||
currentSelection,
|
|
||||||
layoutChoice,
|
|
||||||
graphInteractionMode,
|
|
||||||
colorAccessor,
|
|
||||||
centroidLabel
|
|
||||||
} = this.props;
|
|
||||||
const { reglRender, mode, regl, toolSVG, centroidSVG } = this.state;
|
|
||||||
let stateChanges = {};
|
|
||||||
|
|
||||||
if (reglRender) {
|
|
||||||
if (
|
|
||||||
// If it IS RENDERING and it is NOT IN ZOOM mode, stop rendering.
|
|
||||||
this.reglRenderState === "rendering" &&
|
|
||||||
graphInteractionMode !== "zoom"
|
|
||||||
) {
|
|
||||||
reglRender.cancel();
|
|
||||||
this.reglRenderState = "paused";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
// If it is NOT RENDERING and it IS IN ZOOM mode, start rendering
|
|
||||||
this.reglRenderState !== "rendering" &&
|
|
||||||
graphInteractionMode === "zoom"
|
|
||||||
) {
|
|
||||||
this.restartReglLoop();
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regl && world) {
|
|
||||||
/* update the regl state */
|
|
||||||
const { obsLayout, nObs } = world;
|
|
||||||
const {
|
|
||||||
drawPoints,
|
|
||||||
transform,
|
|
||||||
camera,
|
|
||||||
pointBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
sizeBuffer
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
/* coordinates for each point */
|
|
||||||
const { glScaleX, glScaleY } = transform;
|
|
||||||
const X = obsLayout.col(layoutChoice.currentDimNames[0]).asArray();
|
|
||||||
const Y = obsLayout.col(layoutChoice.currentDimNames[1]).asArray();
|
|
||||||
const newPositions = this.computePointPositions(X, Y, glScaleX, glScaleY);
|
|
||||||
if (renderCache.positions !== newPositions) {
|
|
||||||
/* update our cache & GL if the buffer changes */
|
|
||||||
renderCache.positions = newPositions;
|
|
||||||
pointBuffer({ data: newPositions, dimension: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* colors for each point */
|
|
||||||
const newColors = this.computePointColors(colorRGB);
|
|
||||||
if (renderCache.colors !== newColors) {
|
|
||||||
/* update our cache & GL if the buffer changes */
|
|
||||||
renderCache.colors = newColors;
|
|
||||||
colorBuffer({ data: newColors, dimension: 3 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sizes for each point */
|
|
||||||
const newSizes = this.computePointSizes(nObs, crossfilter);
|
|
||||||
if (renderCache.sizes !== newSizes) {
|
|
||||||
/* update our cache & GL if the buffer changes */
|
|
||||||
renderCache.size = newSizes;
|
|
||||||
sizeBuffer({ data: newSizes, dimension: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.count = nObs;
|
|
||||||
|
|
||||||
regl._refresh();
|
|
||||||
this.reglDraw(
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createToolSVG = () => {
|
|
||||||
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
|
|
||||||
d3.select("#graphAttachPoint")
|
|
||||||
.select("#tool")
|
|
||||||
.remove();
|
|
||||||
|
|
||||||
let handleStart;
|
|
||||||
let handleDrag;
|
|
||||||
let handleEnd;
|
|
||||||
let handleCancel;
|
|
||||||
if (selectionTool === "brush") {
|
|
||||||
handleStart = this.handleBrushStartAction.bind(this);
|
|
||||||
handleDrag = this.handleBrushDragAction.bind(this);
|
|
||||||
handleEnd = this.handleBrushEndAction.bind(this);
|
|
||||||
} else {
|
|
||||||
handleStart = this.handleLassoStart.bind(this);
|
|
||||||
handleEnd = this.handleLassoEnd.bind(this);
|
|
||||||
handleCancel = this.handleLassoCancel.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { svg: newToolSVG, tool, container } = setupSVGandBrushElements(
|
|
||||||
selectionTool,
|
|
||||||
handleStart,
|
|
||||||
handleDrag,
|
|
||||||
handleEnd,
|
|
||||||
handleCancel,
|
|
||||||
responsive,
|
|
||||||
this.graphPaddingRight,
|
|
||||||
graphInteractionMode
|
|
||||||
);
|
|
||||||
|
|
||||||
stateChanges = { ...stateChanges, toolSVG: newToolSVG, tool, container };
|
|
||||||
};
|
|
||||||
|
|
||||||
const createCentroidSVG = () => {
|
|
||||||
d3.select("#graphAttachPoint")
|
|
||||||
.select("#centroid-container")
|
|
||||||
.remove();
|
|
||||||
|
|
||||||
if (centroidLabel.metadataField === "" || !centroidLabel.centroidXY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const centroidScreen = this.mapPointToScreen(centroidLabel.centroidXY);
|
|
||||||
|
|
||||||
const newCentroidSVG = setupCentroidSVG(
|
|
||||||
responsive,
|
|
||||||
this.graphPaddingRight,
|
|
||||||
centroidScreen,
|
|
||||||
centroidLabel.categoryField,
|
|
||||||
colorAccessor
|
|
||||||
);
|
|
||||||
|
|
||||||
stateChanges = { ...stateChanges, centroidSVG: newCentroidSVG };
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.responsive.height !== responsive.height ||
|
|
||||||
prevProps.responsive.width !== responsive.width
|
|
||||||
) {
|
|
||||||
// If the window size has changed we want to recreate all SVGs
|
|
||||||
createToolSVG();
|
|
||||||
createCentroidSVG();
|
|
||||||
} else if (
|
|
||||||
(responsive.height && responsive.width && !toolSVG) ||
|
|
||||||
selectionTool !== prevProps.selectionTool ||
|
|
||||||
prevProps.graphInteractionMode !== graphInteractionMode
|
|
||||||
) {
|
|
||||||
// first time or change of selection tool6
|
|
||||||
createToolSVG();
|
|
||||||
} else if (
|
|
||||||
centroidLabel !== prevProps.centroidLabel ||
|
|
||||||
(responsive.height && responsive.width && !centroidSVG)
|
|
||||||
) {
|
|
||||||
// First time for centroid or label change
|
|
||||||
createCentroidSVG();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
if the selection tool or state has changed, ensure that the selection
|
|
||||||
tool correctly reflects the underlying selection.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
currentSelection !== prevProps.currentSelection ||
|
|
||||||
graphInteractionMode !== prevProps.graphInteractionMode ||
|
|
||||||
stateChanges.toolSVG
|
|
||||||
) {
|
|
||||||
const { tool, container } = this.state;
|
|
||||||
this.selectionToolUpdate(
|
|
||||||
stateChanges.tool ? stateChanges.tool : tool,
|
|
||||||
stateChanges.container ? stateChanges.container : container
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(stateChanges).length > 0) {
|
|
||||||
this.setState(stateChanges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
brushToolUpdate(tool, container) {
|
|
||||||
/*
|
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
|
||||||
anything from this.state, which may be updated asynchronously.
|
|
||||||
*/
|
|
||||||
const { currentSelection } = this.props;
|
|
||||||
if (container) {
|
|
||||||
const toolCurrentSelection = d3.brushSelection(container.node());
|
|
||||||
|
|
||||||
if (currentSelection.mode === "within-rect") {
|
|
||||||
/*
|
|
||||||
if there is a selection, make sure the brush tool matches
|
|
||||||
*/
|
|
||||||
const screenCoords = [
|
|
||||||
this.mapPointToScreen(currentSelection.brushCoords.northwest),
|
|
||||||
this.mapPointToScreen(currentSelection.brushCoords.southeast)
|
|
||||||
];
|
|
||||||
if (!toolCurrentSelection) {
|
|
||||||
/* tool is not selected, so just move the brush */
|
|
||||||
container.call(tool.move, screenCoords);
|
|
||||||
} else {
|
|
||||||
/* there is an active selection and a brush - make sure they match */
|
|
||||||
/* this just sums the difference of each dimension, of each point */
|
|
||||||
let delta = 0;
|
|
||||||
for (let x = 0; x < 2; x += 1) {
|
|
||||||
for (let y = 0; y < 2; y += 1) {
|
|
||||||
delta += Math.abs(
|
|
||||||
screenCoords[x][y] - toolCurrentSelection[x][y]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (delta > 0) {
|
|
||||||
container.call(tool.move, screenCoords);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (toolCurrentSelection) {
|
|
||||||
/* no selection, so clear the brush tool if it is set */
|
|
||||||
container.call(tool.move, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lassoToolUpdate(tool) {
|
|
||||||
/*
|
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
|
||||||
anything from this.state, which may be updated asynchronously.
|
|
||||||
*/
|
|
||||||
const { currentSelection } = this.props;
|
|
||||||
if (currentSelection.mode === "within-polygon") {
|
|
||||||
/*
|
|
||||||
if there is a current selection, make sure the lasso tool matches
|
|
||||||
*/
|
|
||||||
const polygon = currentSelection.polygon.map(p =>
|
|
||||||
this.mapPointToScreen(p)
|
|
||||||
);
|
|
||||||
tool.move(polygon);
|
|
||||||
} else {
|
|
||||||
tool.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionToolUpdate(tool, container) {
|
|
||||||
/*
|
|
||||||
this is called from componentDidUpdate(), so be very careful using
|
|
||||||
anything from this.state, which may be updated asynchronously.
|
|
||||||
*/
|
|
||||||
const { selectionTool } = this.props;
|
|
||||||
switch (selectionTool) {
|
|
||||||
case "brush":
|
|
||||||
this.brushToolUpdate(tool, container);
|
|
||||||
break;
|
|
||||||
case "lasso":
|
|
||||||
this.lassoToolUpdate(tool, container);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
/* punt? */
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) {
|
|
||||||
regl.clear({
|
|
||||||
depth: 1,
|
|
||||||
color: [1, 1, 1, 1]
|
|
||||||
});
|
|
||||||
drawPoints({
|
|
||||||
size: sizeBuffer,
|
|
||||||
distance: camera.distance,
|
|
||||||
color: colorBuffer,
|
|
||||||
position: pointBuffer,
|
|
||||||
count: this.count,
|
|
||||||
view: camera.view()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
restartReglLoop() {
|
|
||||||
const {
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
} = this.state;
|
|
||||||
const reglRender = regl.frame(() => {
|
|
||||||
this.reglDraw(
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
);
|
|
||||||
camera.tick();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
reglRender
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mapScreenToPoint(pin) {
|
|
||||||
/*
|
|
||||||
Map an XY coordinates from screen domain to cell/point range,
|
|
||||||
accounting for current pan/zoom camera.
|
|
||||||
*/
|
|
||||||
const { responsive } = this.props;
|
|
||||||
const { regl, camera, transform } = this.state;
|
|
||||||
const { glScaleX, glScaleY } = transform;
|
|
||||||
|
|
||||||
const gl = regl._gl;
|
|
||||||
|
|
||||||
// get aspect ratio
|
|
||||||
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
|
||||||
const scale = aspect < 1 ? 1 / aspect : 1;
|
|
||||||
|
|
||||||
// compute inverse view matrix
|
|
||||||
const inverse = mat4.invert([], camera.view());
|
|
||||||
|
|
||||||
// transform screen coordinates -> cell coordinates
|
|
||||||
const x = (2 * pin[0]) / (responsive.width - this.graphPaddingRight) - 1;
|
|
||||||
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
|
|
||||||
const pout = [
|
|
||||||
x * inverse[14] * aspect * scale + inverse[12],
|
|
||||||
-(y * inverse[14] * scale + inverse[13])
|
|
||||||
];
|
|
||||||
|
|
||||||
const xy = [glScaleX.invert(pout[0]), glScaleY.invert(pout[1])];
|
|
||||||
return xy;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapPointToScreen(xyCell) {
|
|
||||||
/*
|
|
||||||
Map an XY coordinate from cell/point domain to screen range. Inverse
|
|
||||||
of mapScreenToPoint()
|
|
||||||
*/
|
|
||||||
const { responsive } = this.props;
|
|
||||||
const { regl, camera, transform } = this.state;
|
|
||||||
const { glScaleX, glScaleY } = transform;
|
|
||||||
|
|
||||||
const gl = regl._gl;
|
|
||||||
|
|
||||||
// get aspect ratio
|
|
||||||
const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
|
|
||||||
const scale = aspect < 1 ? 1 / aspect : 1;
|
|
||||||
|
|
||||||
// compute inverse view matrix
|
|
||||||
const inverse = mat4.invert([], camera.view());
|
|
||||||
|
|
||||||
// variable names are choosen to reflect inverse of those used
|
|
||||||
// in mapScreenToPoint().
|
|
||||||
const pout = [glScaleX(xyCell[0]), glScaleY(xyCell[1])];
|
|
||||||
const x = (pout[0] - inverse[12]) / aspect / scale / inverse[14];
|
|
||||||
const y = (-pout[1] - inverse[13]) / scale / inverse[14];
|
|
||||||
|
|
||||||
const pin = [
|
|
||||||
Math.round(((x + 1) * (responsive.width - this.graphPaddingRight)) / 2),
|
|
||||||
Math.round(
|
|
||||||
-((y + 1) / 2 - 1) * (responsive.height - this.graphPaddingTop)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
return pin;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBrushDragAction() {
|
|
||||||
/*
|
|
||||||
event describing brush position:
|
|
||||||
@-------|
|
|
||||||
| |
|
|
||||||
| |
|
|
||||||
|-------@
|
|
||||||
*/
|
|
||||||
// ignore programatically generated events
|
|
||||||
if (d3.event.sourceEvent === null || !d3.event.selection) return;
|
|
||||||
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const s = d3.event.selection;
|
|
||||||
const brushCoords = {
|
|
||||||
northwest: this.mapScreenToPoint([s[0][0], s[0][1]]),
|
|
||||||
southeast: this.mapScreenToPoint([s[1][0], s[1][1]])
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: "graph brush change",
|
|
||||||
brushCoords
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBrushStartAction() {
|
|
||||||
// Ignore programatically generated events.
|
|
||||||
if (!d3.event.sourceEvent) return;
|
|
||||||
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({ type: "graph brush start" });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBrushEndAction() {
|
|
||||||
// Ignore programatically generated events.
|
|
||||||
if (!d3.event.sourceEvent) return;
|
|
||||||
|
|
||||||
/*
|
|
||||||
coordinates will be included if selection made, null
|
|
||||||
if selection cleared.
|
|
||||||
*/
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const s = d3.event.selection;
|
|
||||||
if (s) {
|
|
||||||
const brushCoords = {
|
|
||||||
northwest: this.mapScreenToPoint(s[0]),
|
|
||||||
southeast: this.mapScreenToPoint(s[1])
|
|
||||||
};
|
|
||||||
dispatch({
|
|
||||||
type: "graph brush end",
|
|
||||||
brushCoords
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type: "graph brush deselect"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBrushDeselectAction() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "graph brush deselect"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLassoStart() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "graph lasso start"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// when a lasso is completed, filter to the points within the lasso polygon
|
|
||||||
handleLassoEnd(polygon) {
|
|
||||||
const minimumPolygonArea = 10;
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
polygon.length < 3 ||
|
|
||||||
Math.abs(d3.polygonArea(polygon)) < minimumPolygonArea
|
|
||||||
) {
|
|
||||||
// if less than three points, or super small area, treat as a clear selection.
|
|
||||||
dispatch({ type: "graph lasso deselect" });
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type: "graph lasso end",
|
|
||||||
polygon: polygon.map(xy => this.mapScreenToPoint(xy)) // transform the polygon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLassoCancel() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({ type: "graph lasso cancel" });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLassoDeselectAction() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({ type: "graph lasso deselect" });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeselectAction() {
|
|
||||||
const { selectionTool } = this.props;
|
|
||||||
if (selectionTool === "brush") this.handleBrushDeselectAction();
|
|
||||||
if (selectionTool === "lasso") this.handleLassoDeselectAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOpacityRangeChange(e) {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "change opacity deselected cells in 2d graph background",
|
|
||||||
data: e.target.value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { responsive, graphInteractionMode } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="graphWrapper">
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
zIndex: -9999,
|
|
||||||
position: "fixed",
|
|
||||||
top: this.graphPaddingTop,
|
|
||||||
right: 0
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div id="graphAttachPoint" />
|
|
||||||
<div style={{ padding: 0, margin: 0 }}>
|
|
||||||
<canvas
|
|
||||||
width={responsive.width - this.graphPaddingRight}
|
|
||||||
height={responsive.height - this.graphPaddingTop}
|
|
||||||
data-testid="layout-graph"
|
|
||||||
ref={canvas => {
|
|
||||||
this.reglCanvas = canvas;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Graph;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import * as d3 from "d3";
|
|
||||||
import styles from "./graph.css";
|
|
||||||
|
|
||||||
export default (responsive, graphPaddingRight, xy, text, colorBy) => {
|
|
||||||
const containerWidth = responsive.width - graphPaddingRight;
|
|
||||||
|
|
||||||
const svg = d3
|
|
||||||
.select("#graphAttachPoint")
|
|
||||||
.append("svg")
|
|
||||||
.attr("id", "centroid-container")
|
|
||||||
.attr("data-testid", "centroid-overlay")
|
|
||||||
.attr("width", containerWidth)
|
|
||||||
.attr("height", responsive.height)
|
|
||||||
.attr("class", `${styles.graphSVG}`)
|
|
||||||
.style("z-index", 998)
|
|
||||||
.style("pointer-events", "none");
|
|
||||||
// TODO: Create own styles, ask Colin for an explanation on the css
|
|
||||||
// For now I'm going to put centroid z-index at 998 and lasso on 999
|
|
||||||
|
|
||||||
const label = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("transform", `translate(${xy[0]}, ${xy[1]})`);
|
|
||||||
|
|
||||||
label
|
|
||||||
.append("text")
|
|
||||||
.attr("text-anchor", "middle")
|
|
||||||
.text(text)
|
|
||||||
.style("font-family", "Roboto Condensed")
|
|
||||||
.style("font-size", "18px")
|
|
||||||
.style("font-weight", "700")
|
|
||||||
.style("fill", colorBy ? "black" : "rgb(32, 178, 212)");
|
|
||||||
|
|
||||||
return svg;
|
|
||||||
};
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
// https://bl.ocks.org/pbeshai/8008075f9ce771ee8be39e8c38907570
|
|
||||||
|
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
const Lasso = () => {
|
|
||||||
const dispatch = d3.dispatch("start", "end", "cancel");
|
|
||||||
|
|
||||||
const polygonToPath = polygon =>
|
|
||||||
`M${polygon.map(d => d.join(",")).join("L")}`;
|
|
||||||
|
|
||||||
const distance = (pt1, pt2) =>
|
|
||||||
Math.sqrt((pt2[0] - pt1[0]) ** 2 + (pt2[1] - pt1[1]) ** 2);
|
|
||||||
|
|
||||||
// distance last point has to be to first point before it auto closes when mouse is released
|
|
||||||
const closeDistance = 75;
|
|
||||||
|
|
||||||
const lasso = svg => {
|
|
||||||
let lassoPolygon;
|
|
||||||
let lassoPath;
|
|
||||||
let closePath;
|
|
||||||
|
|
||||||
const handleDragStart = () => {
|
|
||||||
lassoPolygon = [d3.mouse(svg.node())]; // current x y of mouse within element
|
|
||||||
|
|
||||||
if (lassoPath) {
|
|
||||||
lassoPath.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
lassoPath = g
|
|
||||||
.append("path")
|
|
||||||
.attr("data-testid", "lasso-element")
|
|
||||||
.attr("fill", "#0bb")
|
|
||||||
.attr("fill-opacity", 0.1)
|
|
||||||
.attr("stroke", "#0bb")
|
|
||||||
.attr("stroke-dasharray", "3, 3");
|
|
||||||
|
|
||||||
closePath = g
|
|
||||||
.append("line")
|
|
||||||
.attr("x2", lassoPolygon[0][0])
|
|
||||||
.attr("y2", lassoPolygon[0][1])
|
|
||||||
.attr("stroke", "#0bb")
|
|
||||||
.attr("stroke-dasharray", "3, 3")
|
|
||||||
.attr("opacity", 0);
|
|
||||||
|
|
||||||
dispatch.call("start", lasso, lassoPolygon);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrag = () => {
|
|
||||||
const point = d3.mouse(svg.node());
|
|
||||||
lassoPolygon.push(point);
|
|
||||||
lassoPath.attr("d", polygonToPath(lassoPolygon));
|
|
||||||
|
|
||||||
// indicate if we are within closing distance
|
|
||||||
if (
|
|
||||||
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
|
|
||||||
closeDistance
|
|
||||||
) {
|
|
||||||
closePath
|
|
||||||
.attr("x1", point[0])
|
|
||||||
.attr("y1", point[1])
|
|
||||||
.attr("opacity", 1);
|
|
||||||
} else {
|
|
||||||
closePath.attr("opacity", 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
|
||||||
// remove the close path
|
|
||||||
closePath.remove();
|
|
||||||
closePath = null;
|
|
||||||
|
|
||||||
// succesfully closed
|
|
||||||
if (
|
|
||||||
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
|
|
||||||
closeDistance
|
|
||||||
) {
|
|
||||||
lassoPath.attr("d", `${polygonToPath(lassoPolygon)}Z`);
|
|
||||||
dispatch.call("end", lasso, lassoPolygon);
|
|
||||||
|
|
||||||
// otherwise cancel
|
|
||||||
} else {
|
|
||||||
lassoPath.remove();
|
|
||||||
lassoPath = null;
|
|
||||||
lassoPolygon = null;
|
|
||||||
dispatch.call("cancel");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// append a <g> with a rect
|
|
||||||
const g = svg.append("g").attr("class", "lasso-group");
|
|
||||||
const bbox = svg.node().getBoundingClientRect();
|
|
||||||
const area = g
|
|
||||||
.append("rect")
|
|
||||||
.attr("width", bbox.width)
|
|
||||||
.attr("height", bbox.height)
|
|
||||||
.attr("fill", "tomato")
|
|
||||||
.attr("opacity", 0);
|
|
||||||
|
|
||||||
const drag = d3
|
|
||||||
.drag()
|
|
||||||
.on("start", handleDragStart)
|
|
||||||
.on("drag", handleDrag)
|
|
||||||
.on("end", handleDragEnd);
|
|
||||||
|
|
||||||
area.call(drag);
|
|
||||||
|
|
||||||
lasso.reset = () => {
|
|
||||||
if (lassoPath) {
|
|
||||||
lassoPath.remove();
|
|
||||||
lassoPath = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
lassoPolygon = null;
|
|
||||||
if (closePath) {
|
|
||||||
closePath.remove();
|
|
||||||
closePath = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
lasso.move = polygon => {
|
|
||||||
if (polygon !== lassoPolygon || polygon.length !== lassoPolygon.length) {
|
|
||||||
lasso.reset();
|
|
||||||
|
|
||||||
lassoPolygon = polygon;
|
|
||||||
lassoPath = g
|
|
||||||
.append("path")
|
|
||||||
.attr("data-testid", "lasso-element")
|
|
||||||
.attr("fill", "#0bb")
|
|
||||||
.attr("fill-opacity", 0.1)
|
|
||||||
.attr("stroke", "#0bb")
|
|
||||||
.attr("stroke-dasharray", "3, 3");
|
|
||||||
|
|
||||||
lassoPath.attr("d", `${polygonToPath(lassoPolygon)}Z`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
lasso.on = (type, callback) => {
|
|
||||||
dispatch.on(type, callback);
|
|
||||||
return lasso;
|
|
||||||
};
|
|
||||||
|
|
||||||
return lasso;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Lasso;
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import styles from "./graph.css";
|
|
||||||
import Lasso from "./setupLasso";
|
|
||||||
|
|
||||||
/******************************************
|
|
||||||
*******************************************
|
|
||||||
put svg & brush in DOM
|
|
||||||
*******************************************
|
|
||||||
******************************************/
|
|
||||||
|
|
||||||
export default (
|
|
||||||
selectionToolType,
|
|
||||||
handleStartAction,
|
|
||||||
handleDragAction,
|
|
||||||
handleEndAction,
|
|
||||||
handleCancelAction,
|
|
||||||
responsive,
|
|
||||||
graphPaddingRight,
|
|
||||||
graphInteractionMode
|
|
||||||
) => {
|
|
||||||
const svg = d3
|
|
||||||
.select("#graphAttachPoint")
|
|
||||||
.append("svg")
|
|
||||||
.attr("id", "tool")
|
|
||||||
.attr("data-testid", "layout-overlay")
|
|
||||||
.attr("width", responsive.width - graphPaddingRight)
|
|
||||||
.attr("height", responsive.height)
|
|
||||||
.attr("class", `${styles.graphSVG}`)
|
|
||||||
.style("z-index", 999)
|
|
||||||
.style("display", graphInteractionMode === "select" ? "inherit" : "none");
|
|
||||||
|
|
||||||
if (selectionToolType === "brush") {
|
|
||||||
const brush = d3
|
|
||||||
.brush()
|
|
||||||
.extent([
|
|
||||||
[0, 0],
|
|
||||||
[responsive.width - graphPaddingRight, responsive.height]
|
|
||||||
])
|
|
||||||
.on("start", handleStartAction)
|
|
||||||
.on("brush", handleDragAction)
|
|
||||||
// FYI, brush doesn't generate cancel
|
|
||||||
.on("end", handleEndAction);
|
|
||||||
|
|
||||||
const brushContainer = svg
|
|
||||||
.append("g")
|
|
||||||
.attr("class", "graph_brush")
|
|
||||||
.call(brush);
|
|
||||||
|
|
||||||
return { svg, container: brushContainer, tool: brush };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionToolType === "lasso") {
|
|
||||||
const lasso = Lasso()
|
|
||||||
.on("end", handleEndAction)
|
|
||||||
// FYI, Lasso doesn't generate drag
|
|
||||||
.on("start", handleStartAction)
|
|
||||||
.on("cancel", handleCancelAction);
|
|
||||||
|
|
||||||
const lassoContainer = svg.call(lasso);
|
|
||||||
|
|
||||||
return { svg, container: lassoContainer, tool: lasso };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("unknown graph selection tool");
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import Categorical from "../categorical/categorical";
|
|
||||||
import Continuous from "../continuous/continuous";
|
|
||||||
import GeneExpression from "../geneExpression";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import DynamicScatterplot from "../scatterplot/scatterplot";
|
|
||||||
import TopLeftLogoAndTitle from "./topLeftLogoAndTitle";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
responsive: state.responsive,
|
|
||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor
|
|
||||||
}))
|
|
||||||
class LeftSideBar extends React.Component {
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
responsive,
|
|
||||||
scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
/*
|
|
||||||
this magic number should be made less fragile,
|
|
||||||
if cellxgene logo or tabs change, this must as well
|
|
||||||
*/
|
|
||||||
const logoRelatedPadding = 50;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
backgroundColor: "white",
|
|
||||||
/* x y blur spread color */
|
|
||||||
boxShadow: "-3px 0px 6px 2px rgba(153,153,153,0.4)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TopLeftLogoAndTitle />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: responsive.height - logoRelatedPadding,
|
|
||||||
marginTop: logoRelatedPadding,
|
|
||||||
width: globals.leftSidebarWidth,
|
|
||||||
overflowY: "auto",
|
|
||||||
overflowX: "hidden"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Categorical />
|
|
||||||
<GeneExpression />
|
|
||||||
<Continuous />
|
|
||||||
</div>
|
|
||||||
{scatterplotXXaccessor && scatterplotYYaccessor ? (
|
|
||||||
<DynamicScatterplot />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeftSideBar;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import Logo from "../framework/logo";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
responsive: state.responsive,
|
|
||||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
|
||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor
|
|
||||||
}))
|
|
||||||
class LeftSideBar extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { datasetTitle } = this.props;
|
|
||||||
|
|
||||||
const paddingToAvoidScrollBar = 15;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
paddingLeft: 8,
|
|
||||||
paddingTop: 8,
|
|
||||||
width: globals.leftSidebarWidth - paddingToAvoidScrollBar,
|
|
||||||
position: "absolute",
|
|
||||||
backgroundColor: "white",
|
|
||||||
zIndex: 8888
|
|
||||||
/* x y blur spread color */
|
|
||||||
// boxShadow: "-5px -1px 4px 2px rgba(225,225,225,0.4)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Logo size={30} />
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: 28,
|
|
||||||
position: "relative",
|
|
||||||
top: -6,
|
|
||||||
fontWeight: "bold",
|
|
||||||
marginLeft: 5,
|
|
||||||
color: globals.logoColor,
|
|
||||||
userSelect: "none"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
cell<span
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
top: 1,
|
|
||||||
fontWeight: 300,
|
|
||||||
fontSize: 24
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</span>gene
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
data-testid="header"
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
position: "relative",
|
|
||||||
marginLeft: 7,
|
|
||||||
top: -8
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{datasetTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeftSideBar;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { AnchorButton, Tooltip } from "@blueprintjs/core";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { World } from "../../util/stateManager";
|
|
||||||
|
|
||||||
@connect()
|
|
||||||
class CellSetButton extends React.Component {
|
|
||||||
set() {
|
|
||||||
const {
|
|
||||||
differential,
|
|
||||||
crossfilter,
|
|
||||||
dispatch,
|
|
||||||
eitherCellSetOneOrTwo
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Reducer and components assume that value will be null if
|
|
||||||
// no selection made. World..getSelectedByIndex() returns a
|
|
||||||
// zero length TypedArray when nothing is selected.
|
|
||||||
let set = World.getSelectedByIndex(crossfilter);
|
|
||||||
if (set.length === 0) set = null;
|
|
||||||
|
|
||||||
if (!differential.diffExp) {
|
|
||||||
/* diffexp needs to be cleared before we store a new set */
|
|
||||||
dispatch({
|
|
||||||
type: `store current cell selection as differential set ${eitherCellSetOneOrTwo}`,
|
|
||||||
data: set
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { differential, eitherCellSetOneOrTwo } = this.props;
|
|
||||||
const cellListName = `celllist${eitherCellSetOneOrTwo}`;
|
|
||||||
const cellsSelected = differential[cellListName]
|
|
||||||
? differential[cellListName].length
|
|
||||||
: 0;
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
content="Save current selection for differential expression computation"
|
|
||||||
position="top"
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
disabled={differential.diffExp}
|
|
||||||
onClick={this.set.bind(this)}
|
|
||||||
data-testid={`cellset-button-${eitherCellSetOneOrTwo}`}
|
|
||||||
>
|
|
||||||
{eitherCellSetOneOrTwo}
|
|
||||||
{": "}
|
|
||||||
<span data-testid={`cellset-count-${eitherCellSetOneOrTwo}`}>
|
|
||||||
{cellsSelected}
|
|
||||||
</span>
|
|
||||||
{" cells"}
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CellSetButton;
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Position,
|
|
||||||
Button,
|
|
||||||
Popover,
|
|
||||||
NumericInput,
|
|
||||||
Icon
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
|
|
||||||
function Clip(props) {
|
|
||||||
const {
|
|
||||||
pendingClipPercentiles,
|
|
||||||
clipPercentileMin,
|
|
||||||
clipPercentileMax,
|
|
||||||
handleClipOpening,
|
|
||||||
handleClipClosing,
|
|
||||||
handleClipCommit,
|
|
||||||
isClipDisabled,
|
|
||||||
handleClipOnKeyPress,
|
|
||||||
handleClipPercentileMaxValueChange,
|
|
||||||
handleClipPercentileMinValueChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const clipMin =
|
|
||||||
pendingClipPercentiles?.clipPercentileMin ?? clipPercentileMin;
|
|
||||||
const clipMax =
|
|
||||||
pendingClipPercentiles?.clipPercentileMax ?? clipPercentileMax;
|
|
||||||
const activeClipClass =
|
|
||||||
clipPercentileMin > 0 || clipPercentileMax < 100
|
|
||||||
? " bp3-intent-warning"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="bp3-button-group"
|
|
||||||
style={{
|
|
||||||
marginLeft: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
target={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="visualization-settings"
|
|
||||||
className={`bp3-button bp3-icon-timeline-bar-chart ${activeClipClass}`}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
position={Position.BOTTOM_RIGHT}
|
|
||||||
onOpening={handleClipOpening}
|
|
||||||
onClosing={handleClipClosing}
|
|
||||||
content={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
flexDirection: "column",
|
|
||||||
padding: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>Clip all continuous values to percentile range</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
paddingTop: 5,
|
|
||||||
paddingBottom: 5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NumericInput
|
|
||||||
style={{ width: 50 }}
|
|
||||||
data-testid="clip-min-input"
|
|
||||||
onValueChange={handleClipPercentileMinValueChange}
|
|
||||||
onKeyPress={handleClipOnKeyPress}
|
|
||||||
value={clipMin}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
fill={false}
|
|
||||||
minorStepSize={null}
|
|
||||||
rightElement={
|
|
||||||
<div style={{ padding: "4px 2px" }}>
|
|
||||||
<Icon icon="percentage" intent="primary" iconSize={14} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span style={{ marginRight: 5, marginLeft: 5 }}> - </span>
|
|
||||||
<NumericInput
|
|
||||||
style={{ width: 50 }}
|
|
||||||
data-testid="clip-max-input"
|
|
||||||
onValueChange={handleClipPercentileMaxValueChange}
|
|
||||||
onKeyPress={handleClipOnKeyPress}
|
|
||||||
value={clipMax}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
fill={false}
|
|
||||||
minorStepSize={null}
|
|
||||||
rightElement={
|
|
||||||
<div style={{ padding: "4px 2px" }}>
|
|
||||||
<Icon icon="percentage" intent="primary" iconSize={14} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="clip-commit"
|
|
||||||
className="bp3-button"
|
|
||||||
disabled={isClipDisabled()}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
marginRight: 5,
|
|
||||||
marginLeft: 5
|
|
||||||
}}
|
|
||||||
onClick={handleClipCommit}
|
|
||||||
>
|
|
||||||
Clip
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Clip;
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
AnchorButton,
|
|
||||||
Tooltip,
|
|
||||||
Popover,
|
|
||||||
Position,
|
|
||||||
RadioGroup,
|
|
||||||
Radio
|
|
||||||
} from "@blueprintjs/core";
|
|
||||||
import { World } from "../../util/stateManager";
|
|
||||||
import actions from "../../actions";
|
|
||||||
import CellSetButton from "./cellSetButtons";
|
|
||||||
import InformationMenu from "./infoMenu";
|
|
||||||
import UndoRedoReset from "./undoRedoReset";
|
|
||||||
import Clip from "./clip";
|
|
||||||
|
|
||||||
@connect(state => ({
|
|
||||||
universe: state.universe,
|
|
||||||
world: state.world,
|
|
||||||
loading: state.controls.loading,
|
|
||||||
crossfilter: state.crossfilter,
|
|
||||||
differential: state.differential,
|
|
||||||
resettingInterface: state.controls.resettingInterface,
|
|
||||||
layoutChoice: state.layoutChoice,
|
|
||||||
graphInteractionMode: state.controls.graphInteractionMode,
|
|
||||||
clipPercentileMin: Math.round(100 * (state.world?.clipQuantiles?.min ?? 0)),
|
|
||||||
clipPercentileMax: Math.round(100 * (state.world?.clipQuantiles?.max ?? 1)),
|
|
||||||
userDefinedGenes: state.controls.userDefinedGenes,
|
|
||||||
diffexpGenes: state.controls.diffexpGenes,
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
|
||||||
celllist1: state.differential.celllist1,
|
|
||||||
celllist2: state.differential.celllist2,
|
|
||||||
libraryVersions: state.config?.library_versions, // eslint-disable-line camelcase
|
|
||||||
undoDisabled: state["@@undoable/past"].length === 0,
|
|
||||||
redoDisabled: state["@@undoable/future"].length === 0
|
|
||||||
}))
|
|
||||||
class MenuBar extends React.Component {
|
|
||||||
static isValidDigitKeyEvent(e) {
|
|
||||||
/*
|
|
||||||
Return true if this event is necessary to enter a percent number input.
|
|
||||||
Return false if not.
|
|
||||||
|
|
||||||
Returns true for events with keys: backspace, control, alt, meta, [0-9],
|
|
||||||
or events that don't have a key.
|
|
||||||
*/
|
|
||||||
if (e.key === null) return true;
|
|
||||||
if (e.ctrlKey || e.altKey || e.metaKey) return true;
|
|
||||||
|
|
||||||
// concept borrowed from blueprint's numericInputUtils:
|
|
||||||
// keys that print a single character when pressed have a `key` name of
|
|
||||||
// length 1. every other key has a longer `key` name (e.g. "Backspace",
|
|
||||||
// "ArrowUp", "Shift"). since none of those keys can print a character
|
|
||||||
// to the field--and since they may have important native behaviors
|
|
||||||
// beyond printing a character--we don't want to disable their effects.
|
|
||||||
const isSingleCharKey = e.key.length === 1;
|
|
||||||
if (!isSingleCharKey) return true;
|
|
||||||
|
|
||||||
const key = e.key.charCodeAt(0) - 48; /* "0" */
|
|
||||||
return key >= 0 && key <= 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
pendingClipPercentiles: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
isClipDisabled = () => {
|
|
||||||
/*
|
|
||||||
return true if clip button should be disabled.
|
|
||||||
*/
|
|
||||||
const { pendingClipPercentiles } = this.state;
|
|
||||||
const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
|
|
||||||
const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
|
|
||||||
|
|
||||||
const { world } = this.props;
|
|
||||||
const currentClipMin = 100 * world?.clipQuantiles?.min;
|
|
||||||
const currentClipMax = 100 * world?.clipQuantiles?.max;
|
|
||||||
|
|
||||||
// if you change this test, be careful with logic around
|
|
||||||
// comparisons between undefined / NaN handling.
|
|
||||||
const isDisabled =
|
|
||||||
!(clipPercentileMin < clipPercentileMax) ||
|
|
||||||
(clipPercentileMin === currentClipMin &&
|
|
||||||
clipPercentileMax === currentClipMax);
|
|
||||||
|
|
||||||
return isDisabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
isResetDisabled = () => {
|
|
||||||
/*
|
|
||||||
Reset should be disabled when all of the following are true:
|
|
||||||
* nothing is selected in the crossfilter
|
|
||||||
* world EQ universe
|
|
||||||
* nothing is colored by
|
|
||||||
* there are no userDefinedGenes or diffexpGenes displayed
|
|
||||||
* scatterplot is not displayed
|
|
||||||
* nothing in cellset1 or cellset2
|
|
||||||
* clip percentiles are [0,100]
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
crossfilter,
|
|
||||||
world,
|
|
||||||
universe,
|
|
||||||
userDefinedGenes,
|
|
||||||
diffexpGenes,
|
|
||||||
colorAccessor,
|
|
||||||
scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor,
|
|
||||||
celllist1,
|
|
||||||
celllist2,
|
|
||||||
clipPercentileMin,
|
|
||||||
clipPercentileMax
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!crossfilter || !world || !universe) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const nothingSelected = crossfilter.countSelected() === crossfilter.size();
|
|
||||||
const nothingColoredBy = !colorAccessor;
|
|
||||||
const noGenes = userDefinedGenes.length === 0 && diffexpGenes.length === 0;
|
|
||||||
const scatterNotDpl = !scatterplotXXaccessor || !scatterplotYYaccessor;
|
|
||||||
const nothingInCellsets = !celllist1 && !celllist2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
nothingSelected &&
|
|
||||||
World.worldEqUniverse(world, universe) &&
|
|
||||||
nothingColoredBy &&
|
|
||||||
noGenes &&
|
|
||||||
scatterNotDpl &&
|
|
||||||
nothingInCellsets &&
|
|
||||||
clipPercentileMax === 100 &&
|
|
||||||
clipPercentileMin === 0
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
resetInterface = () => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "interface reset started"
|
|
||||||
});
|
|
||||||
dispatch(actions.resetInterface());
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClipOnKeyPress = e => {
|
|
||||||
/*
|
|
||||||
allow only numbers, plus other critical keys which
|
|
||||||
may be required to make a number
|
|
||||||
*/
|
|
||||||
if (!MenuBar.isValidDigitKeyEvent(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClipPercentileMinValueChange = v => {
|
|
||||||
/*
|
|
||||||
Ignore anything that isn't a legit number
|
|
||||||
*/
|
|
||||||
if (!Number.isFinite(v)) return;
|
|
||||||
|
|
||||||
const { pendingClipPercentiles } = this.state;
|
|
||||||
const clipPercentileMax = pendingClipPercentiles?.clipPercentileMax;
|
|
||||||
|
|
||||||
/*
|
|
||||||
clamp to [0, currentClipPercentileMax]
|
|
||||||
*/
|
|
||||||
if (v <= 0) v = 0;
|
|
||||||
if (v > 100) v = 100;
|
|
||||||
const clipPercentileMin = Math.round(v); // paranoia
|
|
||||||
this.setState({
|
|
||||||
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClipPercentileMaxValueChange = v => {
|
|
||||||
/*
|
|
||||||
Ignore anything that isn't a legit number
|
|
||||||
*/
|
|
||||||
if (!Number.isFinite(v)) return;
|
|
||||||
|
|
||||||
const { pendingClipPercentiles } = this.state;
|
|
||||||
const clipPercentileMin = pendingClipPercentiles?.clipPercentileMin;
|
|
||||||
|
|
||||||
/*
|
|
||||||
clamp to [0, 100]
|
|
||||||
*/
|
|
||||||
if (v < 0) v = 0;
|
|
||||||
if (v > 100) v = 100;
|
|
||||||
const clipPercentileMax = Math.round(v); // paranoia
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClipCommit = () => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const { pendingClipPercentiles } = this.state;
|
|
||||||
const { clipPercentileMin, clipPercentileMax } = pendingClipPercentiles;
|
|
||||||
const min = clipPercentileMin / 100;
|
|
||||||
const max = clipPercentileMax / 100;
|
|
||||||
dispatch({
|
|
||||||
type: "set clip quantiles",
|
|
||||||
clipQuantiles: { min, max }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClipOpening = () => {
|
|
||||||
const { clipPercentileMin, clipPercentileMax } = this.props;
|
|
||||||
this.setState({
|
|
||||||
pendingClipPercentiles: { clipPercentileMin, clipPercentileMax }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClipClosing = () => {
|
|
||||||
this.setState({ pendingClipPercentiles: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLayoutChoiceChange = e => {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "set layout choice",
|
|
||||||
layoutChoice: e.currentTarget.value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
computeDiffExp = () => {
|
|
||||||
const { dispatch, differential } = this.props;
|
|
||||||
if (differential.celllist1 && differential.celllist2) {
|
|
||||||
dispatch(
|
|
||||||
actions.requestDifferentialExpression(
|
|
||||||
differential.celllist1,
|
|
||||||
differential.celllist2
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
clearDifferentialExpression = () => {
|
|
||||||
const { dispatch, differential } = this.props;
|
|
||||||
dispatch({
|
|
||||||
type: "clear differential expression",
|
|
||||||
diffExp: differential.diffExp
|
|
||||||
});
|
|
||||||
dispatch({
|
|
||||||
type: "clear scatterplot"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatch,
|
|
||||||
differential,
|
|
||||||
crossfilter,
|
|
||||||
resettingInterface,
|
|
||||||
libraryVersions,
|
|
||||||
undoDisabled,
|
|
||||||
redoDisabled,
|
|
||||||
selectionTool,
|
|
||||||
clipPercentileMin,
|
|
||||||
clipPercentileMax,
|
|
||||||
layoutChoice,
|
|
||||||
graphInteractionMode
|
|
||||||
} = this.props;
|
|
||||||
const { pendingClipPercentiles } = this.state;
|
|
||||||
|
|
||||||
const haveBothCellSets =
|
|
||||||
!!differential.celllist1 && !!differential.celllist2;
|
|
||||||
|
|
||||||
// constants used to create selection tool button
|
|
||||||
let selectionTooltip;
|
|
||||||
let selectionButtonClass;
|
|
||||||
if (selectionTool === "brush") {
|
|
||||||
selectionTooltip = "Brush selection";
|
|
||||||
selectionButtonClass = "bp3-icon-select";
|
|
||||||
} else {
|
|
||||||
selectionTooltip = "Lasso selection";
|
|
||||||
selectionButtonClass = "bp3-icon-polygon-filter";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
right: 8,
|
|
||||||
top: 8
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="bp3-button-group" style={{ marginRight: 10 }}>
|
|
||||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
|
|
||||||
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
|
|
||||||
{!differential.diffExp ? (
|
|
||||||
<Tooltip
|
|
||||||
content="Add two cells selections, see the top 15 differentially expressed genes between them"
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
disabled={!haveBothCellSets}
|
|
||||||
intent="primary"
|
|
||||||
data-testid="diffexp-button"
|
|
||||||
loading={differential.loading}
|
|
||||||
icon="left-join"
|
|
||||||
fill
|
|
||||||
type="button"
|
|
||||||
onClick={this.computeDiffExp}
|
|
||||||
>
|
|
||||||
Compute Differential Expression
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{differential.diffExp ? (
|
|
||||||
<Tooltip
|
|
||||||
content="Remove differentially expressed gene list and clear cell selections"
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
fill
|
|
||||||
intent="warning"
|
|
||||||
onClick={this.clearDifferentialExpression}
|
|
||||||
>
|
|
||||||
Clear Differential Expression
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
|
||||||
content="Show only metadata and cells which are currently selected"
|
|
||||||
position="left"
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
data-testid="subset-button"
|
|
||||||
disabled={
|
|
||||||
crossfilter &&
|
|
||||||
(crossfilter.countSelected() === 0 ||
|
|
||||||
crossfilter.countSelected() === crossfilter.size())
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
marginRight: 10
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch(actions.regraph());
|
|
||||||
dispatch({ type: "increment graph render counter" });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
subset to current selection
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
<div className="bp3-button-group">
|
|
||||||
<Tooltip content={selectionTooltip} position="left">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="mode-lasso"
|
|
||||||
className={`bp3-button ${selectionButtonClass}`}
|
|
||||||
active={graphInteractionMode === "select"}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({
|
|
||||||
type: "change graph interaction mode",
|
|
||||||
data: "select"
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="Pan and zoom" position="left">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="mode-pan-zoom"
|
|
||||||
className="bp3-button bp3-icon-zoom-in"
|
|
||||||
active={graphInteractionMode === "zoom"}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({
|
|
||||||
type: "change graph interaction mode",
|
|
||||||
data: "zoom"
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bp3-button-group"
|
|
||||||
style={{
|
|
||||||
marginLeft: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
target={
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
data-testid="layout-choice"
|
|
||||||
className="bp3-button bp3-icon-heatmap"
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
position={Position.BOTTOM_RIGHT}
|
|
||||||
content={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
flexDirection: "column",
|
|
||||||
padding: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RadioGroup
|
|
||||||
label="Layout Choice"
|
|
||||||
onChange={this.handleLayoutChoiceChange}
|
|
||||||
selectedValue={layoutChoice.current}
|
|
||||||
>
|
|
||||||
{layoutChoice.available.map(name => (
|
|
||||||
<Radio label={name} value={name} key={name} />
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Clip
|
|
||||||
pendingClipPercentiles={pendingClipPercentiles}
|
|
||||||
clipPercentileMin={clipPercentileMin}
|
|
||||||
clipPercentileMax={clipPercentileMax}
|
|
||||||
handleClipOpening={this.handleClipOpening}
|
|
||||||
handleClipClosing={this.handleClipClosing}
|
|
||||||
handleClipCommit={this.handleClipCommit}
|
|
||||||
isClipDisabled={this.isClipDisabled}
|
|
||||||
handleClipOnKeyPress={this.handleClipOnKeyPress}
|
|
||||||
handleClipPercentileMaxValueChange={
|
|
||||||
this.handleClipPercentileMaxValueChange
|
|
||||||
}
|
|
||||||
handleClipPercentileMinValueChange={
|
|
||||||
this.handleClipPercentileMinValueChange
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<UndoRedoReset
|
|
||||||
dispatch={dispatch}
|
|
||||||
isResetDisabled={this.isResetDisabled}
|
|
||||||
resetInterface={this.resetInterface}
|
|
||||||
resettingInterface={resettingInterface}
|
|
||||||
undoDisabled={undoDisabled}
|
|
||||||
redoDisabled={redoDisabled}
|
|
||||||
/>
|
|
||||||
<InformationMenu libraryVersions={libraryVersions} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MenuBar;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
function InformationMenu(props) {
|
|
||||||
const { libraryVersions } = props;
|
|
||||||
return (
|
|
||||||
<div style={{ marginLeft: 10 }} className="bp3-button-group">
|
|
||||||
<Popover
|
|
||||||
content={
|
|
||||||
<Menu>
|
|
||||||
<MenuItem
|
|
||||||
href="https://chanzuckerberg.github.io/cellxgene/faq.html"
|
|
||||||
target="_blank"
|
|
||||||
icon="help"
|
|
||||||
text="FAQ"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
href="https://join-cellxgene-users.herokuapp.com/"
|
|
||||||
target="_blank"
|
|
||||||
icon="chat"
|
|
||||||
text="Chat"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
|
||||||
target="_blank"
|
|
||||||
icon="book"
|
|
||||||
text="Docs"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
href="https://github.com/chanzuckerberg/cellxgene"
|
|
||||||
target="_blank"
|
|
||||||
icon="git-branch"
|
|
||||||
text="Github"
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
target="_blank"
|
|
||||||
text={`cellxgene v${
|
|
||||||
libraryVersions && libraryVersions.cellxgene
|
|
||||||
? libraryVersions.cellxgene
|
|
||||||
: null
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<MenuItem text="MIT License" />
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
position={Position.BOTTOM_RIGHT}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="bp3-button bp3-icon-info-sign"
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InformationMenu;
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import React from "react";
|
|
||||||
import { AnchorButton, Tooltip } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
function InformationMenu(props) {
|
|
||||||
const {
|
|
||||||
resettingInterface,
|
|
||||||
undoDisabled,
|
|
||||||
redoDisabled,
|
|
||||||
resetInterface,
|
|
||||||
isResetDisabled,
|
|
||||||
dispatch
|
|
||||||
} = props;
|
|
||||||
return (
|
|
||||||
<div style={{ marginLeft: 10 }} className="bp3-button-group">
|
|
||||||
<Tooltip content="Undo" position="left">
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
className="bp3-button bp3-icon-undo"
|
|
||||||
disabled={undoDisabled}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({ type: "@@undoable/undo" });
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="Redo" position="left">
|
|
||||||
<AnchorButton
|
|
||||||
type="button"
|
|
||||||
className="bp3-button bp3-icon-redo"
|
|
||||||
disabled={redoDisabled}
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({ type: "@@undoable/redo" });
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
content="Reset cellxgene, clearing all selections"
|
|
||||||
position="left"
|
|
||||||
>
|
|
||||||
<AnchorButton
|
|
||||||
disabled={isResetDisabled()}
|
|
||||||
style={{ marginLeft: 10 }}
|
|
||||||
type="button"
|
|
||||||
loading={resettingInterface}
|
|
||||||
intent="none"
|
|
||||||
icon="refresh"
|
|
||||||
onClick={resetInterface}
|
|
||||||
data-testid="reset"
|
|
||||||
data-testclass={`resetting-${resettingInterface}`}
|
|
||||||
>
|
|
||||||
reset
|
|
||||||
</AnchorButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InformationMenu;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
const mat4 = require("gl-mat4");
|
|
||||||
|
|
||||||
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
|
|
||||||
|
|
||||||
export default function(regl) {
|
|
||||||
return regl({
|
|
||||||
vert: `
|
|
||||||
precision mediump float;
|
|
||||||
attribute vec2 position;
|
|
||||||
attribute vec3 color;
|
|
||||||
attribute float size;
|
|
||||||
uniform float distance;
|
|
||||||
uniform mat4 projection, view;
|
|
||||||
varying vec3 fragColor;
|
|
||||||
void main() {
|
|
||||||
gl_PointSize = 7.0 / pow(distance, 2.5) + size;
|
|
||||||
gl_Position = projection * view * vec4(position.x, -position.y, 0, 1);
|
|
||||||
fragColor = color;
|
|
||||||
}`,
|
|
||||||
|
|
||||||
frag: `
|
|
||||||
precision mediump float;
|
|
||||||
varying vec3 fragColor;
|
|
||||||
void main() {
|
|
||||||
if (length(gl_PointCoord.xy - 0.5) > 0.5) {
|
|
||||||
discard;
|
|
||||||
}
|
|
||||||
gl_FragColor = vec4(fragColor, 1);
|
|
||||||
}`,
|
|
||||||
|
|
||||||
attributes: {
|
|
||||||
position: regl.prop("position"),
|
|
||||||
color: regl.prop("color"),
|
|
||||||
size: regl.prop("size")
|
|
||||||
},
|
|
||||||
|
|
||||||
uniforms: {
|
|
||||||
distance: regl.prop("distance"),
|
|
||||||
view: regl.prop("view"),
|
|
||||||
projection: () => mat4.perspective([], Math.PI / 2, 1, 0.01, 1000)
|
|
||||||
},
|
|
||||||
|
|
||||||
count: regl.prop("count"),
|
|
||||||
|
|
||||||
primitive: "points"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
:local(.scatterplot) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.scatterplot) svg,
|
|
||||||
:local(.scatterplot) canvas {
|
|
||||||
font: 10px sans-serif;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
:local(.scatterplot) canvas {
|
|
||||||
opacity: 0.9;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
// https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc
|
|
||||||
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Button, ButtonGroup } from "@blueprintjs/core";
|
|
||||||
import _regl from "regl";
|
|
||||||
import * as d3 from "d3";
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
|
|
||||||
import _camera from "../../util/camera";
|
|
||||||
|
|
||||||
import setupScatterplot from "./setupScatterplot";
|
|
||||||
import styles from "./scatterplot.css";
|
|
||||||
|
|
||||||
import _drawPoints from "./drawPointsRegl";
|
|
||||||
import scaleLinear from "../../util/scaleLinear";
|
|
||||||
|
|
||||||
import { margin, width, height } from "./util";
|
|
||||||
import finiteExtent from "../../util/finiteExtent";
|
|
||||||
|
|
||||||
@connect(state => {
|
|
||||||
const { world, crossfilter } = state;
|
|
||||||
const { scatterplotXXaccessor, scatterplotYYaccessor } = state.controls;
|
|
||||||
const expressionX =
|
|
||||||
world &&
|
|
||||||
scatterplotXXaccessor &&
|
|
||||||
world.varData.hasCol(scatterplotXXaccessor)
|
|
||||||
? world.varData.col(scatterplotXXaccessor).asArray()
|
|
||||||
: null;
|
|
||||||
const expressionY =
|
|
||||||
world &&
|
|
||||||
scatterplotYYaccessor &&
|
|
||||||
world.varData.hasCol(scatterplotYYaccessor)
|
|
||||||
? world.varData.col(scatterplotYYaccessor).asArray()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
world,
|
|
||||||
|
|
||||||
colorRGB: state.colors.rgb,
|
|
||||||
colorScale: state.colors.scale,
|
|
||||||
colorAccessor: state.colors.colorAccessor,
|
|
||||||
|
|
||||||
// Accessors are var/gene names (strings)
|
|
||||||
scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor,
|
|
||||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
|
||||||
|
|
||||||
differential: state.differential,
|
|
||||||
|
|
||||||
expressionX,
|
|
||||||
expressionY,
|
|
||||||
|
|
||||||
crossfilter
|
|
||||||
};
|
|
||||||
})
|
|
||||||
class Scatterplot extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.count = 0;
|
|
||||||
this.axes = false;
|
|
||||||
this.renderCache = {
|
|
||||||
positions: null,
|
|
||||||
colors: null,
|
|
||||||
sizes: null,
|
|
||||||
xScale: null,
|
|
||||||
yScale: null
|
|
||||||
};
|
|
||||||
this.state = {
|
|
||||||
svg: null,
|
|
||||||
minimized: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { svg } = setupScatterplot(width, height, margin);
|
|
||||||
let scales;
|
|
||||||
const { expressionX, expressionY } = this.props;
|
|
||||||
|
|
||||||
if (svg && expressionX && expressionY) {
|
|
||||||
scales = Scatterplot.setupScales(expressionX, expressionY);
|
|
||||||
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
|
|
||||||
this.renderCache = { ...this.renderCache, ...scales };
|
|
||||||
}
|
|
||||||
|
|
||||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
|
||||||
const regl = _regl(this.reglCanvas);
|
|
||||||
|
|
||||||
const drawPoints = _drawPoints(regl);
|
|
||||||
|
|
||||||
// preallocate buffers
|
|
||||||
const pointBuffer = regl.buffer();
|
|
||||||
const colorBuffer = regl.buffer();
|
|
||||||
const sizeBuffer = regl.buffer();
|
|
||||||
|
|
||||||
const reglRender = regl.frame(() => {
|
|
||||||
this.reglDraw(
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
);
|
|
||||||
camera.tick();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.reglRenderState = "rendering";
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
regl,
|
|
||||||
sizeBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
svg,
|
|
||||||
reglRender,
|
|
||||||
camera,
|
|
||||||
drawPoints
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
world,
|
|
||||||
crossfilter,
|
|
||||||
scatterplotXXaccessor,
|
|
||||||
scatterplotYYaccessor,
|
|
||||||
expressionX,
|
|
||||||
expressionY,
|
|
||||||
colorRGB
|
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
reglRender,
|
|
||||||
regl,
|
|
||||||
pointBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
sizeBuffer,
|
|
||||||
svg,
|
|
||||||
drawPoints,
|
|
||||||
camera
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (
|
|
||||||
scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
|
||||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
|
|
||||||
world !== prevProps.world // shape or clip of world changed
|
|
||||||
) {
|
|
||||||
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
|
||||||
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
|
|
||||||
this.renderCache = { ...this.renderCache, ...scales };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reglRender && this.reglRenderState === "rendering") {
|
|
||||||
reglRender.cancel();
|
|
||||||
this.reglRenderState = "paused";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
world &&
|
|
||||||
regl &&
|
|
||||||
pointBuffer &&
|
|
||||||
colorBuffer &&
|
|
||||||
sizeBuffer &&
|
|
||||||
expressionX &&
|
|
||||||
expressionY &&
|
|
||||||
scatterplotXXaccessor &&
|
|
||||||
scatterplotYYaccessor
|
|
||||||
) {
|
|
||||||
const { renderCache } = this;
|
|
||||||
const { xScale, yScale } = this.renderCache;
|
|
||||||
const cellCount = expressionX.length;
|
|
||||||
|
|
||||||
// Points change when expressionX or expressionY change.
|
|
||||||
if (
|
|
||||||
!renderCache.positions ||
|
|
||||||
expressionX !== prevProps.expressionX ||
|
|
||||||
expressionY !== prevProps.expressionY
|
|
||||||
) {
|
|
||||||
if (!renderCache.positions) {
|
|
||||||
renderCache.positions = new Float32Array(2 * cellCount);
|
|
||||||
}
|
|
||||||
const glScaleX = scaleLinear([0, width], [-0.95, 0.95]);
|
|
||||||
const glScaleY = scaleLinear([0, height], [-1, 1]);
|
|
||||||
for (let i = 0, { positions } = renderCache; i < cellCount; i += 1) {
|
|
||||||
positions[2 * i] = glScaleX(xScale(expressionX[i]));
|
|
||||||
positions[2 * i + 1] = glScaleY(yScale(expressionY[i]));
|
|
||||||
}
|
|
||||||
pointBuffer({ data: renderCache.positions, dimension: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Colors for each point - change only when props.colorsRGB change.
|
|
||||||
if (!renderCache.colors || colorRGB !== prevProps.colorRGB) {
|
|
||||||
if (!renderCache.colors) {
|
|
||||||
renderCache.colors = new Float32Array(3 * cellCount);
|
|
||||||
}
|
|
||||||
for (let i = 0, { colors } = renderCache; i < cellCount; i += 1) {
|
|
||||||
colors.set(colorRGB[i], 3 * i);
|
|
||||||
}
|
|
||||||
colorBuffer({ data: renderCache.colors, dimension: 3 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sizes for each point - updates are triggered only when selected
|
|
||||||
// obs change
|
|
||||||
if (!renderCache.sizes || crossfilter !== prevProps.crossfilter) {
|
|
||||||
if (!renderCache.sizes) {
|
|
||||||
renderCache.sizes = new Float32Array(cellCount);
|
|
||||||
}
|
|
||||||
crossfilter.fillByIsSelected(renderCache.sizes, 4, 0.2);
|
|
||||||
sizeBuffer({ data: renderCache.sizes, dimension: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.count = cellCount;
|
|
||||||
|
|
||||||
regl._refresh();
|
|
||||||
this.reglDraw(
|
|
||||||
regl,
|
|
||||||
drawPoints,
|
|
||||||
sizeBuffer,
|
|
||||||
colorBuffer,
|
|
||||||
pointBuffer,
|
|
||||||
camera
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static setupScales(expressionX, expressionY) {
|
|
||||||
const xScale = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain(finiteExtent(expressionX))
|
|
||||||
.range([0, width]);
|
|
||||||
const yScale = d3
|
|
||||||
.scaleLinear()
|
|
||||||
.domain(finiteExtent(expressionY))
|
|
||||||
.range([height, 0]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
xScale,
|
|
||||||
yScale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) {
|
|
||||||
regl.clear({
|
|
||||||
depth: 1,
|
|
||||||
color: [1, 1, 1, 1]
|
|
||||||
});
|
|
||||||
|
|
||||||
drawPoints({
|
|
||||||
size: sizeBuffer,
|
|
||||||
distance: camera.distance,
|
|
||||||
color: colorBuffer,
|
|
||||||
position: pointBuffer,
|
|
||||||
count: this.count,
|
|
||||||
view: camera.view()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
drawAxesSVG(xScale, yScale, svg) {
|
|
||||||
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
|
|
||||||
svg.selectAll("*").remove();
|
|
||||||
|
|
||||||
// the axes are much cleaner and easier now. No need to rotate and orient
|
|
||||||
// the axis, just call axisBottom, axisLeft etc.
|
|
||||||
const xAxis = d3
|
|
||||||
.axisBottom()
|
|
||||||
.ticks(7)
|
|
||||||
.scale(xScale);
|
|
||||||
|
|
||||||
const yAxis = d3
|
|
||||||
.axisLeft()
|
|
||||||
.ticks(7)
|
|
||||||
.scale(yScale);
|
|
||||||
|
|
||||||
// adding axes is also simpler now, just translate x-axis to (0,height)
|
|
||||||
// and it's alread defined to be a bottom axis.
|
|
||||||
svg
|
|
||||||
.append("g")
|
|
||||||
.attr("transform", `translate(0,${height})`)
|
|
||||||
.attr("class", "x axis")
|
|
||||||
.call(xAxis);
|
|
||||||
|
|
||||||
// y-axis is translated to (0,0)
|
|
||||||
svg
|
|
||||||
.append("g")
|
|
||||||
.attr("transform", "translate(0,0)")
|
|
||||||
.attr("class", "y axis")
|
|
||||||
.call(yAxis);
|
|
||||||
|
|
||||||
// adding label. For x-axis, it's at (10, 10), and for y-axis at (width, height-10).
|
|
||||||
svg
|
|
||||||
.append("text")
|
|
||||||
.attr("x", 10)
|
|
||||||
.attr("y", 10)
|
|
||||||
.attr("class", "label")
|
|
||||||
.style("font-style", "italic")
|
|
||||||
.text(scatterplotYYaccessor);
|
|
||||||
|
|
||||||
svg
|
|
||||||
.append("text")
|
|
||||||
.attr("x", width)
|
|
||||||
.attr("y", height - 10)
|
|
||||||
.attr("text-anchor", "end")
|
|
||||||
.attr("class", "label")
|
|
||||||
.style("font-style", "italic")
|
|
||||||
.text(scatterplotXXaccessor);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
const { minimized } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
bottom: minimized ? -height + -margin.top : 0,
|
|
||||||
borderRadius: "3px 3px 0px 0px",
|
|
||||||
left: globals.leftSidebarWidth + globals.scatterplotMarginLeft,
|
|
||||||
padding: "0px 20px 20px 0px",
|
|
||||||
backgroundColor: "white",
|
|
||||||
/* x y blur spread color */
|
|
||||||
boxShadow: "0px 0px 6px 2px rgba(153,153,153,0.4)"
|
|
||||||
}}
|
|
||||||
id="scatterplot_wrapper"
|
|
||||||
>
|
|
||||||
<ButtonGroup
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
right: 5,
|
|
||||||
top: 5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
minimal
|
|
||||||
onClick={() => {
|
|
||||||
this.setState({ minimized: !minimized });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{minimized ? "show scatterplot" : "hide"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
minimal
|
|
||||||
data-testid="clear-scatterplot"
|
|
||||||
onClick={() =>
|
|
||||||
dispatch({
|
|
||||||
type: "clear scatterplot"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
remove
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
<div
|
|
||||||
className={styles.scatterplot}
|
|
||||||
id="scatterplot"
|
|
||||||
style={{
|
|
||||||
width: `${width + margin.left + margin.right}px`,
|
|
||||||
height: `${height + margin.top + margin.bottom}px`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<canvas
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
data-testid="scatterplot"
|
|
||||||
style={{
|
|
||||||
marginLeft: margin.left - 7,
|
|
||||||
marginTop: margin.top
|
|
||||||
}}
|
|
||||||
ref={canvas => {
|
|
||||||
this.reglCanvas = canvas;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Scatterplot;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
/*****************************************
|
|
||||||
******************************************
|
|
||||||
Setup SVG & Canvas elements
|
|
||||||
******************************************
|
|
||||||
******************************************/
|
|
||||||
|
|
||||||
import * as d3 from "d3";
|
|
||||||
|
|
||||||
const setupScatterplot = (width, height, margin) => {
|
|
||||||
const container = d3.select("#scatterplot");
|
|
||||||
|
|
||||||
const svg = container
|
|
||||||
.append("svg")
|
|
||||||
.attr("width", width + margin.left + margin.right)
|
|
||||||
.attr("height", height + margin.top + margin.bottom)
|
|
||||||
.attr("data-testid", "scatterplot-svg")
|
|
||||||
.append("g")
|
|
||||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
svg
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default setupScatterplot;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
export const margin = { top: 40, right: 5, bottom: 20, left: 60 };
|
|
||||||
export const width = 340;
|
|
||||||
export const height = 340 - margin.top - margin.bottom;
|
|
||||||
export const innerHeight = height - 2;
|
|
||||||
|
|
||||||
export const devicePixelRatio = window.devicePixelRatio || 1;
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { Colors } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
/* if a categorical metadata field has more options than this, truncate */
|
|
||||||
export const maxCategoricalOptionsToDisplay = 100;
|
|
||||||
|
|
||||||
/*
|
|
||||||
these are default values for configuration the CLI may supply.
|
|
||||||
See the REST API and CLI specs for more info.
|
|
||||||
*/
|
|
||||||
export const configDefaults = {
|
|
||||||
features: {},
|
|
||||||
displayNames: {},
|
|
||||||
parameters: {
|
|
||||||
"max-category-items": 1000
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* colors */
|
|
||||||
export const blue = Colors.BLUE3;
|
|
||||||
export const linkBlue = Colors.BLUE5;
|
|
||||||
export const lightestGrey = "rgb(249,249,249)";
|
|
||||||
export const lighterGrey = "rgb(245,245,245)";
|
|
||||||
export const lightGrey = "rgb(211,211,211)";
|
|
||||||
export const mediumGrey = "rgb(153,153,153)";
|
|
||||||
export const darkGrey = "rgb(102,102,102)";
|
|
||||||
export const darkerGrey = "rgb(51,51,51)";
|
|
||||||
|
|
||||||
export const brightBlue = "#4a90e2";
|
|
||||||
export const brightGreen = "#A2D729";
|
|
||||||
export const darkGreen = "#448C4D";
|
|
||||||
|
|
||||||
export const nonFiniteCellColor = lightGrey;
|
|
||||||
export const defaultCellColor = "rgb(0,0,0,1)";
|
|
||||||
export const logoColor = "black"; /* logo pink: "#E9429A" */
|
|
||||||
|
|
||||||
/* typography constants */
|
|
||||||
|
|
||||||
export const tiniestFontSize = 12;
|
|
||||||
export const largestFontSize = 24;
|
|
||||||
export const uppercaseLetterSpacing = "0.04em";
|
|
||||||
export const bolder = 700;
|
|
||||||
export const accentFont = "Georgia,Times,Times New Roman,serif";
|
|
||||||
export const maxParagraphWidth = 600;
|
|
||||||
|
|
||||||
/* layout styles constants */
|
|
||||||
|
|
||||||
export const cellxgeneTitleLeftPadding = 14;
|
|
||||||
export const cellxgeneTitleTopPadding = 7;
|
|
||||||
|
|
||||||
export const maxControlsWidth = 800;
|
|
||||||
|
|
||||||
export const graphMargin = { top: 20, right: 10, bottom: 30, left: 40 };
|
|
||||||
export const graphWidth = 700;
|
|
||||||
export const graphHeight = 700;
|
|
||||||
export const scatterplotMarginLeft = 25;
|
|
||||||
|
|
||||||
export const leftSidebarWidth = 365;
|
|
||||||
export const leftSidebarSectionHeading = {
|
|
||||||
fontSize: 18,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
fontWeight: 500,
|
|
||||||
letterSpacing: ".05em"
|
|
||||||
};
|
|
||||||
export const leftSidebarSectionPadding = 10;
|
|
||||||
|
|
||||||
let _API = {
|
|
||||||
// prefix: "http://api.clustering.czi.technology/api/",
|
|
||||||
// prefix: "http://tabulamuris.cxg.czi.technology/api/",
|
|
||||||
// prefix: "http://api-staging.clustering.czi.technology/api/",
|
|
||||||
prefix: "http://localhost:5005/api/",
|
|
||||||
version: "v0.2/"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window.CELLXGENE && window.CELLXGENE.API) _API = window.CELLXGENE.API;
|
|
||||||
export const API = _API;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/*
|
|
||||||
>>>GLOBAL<<< CSS imports & definitions
|
|
||||||
|
|
||||||
Do NOT put anything in this file that is not global. We do not perform
|
|
||||||
namespace mangling on these definitions. See webpack config for specifics.
|
|
||||||
*/
|
|
||||||
@import "~@blueprintjs/core/lib/css/blueprint.css";
|
|
||||||
@import "~@blueprintjs/icons/lib/css/blueprint-icons.css";
|
|
||||||
@import "~@blueprintjs/select/lib/css/blueprint-select.css";
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
import React from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import { Provider } from "react-redux";
|
|
||||||
import { FocusStyleManager } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
/* our code */
|
|
||||||
import App from "./components/app";
|
|
||||||
import store from "./reducers";
|
|
||||||
|
|
||||||
FocusStyleManager.onlyShowFocusOnTabs();
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<App />
|
|
||||||
</Provider>,
|
|
||||||
document.getElementById("root")
|
|
||||||
);
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
export default function cascadeReducers(arg) {
|
|
||||||
/*
|
|
||||||
Combined a set of cascading reducers into a single reducer. Cascading
|
|
||||||
reducers are reducers which may rely on state computed by another reducer.
|
|
||||||
Therefore, they:
|
|
||||||
- must be composed in a particular order (currently, this is a simple
|
|
||||||
linear list of reducers, run in list order)
|
|
||||||
- must have access to partially updated "next state" so they can further
|
|
||||||
derive state.
|
|
||||||
|
|
||||||
Parameter is one of:
|
|
||||||
- a Map object
|
|
||||||
- an array of tuples, [ [key1, reducer1], [key2, reducer2], ... ]
|
|
||||||
Ie, cascadeReducers([ ["a", reduceA], ["b", reduceB] ])
|
|
||||||
|
|
||||||
Each reducer will be called with the sigature:
|
|
||||||
(prevState, action, sharedNextState, sharedPrevState) => newState
|
|
||||||
|
|
||||||
cascadeReducers will build a composite newState object, much
|
|
||||||
like combinedReducers. Additional semantics:
|
|
||||||
- reducers guaranteed to be called in order
|
|
||||||
- each reducer will receive shared objects
|
|
||||||
*/
|
|
||||||
const reducers = arg instanceof Map ? arg : new Map(arg);
|
|
||||||
const reducerKeys = [...reducers.keys()];
|
|
||||||
return (prevState, action) => {
|
|
||||||
const nextState = {};
|
|
||||||
let stateChange = false;
|
|
||||||
for (let i = 0, l = reducerKeys.length; i < l; i += 1) {
|
|
||||||
const key = reducerKeys[i];
|
|
||||||
const reducer = reducers.get(key);
|
|
||||||
const prevStateForKey = prevState ? prevState[key] : undefined;
|
|
||||||
const nextStateForKey = reducer(
|
|
||||||
prevStateForKey,
|
|
||||||
action,
|
|
||||||
nextState,
|
|
||||||
prevState
|
|
||||||
);
|
|
||||||
nextState[key] = nextStateForKey;
|
|
||||||
stateChange = stateChange || nextStateForKey !== prevStateForKey;
|
|
||||||
}
|
|
||||||
return stateChange ? nextState : prevState;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { ControlsHelpers } from "../util/stateManager";
|
|
||||||
import * as globals from "../globals";
|
|
||||||
|
|
||||||
function maxCategoryItems(state) {
|
|
||||||
return (
|
|
||||||
state.config.parameters?.["max-category-items"] ??
|
|
||||||
globals.configDefaults.parameters["max-category-items"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CategoricalSelection = (
|
|
||||||
state,
|
|
||||||
action,
|
|
||||||
nextSharedState,
|
|
||||||
prevSharedState
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load complete (universe exists)":
|
|
||||||
case "set World to current selection":
|
|
||||||
case "reset World to eq Universe":
|
|
||||||
case "set clip quantiles": {
|
|
||||||
const { world } = nextSharedState;
|
|
||||||
return ControlsHelpers.createCategoricalSelection(
|
|
||||||
maxCategoryItems(prevSharedState),
|
|
||||||
world
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "categorical metadata filter select": {
|
|
||||||
/*
|
|
||||||
Set the specific category in this field to false
|
|
||||||
*/
|
|
||||||
const newCategoryValueSelected = Array.from(
|
|
||||||
state[action.metadataField].categoryValueSelected
|
|
||||||
);
|
|
||||||
newCategoryValueSelected[action.categoryIndex] = true;
|
|
||||||
const newCategoricalSelection = {
|
|
||||||
...state,
|
|
||||||
[action.metadataField]: {
|
|
||||||
...state[action.metadataField],
|
|
||||||
categoryValueSelected: newCategoryValueSelected
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return newCategoricalSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "categorical metadata filter deselect": {
|
|
||||||
/*
|
|
||||||
Set the specific category in this field to false
|
|
||||||
*/
|
|
||||||
const newCategoryValueSelected = Array.from(
|
|
||||||
state[action.metadataField].categoryValueSelected
|
|
||||||
);
|
|
||||||
newCategoryValueSelected[action.categoryIndex] = false;
|
|
||||||
const newCategoricalSelection = {
|
|
||||||
...state,
|
|
||||||
[action.metadataField]: {
|
|
||||||
...state[action.metadataField],
|
|
||||||
categoryValueSelected: newCategoryValueSelected
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return newCategoricalSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "categorical metadata filter none of these": {
|
|
||||||
/*
|
|
||||||
set all categories in this field to false.
|
|
||||||
*/
|
|
||||||
const newCategoricalSelection = {
|
|
||||||
...state,
|
|
||||||
[action.metadataField]: {
|
|
||||||
...state[action.metadataField],
|
|
||||||
categorySelected: false,
|
|
||||||
categoryValueSelected: Array.from(
|
|
||||||
state[action.metadataField].categoryValueSelected
|
|
||||||
).fill(false)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return newCategoricalSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "categorical metadata filter all of these": {
|
|
||||||
/*
|
|
||||||
set all categories in this field to true.
|
|
||||||
*/
|
|
||||||
const newCategoricalSelection = {
|
|
||||||
...state,
|
|
||||||
[action.metadataField]: {
|
|
||||||
...state[action.metadataField],
|
|
||||||
categorySelected: true,
|
|
||||||
categoryValueSelected: Array.from(
|
|
||||||
state[action.metadataField].categoryValueSelected
|
|
||||||
).fill(true)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return newCategoricalSelection;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CategoricalSelection;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import calcCentroid from "../util/centroid";
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
metadataField: "",
|
|
||||||
categoryIndex: -1,
|
|
||||||
categoryField: "",
|
|
||||||
centroidXY: [-1, -1]
|
|
||||||
};
|
|
||||||
|
|
||||||
const CentroidLabel = (state = initialState, action, sharedNextState) => {
|
|
||||||
const { categoricalSelection, world, layoutChoice } = sharedNextState;
|
|
||||||
const { metadataField, categoryIndex } = action;
|
|
||||||
const categoryField =
|
|
||||||
categoricalSelection?.[metadataField]?.categoryValues[categoryIndex];
|
|
||||||
switch (action.type) {
|
|
||||||
case "category value mouse hover start":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
metadataField,
|
|
||||||
categoryIndex,
|
|
||||||
categoryField,
|
|
||||||
centroidXY: calcCentroid(
|
|
||||||
world,
|
|
||||||
metadataField,
|
|
||||||
categoryField,
|
|
||||||
layoutChoice.currentDimNames
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
case "category value mouse hover end":
|
|
||||||
if (
|
|
||||||
metadataField === state.metadataField &&
|
|
||||||
categoryIndex === state.categoryIndex
|
|
||||||
) {
|
|
||||||
return initialState;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CentroidLabel;
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { createColors } from "../util/stateManager";
|
|
||||||
|
|
||||||
const ColorsReducer = (
|
|
||||||
state = {
|
|
||||||
colorMode: null,
|
|
||||||
colorAccessor: null,
|
|
||||||
rgb: null,
|
|
||||||
scale: null
|
|
||||||
},
|
|
||||||
action,
|
|
||||||
nextSharedState,
|
|
||||||
prevSharedState
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load complete (universe exists)":
|
|
||||||
case "reset World to eq Universe": {
|
|
||||||
const { world } = nextSharedState;
|
|
||||||
const colorMode = null;
|
|
||||||
const colorAccessor = null;
|
|
||||||
const { rgb, scale } = createColors(world, colorMode);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
colorAccessor,
|
|
||||||
colorMode,
|
|
||||||
rgb,
|
|
||||||
scale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "set clip quantiles":
|
|
||||||
case "set World to current selection": {
|
|
||||||
const { colorMode, colorAccessor } = state;
|
|
||||||
const { world } = nextSharedState;
|
|
||||||
const { rgb, scale } = createColors(world, colorMode, colorAccessor);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
rgb,
|
|
||||||
scale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "reset colorscale": {
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
const { rgb, scale } = createColors(world);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
colorMode: null,
|
|
||||||
colorAccessor: null,
|
|
||||||
rgb,
|
|
||||||
scale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "color by categorical metadata":
|
|
||||||
case "color by continuous metadata": {
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
|
|
||||||
/* toggle between this mode and reset */
|
|
||||||
const resetCurrent =
|
|
||||||
action.type === state.colorMode &&
|
|
||||||
action.colorAccessor === state.colorAccessor;
|
|
||||||
const colorMode = !resetCurrent ? action.type : null;
|
|
||||||
const colorAccessor = !resetCurrent ? action.colorAccessor : null;
|
|
||||||
|
|
||||||
const { rgb, scale } = createColors(world, colorMode, colorAccessor);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
colorMode,
|
|
||||||
colorAccessor,
|
|
||||||
rgb,
|
|
||||||
scale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "color by expression": {
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
|
|
||||||
/* toggle between this mode and reset */
|
|
||||||
const resetCurrent =
|
|
||||||
action.type === state.colorMode && action.gene === state.colorAccessor;
|
|
||||||
const colorMode = !resetCurrent ? action.type : null;
|
|
||||||
const colorAccessor = !resetCurrent ? action.gene : null;
|
|
||||||
|
|
||||||
const { rgb, scale } = createColors(world, colorMode, colorAccessor);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
colorMode,
|
|
||||||
colorAccessor,
|
|
||||||
rgb,
|
|
||||||
scale
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ColorsReducer;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
const Config = (
|
|
||||||
state = {
|
|
||||||
displayNames: null,
|
|
||||||
features: null,
|
|
||||||
parameters: null
|
|
||||||
},
|
|
||||||
action
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load start":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: true,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
case "configuration load complete":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
...action.config
|
|
||||||
};
|
|
||||||
case "initial data load error":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
error: action.error
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Config;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { makeContinuousDimensionName } from "../util/nameCreators";
|
|
||||||
|
|
||||||
const ContinuousSelection = (state = {}, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "reset World to eq Universe":
|
|
||||||
case "set clip quantiles": {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
case "continuous metadata histogram start":
|
|
||||||
case "continuous metadata histogram brush":
|
|
||||||
case "continuous metadata histogram end": {
|
|
||||||
const name = makeContinuousDimensionName(
|
|
||||||
action.continuousNamespace,
|
|
||||||
action.selection
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
[name]: action.range
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "continuous metadata histogram cancel": {
|
|
||||||
const name = makeContinuousDimensionName(
|
|
||||||
action.continuousNamespace,
|
|
||||||
action.selection
|
|
||||||
);
|
|
||||||
const { [name]: deletedField, ...newState } = state;
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContinuousSelection;
|
|
||||||
Vendored
-190
@@ -1,190 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
|
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
import { WorldUtil } from "../util/stateManager";
|
|
||||||
|
|
||||||
const Controls = (
|
|
||||||
state = {
|
|
||||||
// data loading flag
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
|
|
||||||
// all of the data + selection state
|
|
||||||
userDefinedGenes: [],
|
|
||||||
userDefinedGenesLoading: false,
|
|
||||||
diffexpGenes: [],
|
|
||||||
|
|
||||||
resettingInterface: false,
|
|
||||||
graphInteractionMode: "select",
|
|
||||||
opacityForDeselectedCells: 0.2,
|
|
||||||
scatterplotXXaccessor: null, // just easier to read
|
|
||||||
scatterplotYYaccessor: null,
|
|
||||||
graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */,
|
|
||||||
__storedStateForCelllist1__: null /* will need procedural control of brush ie., brush.extent https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81 */,
|
|
||||||
__storedStateForCelllist2__: null
|
|
||||||
},
|
|
||||||
action,
|
|
||||||
nextSharedState,
|
|
||||||
prevSharedState
|
|
||||||
) => {
|
|
||||||
/*
|
|
||||||
For now, log anything looking like an error to the console.
|
|
||||||
*/
|
|
||||||
if (action.error || /error/i.test(action.type)) {
|
|
||||||
console.error(action.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
/*****************************************************
|
|
||||||
Initialization, World/Universe management
|
|
||||||
and data loading.
|
|
||||||
******************************************************/
|
|
||||||
case "initial data load start": {
|
|
||||||
return { ...state, loading: true };
|
|
||||||
}
|
|
||||||
case "initial data load complete (universe exists)": {
|
|
||||||
/* first light - create world & other data-driven defaults */
|
|
||||||
WorldUtil.clearCaches();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
resettingInterface: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "reset World to eq Universe": {
|
|
||||||
WorldUtil.clearCaches();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
resettingInterface: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "set World to current selection": {
|
|
||||||
WorldUtil.clearCaches();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "request user defined gene started": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
userDefinedGenesLoading: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "request user defined gene error": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
userDefinedGenesLoading: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "request user defined gene success": {
|
|
||||||
const { userDefinedGenes } = state;
|
|
||||||
const _userDefinedGenes = _.uniq(
|
|
||||||
userDefinedGenes.concat(action.data.genes)
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
userDefinedGenes: _userDefinedGenes,
|
|
||||||
userDefinedGenesLoading: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "request differential expression success": {
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
|
||||||
const _diffexpGenes = [];
|
|
||||||
action.data.forEach(d => {
|
|
||||||
_diffexpGenes.push(world.varAnnotations.at(d[0], varIndexName));
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
diffexpGenes: _diffexpGenes
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "clear differential expression": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
diffexpGenes: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "clear user defined gene": {
|
|
||||||
const { userDefinedGenes } = state;
|
|
||||||
const newUserDefinedGenes = _.filter(
|
|
||||||
userDefinedGenes,
|
|
||||||
d => d !== action.data
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
userDefinedGenes: newUserDefinedGenes
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "clear all user defined genes": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
userDefinedGenes: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "expression load error":
|
|
||||||
case "initial data load error": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
error: action.error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
User Events
|
|
||||||
*******************************/
|
|
||||||
case "change graph interaction mode":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
graphInteractionMode: action.data
|
|
||||||
};
|
|
||||||
case "change opacity deselected cells in 2d graph background":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
opacityForDeselectedCells: action.data
|
|
||||||
};
|
|
||||||
case "increment graph render counter": {
|
|
||||||
const c = state.graphRenderCounter + 1;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
graphRenderCounter: c
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "interface reset started": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
resettingInterface: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*******************************
|
|
||||||
Scatterplot
|
|
||||||
*******************************/
|
|
||||||
case "set scatterplot x":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
scatterplotXXaccessor: action.data
|
|
||||||
};
|
|
||||||
case "set scatterplot y":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
scatterplotYYaccessor: action.data
|
|
||||||
};
|
|
||||||
case "clear scatterplot":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
scatterplotXXaccessor: null,
|
|
||||||
scatterplotYYaccessor: null
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Controls;
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
import Crossfilter from "../util/typedCrossfilter";
|
|
||||||
import { World, ControlsHelpers } from "../util/stateManager";
|
|
||||||
import {
|
|
||||||
layoutDimensionName,
|
|
||||||
obsAnnoDimensionName,
|
|
||||||
userDefinedDimensionName,
|
|
||||||
diffexpDimensionName,
|
|
||||||
makeContinuousDimensionName
|
|
||||||
} from "../util/nameCreators";
|
|
||||||
|
|
||||||
const XYDimName = layoutDimensionName("XY");
|
|
||||||
|
|
||||||
const CrossfilterReducer = (
|
|
||||||
state = null,
|
|
||||||
action,
|
|
||||||
nextSharedState,
|
|
||||||
prevSharedState
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load complete (universe exists)": {
|
|
||||||
const { world, layoutChoice } = nextSharedState;
|
|
||||||
const crossfilter = World.createObsDimensions(
|
|
||||||
new Crossfilter(world.obsAnnotations),
|
|
||||||
world,
|
|
||||||
layoutChoice.currentDimNames
|
|
||||||
);
|
|
||||||
return crossfilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "reset World to eq Universe": {
|
|
||||||
const { userDefinedGenes, diffexpGenes } = prevSharedState.controls;
|
|
||||||
const { world } = nextSharedState;
|
|
||||||
const crossfilter = ControlsHelpers.createGeneDimensions(
|
|
||||||
userDefinedGenes,
|
|
||||||
diffexpGenes,
|
|
||||||
world,
|
|
||||||
prevSharedState.resetCache.crossfilter
|
|
||||||
);
|
|
||||||
return crossfilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "set clip quantiles":
|
|
||||||
case "set World to current selection": {
|
|
||||||
const { userDefinedGenes, diffexpGenes } = prevSharedState.controls;
|
|
||||||
const { world, layoutChoice } = nextSharedState;
|
|
||||||
let crossfilter = new Crossfilter(world.obsAnnotations);
|
|
||||||
crossfilter = World.createObsDimensions(
|
|
||||||
crossfilter,
|
|
||||||
world,
|
|
||||||
layoutChoice.currentDimNames
|
|
||||||
);
|
|
||||||
crossfilter = ControlsHelpers.createGeneDimensions(
|
|
||||||
userDefinedGenes,
|
|
||||||
diffexpGenes,
|
|
||||||
world,
|
|
||||||
crossfilter
|
|
||||||
);
|
|
||||||
return crossfilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "set layout choice": {
|
|
||||||
/*
|
|
||||||
when switching layouts:
|
|
||||||
- delete the existing XY index
|
|
||||||
- add the new XY index (which implicitly selects all on it)
|
|
||||||
*/
|
|
||||||
const { world, layoutChoice } = nextSharedState;
|
|
||||||
return state
|
|
||||||
.delDimension(layoutDimensionName("XY"))
|
|
||||||
.addDimension(
|
|
||||||
layoutDimensionName("XY"),
|
|
||||||
"spatial",
|
|
||||||
world.obsLayout.col(layoutChoice.currentDimNames[0]).asArray(),
|
|
||||||
world.obsLayout.col(layoutChoice.currentDimNames[1]).asArray()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "request user defined gene success": {
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
const gene = action.data.genes[0];
|
|
||||||
return state.addDimension(
|
|
||||||
userDefinedDimensionName(gene),
|
|
||||||
"scalar",
|
|
||||||
world.varData.col(gene).asArray(),
|
|
||||||
Float32Array
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "request differential expression success": {
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
|
||||||
const genes = _.map(action.data, d =>
|
|
||||||
world.varAnnotations.at(d[0], varIndexName)
|
|
||||||
);
|
|
||||||
const crossfilter = _.reduce(
|
|
||||||
genes,
|
|
||||||
(xfltr, gene) =>
|
|
||||||
xfltr.addDimension(
|
|
||||||
diffexpDimensionName(gene),
|
|
||||||
"scalar",
|
|
||||||
world.varData.col(gene).asArray(),
|
|
||||||
Float32Array
|
|
||||||
),
|
|
||||||
state
|
|
||||||
);
|
|
||||||
return crossfilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "clear differential expression": {
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
|
||||||
const crossfilter = _.reduce(
|
|
||||||
action.diffExp,
|
|
||||||
(xfltr, values) => {
|
|
||||||
const name = world.varAnnotations.at(values[0], varIndexName);
|
|
||||||
return xfltr.delDimension(diffexpDimensionName(name));
|
|
||||||
},
|
|
||||||
state
|
|
||||||
);
|
|
||||||
return crossfilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "clear user defined gene": {
|
|
||||||
return state.delDimension(userDefinedDimensionName(action.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
case "clear all user defined genes": {
|
|
||||||
const { userDefinedGenes } = prevSharedState.controls;
|
|
||||||
const crossfilter = _.reduce(
|
|
||||||
userDefinedGenes,
|
|
||||||
(xfltr, gene) => xfltr.delDimension(userDefinedDimensionName(gene)),
|
|
||||||
state
|
|
||||||
);
|
|
||||||
return crossfilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "graph brush end":
|
|
||||||
case "graph brush change": {
|
|
||||||
const [minX, maxY] = action.brushCoords.northwest;
|
|
||||||
const [maxX, minY] = action.brushCoords.southeast;
|
|
||||||
return state.select(XYDimName, {
|
|
||||||
mode: "within-rect",
|
|
||||||
minX,
|
|
||||||
minY,
|
|
||||||
maxX,
|
|
||||||
maxY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
case "graph lasso end": {
|
|
||||||
const { polygon } = action;
|
|
||||||
return state.select(XYDimName, {
|
|
||||||
mode: "within-polygon",
|
|
||||||
polygon
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
case "graph lasso cancel":
|
|
||||||
case "graph brush cancel":
|
|
||||||
case "graph lasso deselect":
|
|
||||||
case "graph brush deselect": {
|
|
||||||
return state.select(XYDimName, { mode: "all" });
|
|
||||||
}
|
|
||||||
|
|
||||||
case "continuous metadata histogram start":
|
|
||||||
case "continuous metadata histogram brush":
|
|
||||||
case "continuous metadata histogram cancel":
|
|
||||||
case "continuous metadata histogram end": {
|
|
||||||
const name = makeContinuousDimensionName(
|
|
||||||
action.continuousNamespace,
|
|
||||||
action.selection
|
|
||||||
);
|
|
||||||
// action.selection: metadata name being selected
|
|
||||||
// action.range: filter range, or null if deselected
|
|
||||||
if (!action.range) {
|
|
||||||
return state.select(name, { mode: "all" });
|
|
||||||
}
|
|
||||||
const [lo, hi] = action.range;
|
|
||||||
const newState = state.select(name, { mode: "range", lo, hi });
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "categorical metadata filter select":
|
|
||||||
case "categorical metadata filter deselect": {
|
|
||||||
const { categoricalSelection } = nextSharedState;
|
|
||||||
const { world } = prevSharedState;
|
|
||||||
const cat = categoricalSelection[action.metadataField];
|
|
||||||
const col = world.obsAnnotations.col(action.metadataField);
|
|
||||||
return state.select(obsAnnoDimensionName(action.metadataField), {
|
|
||||||
mode: "exact",
|
|
||||||
values: ControlsHelpers.selectedValuesForCategory(cat, col)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
case "categorical metadata filter none of these": {
|
|
||||||
return state.select(obsAnnoDimensionName(action.metadataField), {
|
|
||||||
mode: "none"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
case "categorical metadata filter all of these": {
|
|
||||||
return state.select(obsAnnoDimensionName(action.metadataField), {
|
|
||||||
mode: "all"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CrossfilterReducer;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
|
|
||||||
const Differential = (
|
|
||||||
state = {
|
|
||||||
diffExp: null,
|
|
||||||
loading: null,
|
|
||||||
error: null,
|
|
||||||
celllist1: null,
|
|
||||||
celllist2: null
|
|
||||||
},
|
|
||||||
action
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "request differential expression started":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: true,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
case "request differential expression success":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
error: null,
|
|
||||||
loading: false,
|
|
||||||
diffExp: action.data
|
|
||||||
};
|
|
||||||
case "request differential expression error":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
error: action.data
|
|
||||||
};
|
|
||||||
case "store current cell selection as differential set 1":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
celllist1: action.data
|
|
||||||
};
|
|
||||||
case "store current cell selection as differential set 2":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
celllist2: action.data
|
|
||||||
};
|
|
||||||
case "clear differential expression":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
diffExp: null,
|
|
||||||
celllist1: null,
|
|
||||||
celllist2: null
|
|
||||||
};
|
|
||||||
case "reset World to eq Universe":
|
|
||||||
case "set World to current selection":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
diffExp: null,
|
|
||||||
celllist1: null,
|
|
||||||
celllist2: null
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Differential;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
const GraphSelection = (
|
|
||||||
state = {
|
|
||||||
tool: "lasso", // what selection tool mode (lasso, brush, ...)
|
|
||||||
selection: { mode: "all" } // current selection, which is tool specific
|
|
||||||
},
|
|
||||||
action
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "set clip quantiles":
|
|
||||||
case "reset World to eq Universe":
|
|
||||||
case "set layout choice": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selection: {
|
|
||||||
mode: "all"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "graph brush end":
|
|
||||||
case "graph brush change": {
|
|
||||||
const { brushCoords } = action;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selection: {
|
|
||||||
mode: "within-rect",
|
|
||||||
brushCoords
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "graph lasso end": {
|
|
||||||
const { polygon } = action;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selection: {
|
|
||||||
mode: "within-polygon",
|
|
||||||
polygon
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case "graph lasso cancel":
|
|
||||||
case "graph brush cancel":
|
|
||||||
case "graph lasso deselect":
|
|
||||||
case "graph brush deselect": {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selection: {
|
|
||||||
mode: "all"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GraphSelection;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { createStore, applyMiddleware } from "redux";
|
|
||||||
import thunk from "redux-thunk";
|
|
||||||
|
|
||||||
import cascadeReducers from "./cascade";
|
|
||||||
import undoable from "./undoable";
|
|
||||||
import config from "./config";
|
|
||||||
import universe from "./universe";
|
|
||||||
import world from "./world";
|
|
||||||
import categoricalSelection from "./categoricalSelection";
|
|
||||||
import continuousSelection from "./continuousSelection";
|
|
||||||
import graphSelection from "./graphSelection";
|
|
||||||
import crossfilter from "./crossfilter";
|
|
||||||
import colors from "./colors";
|
|
||||||
import differential from "./differential";
|
|
||||||
import layoutChoice from "./layoutChoice";
|
|
||||||
import responsive from "./responsive";
|
|
||||||
import controls from "./controls";
|
|
||||||
import resetCache from "./resetCache";
|
|
||||||
import centroidLabel from "./centroidLabel";
|
|
||||||
|
|
||||||
import undoableConfig from "./undoableConfig";
|
|
||||||
|
|
||||||
const Reducer = undoable(
|
|
||||||
cascadeReducers([
|
|
||||||
["config", config],
|
|
||||||
["universe", universe],
|
|
||||||
["world", world],
|
|
||||||
["layoutChoice", layoutChoice],
|
|
||||||
["categoricalSelection", categoricalSelection],
|
|
||||||
["continuousSelection", continuousSelection],
|
|
||||||
["graphSelection", graphSelection],
|
|
||||||
["crossfilter", crossfilter],
|
|
||||||
["colors", colors],
|
|
||||||
["controls", controls],
|
|
||||||
["differential", differential],
|
|
||||||
["responsive", responsive],
|
|
||||||
["centroidLabel", centroidLabel],
|
|
||||||
["resetCache", resetCache]
|
|
||||||
]),
|
|
||||||
[
|
|
||||||
"world",
|
|
||||||
"categoricalSelection",
|
|
||||||
"continuousSelection",
|
|
||||||
"graphSelection",
|
|
||||||
"crossfilter",
|
|
||||||
"colors",
|
|
||||||
"controls",
|
|
||||||
"differential",
|
|
||||||
"layoutChoice"
|
|
||||||
],
|
|
||||||
undoableConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
const store = createStore(Reducer, applyMiddleware(thunk));
|
|
||||||
|
|
||||||
export default store;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
we have a UI heuristic to pick the default layout, based on assumptions
|
|
||||||
about commonly used names. Preferentially, pick in the following order:
|
|
||||||
|
|
||||||
1. "umap"
|
|
||||||
2. "tsne"
|
|
||||||
3. "pca"
|
|
||||||
4. give up, use the first available
|
|
||||||
*/
|
|
||||||
function bestDefaultLayout(layouts) {
|
|
||||||
const preferredNames = ["umap", "tsne", "pca"];
|
|
||||||
const idx = preferredNames.findIndex(name => layouts.indexOf(name) !== -1);
|
|
||||||
if (idx !== -1) return preferredNames[idx];
|
|
||||||
return layouts[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const LayoutChoice = (
|
|
||||||
state = {
|
|
||||||
available: [], // all available choices
|
|
||||||
current: undefined, // name of the current layout, eg, 'umap'
|
|
||||||
currentDimNames: [] // dimension name
|
|
||||||
},
|
|
||||||
action,
|
|
||||||
nextSharedState
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load complete (universe exists)":
|
|
||||||
case "reset World to eq Universe": {
|
|
||||||
// set default to default
|
|
||||||
const { schema } = nextSharedState.world;
|
|
||||||
const available = schema.layout.obs.map(v => v.name);
|
|
||||||
const current = bestDefaultLayout(available);
|
|
||||||
const currentDimNames = schema.layout.obsByName[current].dims;
|
|
||||||
return { available, current, currentDimNames };
|
|
||||||
}
|
|
||||||
|
|
||||||
case "set layout choice": {
|
|
||||||
const { schema } = nextSharedState.world;
|
|
||||||
const current = action.layoutChoice;
|
|
||||||
const currentDimNames = schema.layout.obsByName[current].dims;
|
|
||||||
return { ...state, current, currentDimNames };
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LayoutChoice;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
Reducer which caches derived state to be used in a reset or other
|
|
||||||
recomputation.
|
|
||||||
|
|
||||||
Currently this only caches the baseline (full universe) world & crossfilter,
|
|
||||||
for use in a Reset.
|
|
||||||
*/
|
|
||||||
const ResetCacheReducer = (
|
|
||||||
state = {
|
|
||||||
world: null,
|
|
||||||
crossfilter: null
|
|
||||||
},
|
|
||||||
action,
|
|
||||||
nextSharedState
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load complete (universe exists)": {
|
|
||||||
const { world, crossfilter } = nextSharedState;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
world,
|
|
||||||
crossfilter
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ResetCacheReducer;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
const Responsive = (
|
|
||||||
state = {
|
|
||||||
width: null,
|
|
||||||
height: null
|
|
||||||
},
|
|
||||||
action
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "window resize":
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
width: action.data.width,
|
|
||||||
height: action.data.height
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Responsive;
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
/*
|
|
||||||
A redo/undo meta reducer for Redux. Designed to work well with the cascadeReducer().
|
|
||||||
|
|
||||||
Requires three parameters:
|
|
||||||
* reducer - a reducer, which MUST return an object as state.
|
|
||||||
* undoableKeys - an array of object keys (strings). If any of these keys
|
|
||||||
are in the object/state returned by the reducer, they will be treated as
|
|
||||||
state to be made "undoable".
|
|
||||||
* options - an optional object, which may contain the following parameters:
|
|
||||||
* historyLimit: max number of historical states to remember (aka max undo depth)
|
|
||||||
* actionFilter: filter function, (state, action, filterState) => value.
|
|
||||||
See below for details.
|
|
||||||
* debug: if truish, will print helpful log messages about history manipulation
|
|
||||||
|
|
||||||
This meta reducer accepts three actions types:
|
|
||||||
* @@undoable/undo - move back in history
|
|
||||||
* @@undoable/redo - move forward in history
|
|
||||||
* @@undoable/clear - clear history
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Action filter - controls the undoable reducer side-effects. If not
|
|
||||||
specified, the action filter defaults to "save", ie, pushes a redo
|
|
||||||
point upon each action.
|
|
||||||
|
|
||||||
The action filter callback has access to the current action, the entire
|
|
||||||
undoable reducer state, and any state it wants to manage ("filterState").
|
|
||||||
This filter state will be passed to each action filter call, and any
|
|
||||||
value returned (via @@undoable/filterState field described below) will be
|
|
||||||
MERGED into the current filter state.
|
|
||||||
|
|
||||||
An object must be returned (the "undoable action"), indicating desired
|
|
||||||
history state processing. The undoable action object contents, by key:
|
|
||||||
|
|
||||||
@@undoable/filterAction: required. Can be one of:
|
|
||||||
"skip" - reduce the current action, but no other side effects.
|
|
||||||
Same as returning false.
|
|
||||||
"clear" - reduce the current action, and clear history state.
|
|
||||||
"save" - push the previous state onto the history stack (ie,
|
|
||||||
before reducing the action)
|
|
||||||
"stashPending" - reduce action, save state as pending. Does not
|
|
||||||
not commit it to history. Along with cancelPending and applyPending,
|
|
||||||
can be used to delay commit of history (eg, for multi-action
|
|
||||||
groupings, asynch operations, etc).
|
|
||||||
"cancelPending" - reduce action, cancel any pending state save.
|
|
||||||
"applyPending" - commit any pending state to the history stack,
|
|
||||||
then reduce action.
|
|
||||||
|
|
||||||
@@undoable/filterState: optional. If this value is set, it will be
|
|
||||||
MERGED into the current filter state. The value and semantics of any
|
|
||||||
filter state are entirely at the discretion of the action filter.
|
|
||||||
|
|
||||||
*/
|
|
||||||
import fromEntries from "../util/fromEntries";
|
|
||||||
|
|
||||||
const historyKeyPrefix = "@@undoable/";
|
|
||||||
const pastKey = `${historyKeyPrefix}past`;
|
|
||||||
const futureKey = `${historyKeyPrefix}future`;
|
|
||||||
const filterStateKey = `${historyKeyPrefix}filterState`;
|
|
||||||
const filterActionKey = `${historyKeyPrefix}filterAction`;
|
|
||||||
const pendingKey = `${historyKeyPrefix}pending`;
|
|
||||||
const defaultHistoryLimit = -100;
|
|
||||||
|
|
||||||
const Undoable = (reducer, undoableKeys, options = {}) => {
|
|
||||||
const { debug } = options;
|
|
||||||
let { historyLimit } = options;
|
|
||||||
if (!historyLimit) historyLimit = defaultHistoryLimit;
|
|
||||||
if (historyLimit > 0) historyLimit = -historyLimit;
|
|
||||||
const actionFilter =
|
|
||||||
options.actionFilter || (() => ({ [filterActionKey]: "save" }));
|
|
||||||
|
|
||||||
if (!Array.isArray(undoableKeys) || undoableKeys.length === 0)
|
|
||||||
throw new Error("undoable keys array must be specified");
|
|
||||||
const undoableKeysSet = new Set(undoableKeys);
|
|
||||||
|
|
||||||
/*
|
|
||||||
Undo the current to previous history
|
|
||||||
*/
|
|
||||||
function undo(currentState) {
|
|
||||||
const past = currentState[pastKey];
|
|
||||||
const future = currentState[futureKey];
|
|
||||||
if (past.length === 0) return currentState;
|
|
||||||
const currentUndoableState = Object.entries(currentState).filter(kv =>
|
|
||||||
undoableKeysSet.has(kv[0])
|
|
||||||
);
|
|
||||||
const newPast = [...past];
|
|
||||||
const newState = newPast.pop();
|
|
||||||
const newFuture = push(future, currentUndoableState);
|
|
||||||
const nextState = {
|
|
||||||
...currentState,
|
|
||||||
...fromEntries(newState),
|
|
||||||
[pastKey]: newPast,
|
|
||||||
[futureKey]: newFuture,
|
|
||||||
[pendingKey]: null
|
|
||||||
};
|
|
||||||
return nextState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Replay future, previously undone.
|
|
||||||
*/
|
|
||||||
function redo(currentState) {
|
|
||||||
const past = currentState[pastKey] || [];
|
|
||||||
const future = currentState[futureKey] || [];
|
|
||||||
if (future.length === 0) return currentState;
|
|
||||||
const currentUndoableState = Object.entries(currentState).filter(kv =>
|
|
||||||
undoableKeysSet.has(kv[0])
|
|
||||||
);
|
|
||||||
const newFuture = [...future];
|
|
||||||
const newState = newFuture.pop();
|
|
||||||
const newPast = push(past, currentUndoableState);
|
|
||||||
const nextState = {
|
|
||||||
...currentState,
|
|
||||||
...fromEntries(newState),
|
|
||||||
[pastKey]: newPast,
|
|
||||||
[futureKey]: newFuture,
|
|
||||||
[pendingKey]: null
|
|
||||||
};
|
|
||||||
return nextState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Clear the history state. No side-effects on current state.
|
|
||||||
*/
|
|
||||||
function clear(currentState) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
[pastKey]: [],
|
|
||||||
[futureKey]: [],
|
|
||||||
[filterStateKey]: {},
|
|
||||||
[pendingKey]: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Reduce current action, with no history side-effects
|
|
||||||
*/
|
|
||||||
function skip(currentState, action, filterState) {
|
|
||||||
const past = currentState[pastKey] || [];
|
|
||||||
const pending = currentState[pendingKey];
|
|
||||||
const res = reducer(currentState, action);
|
|
||||||
return {
|
|
||||||
...res,
|
|
||||||
[pastKey]: past,
|
|
||||||
[futureKey]: [],
|
|
||||||
[filterStateKey]: filterState,
|
|
||||||
[pendingKey]: pending
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Save current state in the history, then reduce action.
|
|
||||||
*/
|
|
||||||
function save(currentState, action, filterState) {
|
|
||||||
const past = currentState[pastKey] || [];
|
|
||||||
const currentUndoableState = Object.entries(currentState).filter(kv =>
|
|
||||||
undoableKeysSet.has(kv[0])
|
|
||||||
);
|
|
||||||
const res = reducer(currentState, action);
|
|
||||||
const newPast = push(past, currentUndoableState, historyLimit);
|
|
||||||
const nextState = {
|
|
||||||
...res,
|
|
||||||
[pastKey]: newPast,
|
|
||||||
[futureKey]: [],
|
|
||||||
[filterStateKey]: filterState,
|
|
||||||
[pendingKey]: null
|
|
||||||
};
|
|
||||||
return nextState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Save current state as pending history change. No other side effects.
|
|
||||||
*/
|
|
||||||
function stashPending(currentState) {
|
|
||||||
const currentUndoableState = Object.entries(currentState).filter(kv =>
|
|
||||||
undoableKeysSet.has(kv[0])
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
[pendingKey]: currentUndoableState
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Cancel pending history state change. No other side effects.
|
|
||||||
*/
|
|
||||||
function cancelPending(currentState) {
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
[pendingKey]: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Push pending state onto the history stack
|
|
||||||
*/
|
|
||||||
function applyPending(currentState) {
|
|
||||||
const past = currentState[pastKey] || [];
|
|
||||||
const pendingState = currentState[pendingKey];
|
|
||||||
const newPast = push(past, pendingState, historyLimit);
|
|
||||||
const nextState = {
|
|
||||||
...currentState,
|
|
||||||
[pastKey]: newPast,
|
|
||||||
[futureKey]: [],
|
|
||||||
[pendingKey]: null
|
|
||||||
};
|
|
||||||
return nextState;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
currentState = {
|
|
||||||
[pastKey]: [],
|
|
||||||
[futureKey]: [],
|
|
||||||
[filterStateKey]: {},
|
|
||||||
[pendingKey]: null
|
|
||||||
},
|
|
||||||
action
|
|
||||||
) => {
|
|
||||||
const aType = action.type;
|
|
||||||
switch (aType) {
|
|
||||||
case "@@undoable/undo": {
|
|
||||||
return undo(currentState, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "@@undoable/redo": {
|
|
||||||
return redo(currentState, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "@@undoable/clear": {
|
|
||||||
return clear(currentState, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
const currentFilterState = currentState[filterStateKey];
|
|
||||||
const actionFilterResp = actionFilter(
|
|
||||||
currentState,
|
|
||||||
action,
|
|
||||||
currentFilterState
|
|
||||||
);
|
|
||||||
const {
|
|
||||||
[filterActionKey]: filterAction,
|
|
||||||
[filterStateKey]: filterStateUpdate
|
|
||||||
} = actionFilterResp;
|
|
||||||
const nextFilterState = { ...currentFilterState, ...filterStateUpdate };
|
|
||||||
|
|
||||||
switch (filterAction) {
|
|
||||||
case "clear":
|
|
||||||
if (debug) console.log("---- CLEAR HISTO", action.type);
|
|
||||||
return clear(skip(currentState, action, nextFilterState));
|
|
||||||
|
|
||||||
case "save":
|
|
||||||
if (debug) console.log("---- SAVE HISTO", action.type);
|
|
||||||
return save(currentState, action, nextFilterState);
|
|
||||||
|
|
||||||
case "stashPending":
|
|
||||||
if (debug) console.log("---- STASH PENDING", action.type);
|
|
||||||
return skip(stashPending(currentState), action, nextFilterState);
|
|
||||||
|
|
||||||
case "cancelPending":
|
|
||||||
if (debug) console.log("---- CANCEL PENDING", action.type);
|
|
||||||
return skip(cancelPending(currentState), action, nextFilterState);
|
|
||||||
|
|
||||||
case "applyPending":
|
|
||||||
if (debug) console.log("---- APPLY PENDING", action.type);
|
|
||||||
return skip(applyPending(currentState), action, nextFilterState);
|
|
||||||
|
|
||||||
case "skip":
|
|
||||||
default:
|
|
||||||
return skip(currentState, action, nextFilterState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function push(arr, val, limit = undefined) {
|
|
||||||
/*
|
|
||||||
functional array push, with a max length limit to the new array.
|
|
||||||
Like Array.push, except it returns new array and discards as needed
|
|
||||||
to enforce the length limit.
|
|
||||||
*/
|
|
||||||
const narr = arr.slice(limit);
|
|
||||||
narr.push(val);
|
|
||||||
return narr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Undoable;
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import StateMachine from "../util/statemachine";
|
|
||||||
import createFsmTransitions from "./undoableFsm";
|
|
||||||
|
|
||||||
const actionKey = "@@undoable/filterAction";
|
|
||||||
const stateKey = "@@undoable/filterState";
|
|
||||||
|
|
||||||
/*
|
|
||||||
these actions will not affect history
|
|
||||||
*/
|
|
||||||
const skipOnActions = new Set([
|
|
||||||
"url changed",
|
|
||||||
"interface reset started",
|
|
||||||
"initial data load start",
|
|
||||||
"configuration load complete",
|
|
||||||
"increment graph render counter",
|
|
||||||
"window resize",
|
|
||||||
"user reset start",
|
|
||||||
"reset colorscale",
|
|
||||||
|
|
||||||
"graph brush change",
|
|
||||||
"continuous metadata histogram brush",
|
|
||||||
|
|
||||||
"expression load start",
|
|
||||||
"expression load success",
|
|
||||||
"expression load error",
|
|
||||||
|
|
||||||
"request user defined gene started",
|
|
||||||
"request user defined gene success",
|
|
||||||
"clear all user defined genes",
|
|
||||||
|
|
||||||
"get single gene expression for coloring started",
|
|
||||||
"get single gene expression for coloring error",
|
|
||||||
|
|
||||||
"category value mouse hover start",
|
|
||||||
"category value mouse hover end"
|
|
||||||
]);
|
|
||||||
|
|
||||||
/*
|
|
||||||
identical, repeated occurances of these action types will be debounced.
|
|
||||||
Entire action must be identical (all keys).
|
|
||||||
*/
|
|
||||||
const debounceOnActions = new Set([
|
|
||||||
"color by categorical metadata",
|
|
||||||
"color by continuous metadata",
|
|
||||||
"color by expression"
|
|
||||||
]);
|
|
||||||
|
|
||||||
/*
|
|
||||||
history will be cleared when these actions occur
|
|
||||||
*/
|
|
||||||
const clearOnActions = new Set([
|
|
||||||
"initial data load complete (universe exists)",
|
|
||||||
"reset World to eq Universe",
|
|
||||||
"initial data load error",
|
|
||||||
"user reset end"
|
|
||||||
]);
|
|
||||||
|
|
||||||
/*
|
|
||||||
An immediate history save will be done for these
|
|
||||||
*/
|
|
||||||
const saveOnActions = new Set([
|
|
||||||
"categorical metadata filter select",
|
|
||||||
"categorical metadata filter deselect",
|
|
||||||
"categorical metadata filter all of these",
|
|
||||||
"categorical metadata filter none of these",
|
|
||||||
|
|
||||||
"color by categorical metadata",
|
|
||||||
"color by continuous metadata",
|
|
||||||
"color by expression",
|
|
||||||
|
|
||||||
"set scatterplot x",
|
|
||||||
"set scatterplot y",
|
|
||||||
|
|
||||||
"store current cell selection as differential set 1",
|
|
||||||
"store current cell selection as differential set 2",
|
|
||||||
|
|
||||||
"set World to current selection",
|
|
||||||
"set clip quantiles",
|
|
||||||
|
|
||||||
"set layout choice",
|
|
||||||
"change graph interaction mode"
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
StateMachine - processing complex action handling - see FSM graph for
|
|
||||||
actual structure, in undoableFsm.js
|
|
||||||
**/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Default FSM actions. Used to side-effect transitions in the graph.
|
|
||||||
See graph definition for the transitions that use each.
|
|
||||||
|
|
||||||
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
|
|
||||||
*/
|
|
||||||
const stashPending = fsm => ({
|
|
||||||
[actionKey]: "stashPending",
|
|
||||||
[stateKey]: { fsm }
|
|
||||||
});
|
|
||||||
const cancelPending = () => ({
|
|
||||||
[actionKey]: "cancelPending",
|
|
||||||
[stateKey]: { fsm: null }
|
|
||||||
});
|
|
||||||
const applyPending = () => ({
|
|
||||||
[actionKey]: "applyPending",
|
|
||||||
[stateKey]: { fsm: null }
|
|
||||||
});
|
|
||||||
const skip = (fsm, transition) => ({
|
|
||||||
[actionKey]: "skip",
|
|
||||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null }
|
|
||||||
});
|
|
||||||
const clear = () => ({ [actionKey]: "clear", [stateKey]: { fsm: null } });
|
|
||||||
const save = (fsm, transition) => ({
|
|
||||||
[actionKey]: "save",
|
|
||||||
[stateKey]: { fsm: transition.to !== "done" ? fsm : null }
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
Error handler for state transitions that are unexpected. Called by
|
|
||||||
StateMachine when it doesn't know what to do.
|
|
||||||
|
|
||||||
Signature: (fsm, event, from) => undoableAction
|
|
||||||
*/
|
|
||||||
const onFsmError = (fsm, event, from) => {
|
|
||||||
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
|
|
||||||
// In production, try to recover gracefully if we have unexpected state
|
|
||||||
return clear(fsm);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Definition of the transition graph mapping action types to history side effects.
|
|
||||||
*/
|
|
||||||
const fsmTransitions = createFsmTransitions(
|
|
||||||
stashPending,
|
|
||||||
cancelPending,
|
|
||||||
applyPending,
|
|
||||||
skip,
|
|
||||||
clear,
|
|
||||||
save
|
|
||||||
);
|
|
||||||
/* State machine we clone whenever we need to run it */
|
|
||||||
const seedFsm = new StateMachine("init", fsmTransitions, onFsmError);
|
|
||||||
|
|
||||||
/*
|
|
||||||
See undoable.js for description action filter interface description.
|
|
||||||
|
|
||||||
Basic approach:
|
|
||||||
* trivial handlers for skip, clear & save cases to keep config simple.
|
|
||||||
* only implement complex state machines where absolutely required (eg,
|
|
||||||
multi-event seleciton and the like)
|
|
||||||
*/
|
|
||||||
const actionFilter = debug => (state, action, prevFilterState) => {
|
|
||||||
const actionType = action.type;
|
|
||||||
const filterState = {
|
|
||||||
...prevFilterState,
|
|
||||||
prevAction: action
|
|
||||||
};
|
|
||||||
if (skipOnActions.has(actionType)) {
|
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
debounceOnActions.has(actionType) &&
|
|
||||||
shallowObjectEq(action, prevFilterState.prevAction)
|
|
||||||
) {
|
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
|
||||||
}
|
|
||||||
if (clearOnActions.has(actionType)) {
|
|
||||||
return { [actionKey]: "clear", [stateKey]: filterState };
|
|
||||||
}
|
|
||||||
if (saveOnActions.has(actionType)) {
|
|
||||||
return { [actionKey]: "save", [stateKey]: filterState };
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Else, something more complex OR unknown to us....
|
|
||||||
*/
|
|
||||||
if (seedFsm.events.has(actionType)) {
|
|
||||||
let { fsm } = filterState;
|
|
||||||
if (!fsm) {
|
|
||||||
/* no active FSM, so create one in init state */
|
|
||||||
fsm = seedFsm.clone("init");
|
|
||||||
}
|
|
||||||
return fsm.next(action.type, { state, action });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* else, we have no idea what this is - skip it */
|
|
||||||
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
|
||||||
return { [actionKey]: "skip", [stateKey]: filterState };
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
return true if objA and objB are ===, OR if:
|
|
||||||
- are both objects and not null
|
|
||||||
- have same own properties
|
|
||||||
- all values are strict equal (===)
|
|
||||||
*/
|
|
||||||
function shallowObjectEq(objA, objB) {
|
|
||||||
if (objA === objB) return true;
|
|
||||||
if (!objA || !objB) return false;
|
|
||||||
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
|
|
||||||
if (!shallowArrayEq(Object.values(objA), Object.values(objB))) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
return true if arrA and arrB contain the same strict-equal values,
|
|
||||||
in the same order.
|
|
||||||
*/
|
|
||||||
function shallowArrayEq(arrA, arrB) {
|
|
||||||
if (arrA.length !== arrB.length) return false;
|
|
||||||
for (let i = 0, l = arrA.length; i < l; i += 1) {
|
|
||||||
if (arrA[i] !== arrB[i]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* configuration for the undoable meta reducer */
|
|
||||||
const debug = false; // set truish for undoble debugging
|
|
||||||
const undoableConfig = {
|
|
||||||
debug,
|
|
||||||
historyLimit: 50, // maximum history size
|
|
||||||
actionFilter: actionFilter(debug)
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
this code is strictly for sanity checking configuration, and is only
|
|
||||||
enabled when we are debugging the undoable configuration (ie, debug === true).
|
|
||||||
*/
|
|
||||||
if (debug) {
|
|
||||||
/*
|
|
||||||
Confirm no intersection between the various trivial rejection action filters
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
new Set([...skipOnActions].filter(x => clearOnActions.has(x))).size > 0 ||
|
|
||||||
new Set([...skipOnActions].filter(x => saveOnActions.has(x))).size > 0 ||
|
|
||||||
new Set([...clearOnActions].filter(x => saveOnActions.has(x))).size > 0
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
"Undoable misconfiguration - action filters have redundant events"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Confirm that no FSM events are blocked by a trivial rejection filter.
|
|
||||||
If this occurs, the FSM can't ever see the events needed to process
|
|
||||||
state transitions.
|
|
||||||
*/
|
|
||||||
const trivialFilters = new Set([
|
|
||||||
...skipOnActions,
|
|
||||||
...clearOnActions,
|
|
||||||
...saveOnActions
|
|
||||||
]);
|
|
||||||
const trivialOverlapWithFsm = new Set(
|
|
||||||
[...trivialFilters].filter(x => seedFsm.events.has(x))
|
|
||||||
);
|
|
||||||
if (trivialOverlapWithFsm.size > 0) {
|
|
||||||
console.error(
|
|
||||||
"Undoable misconfiguration - trivival action filter blocking FSM filter",
|
|
||||||
[...trivialOverlapWithFsm]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default undoableConfig;
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
/*
|
|
||||||
State transition graph for complex action/history interactions.
|
|
||||||
|
|
||||||
Assumed configuration from undoableConfig:
|
|
||||||
* By convention, "init" is used as the start state for all, and "done"
|
|
||||||
as the final state.
|
|
||||||
* Unexpected states will result in an error, plus a clear and cancelPending
|
|
||||||
side-effect.
|
|
||||||
|
|
||||||
TODO: is is possible there is a more concise format for this, as it is
|
|
||||||
a fairly repetitive pattern.
|
|
||||||
|
|
||||||
These events are largely one of two types:
|
|
||||||
a) async operations or multi-event options that should only be committed
|
|
||||||
upon some success criteria, otherwise cancelled.
|
|
||||||
|
|
||||||
b) compound actions that should be collapsed into a single history change.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const createFsmTransitions = (
|
|
||||||
stashPending,
|
|
||||||
cancelPending,
|
|
||||||
applyPending,
|
|
||||||
skip,
|
|
||||||
clear,
|
|
||||||
save
|
|
||||||
) => {
|
|
||||||
return [
|
|
||||||
/* graph selection brushing */
|
|
||||||
{
|
|
||||||
event: "graph brush start",
|
|
||||||
from: "init",
|
|
||||||
to: "graph brush in progress",
|
|
||||||
action: stashPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "graph brush cancel",
|
|
||||||
from: "graph brush in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "graph brush deselect",
|
|
||||||
from: "graph brush in progress",
|
|
||||||
to: "done",
|
|
||||||
/* if current selection is all, cancelPending. Else, applyPending */
|
|
||||||
action: (fsm, transition, data) =>
|
|
||||||
data.state.graphSelection.selection.mode === "all"
|
|
||||||
? cancelPending()
|
|
||||||
: applyPending()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "graph brush end",
|
|
||||||
from: "graph brush in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
|
|
||||||
/* graph selection lasso */
|
|
||||||
{
|
|
||||||
event: "graph lasso start",
|
|
||||||
from: "init",
|
|
||||||
to: "graph lasso in progress",
|
|
||||||
action: stashPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "graph lasso cancel",
|
|
||||||
from: "graph lasso in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "graph lasso deselect",
|
|
||||||
from: "graph lasso in progress",
|
|
||||||
to: "done",
|
|
||||||
/* if current selection is all, cancelPending. Else, applyPending */
|
|
||||||
action: (fsm, transition, data) =>
|
|
||||||
data.state.graphSelection.selection.mode === "all"
|
|
||||||
? cancelPending()
|
|
||||||
: applyPending()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "graph lasso end",
|
|
||||||
from: "graph lasso in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Continuous metadata histogram brush selection */
|
|
||||||
{
|
|
||||||
event: "continuous metadata histogram start",
|
|
||||||
from: "init",
|
|
||||||
to: "continuous histo select in progress",
|
|
||||||
action: stashPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "continuous metadata histogram cancel",
|
|
||||||
from: "continuous histo select in progress",
|
|
||||||
to: "done",
|
|
||||||
action: cancelPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "continuous metadata histogram end",
|
|
||||||
from: "continuous histo select in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Single gene request by user */
|
|
||||||
{
|
|
||||||
event: "single user defined gene start",
|
|
||||||
from: "init",
|
|
||||||
to: "single user gene request in progress",
|
|
||||||
action: stashPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "request user defined gene error",
|
|
||||||
from: "single user gene request in progress",
|
|
||||||
to: "single user gene error in progress",
|
|
||||||
action: skip
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "single user defined gene error",
|
|
||||||
from: "single user gene error in progress",
|
|
||||||
to: "done",
|
|
||||||
action: cancelPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "single user defined gene complete",
|
|
||||||
from: "single user gene request in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Bulk gene request by user */
|
|
||||||
{
|
|
||||||
event: "bulk user defined gene start",
|
|
||||||
from: "init",
|
|
||||||
to: "bulk user gene request in progress",
|
|
||||||
action: stashPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "request user defined gene error",
|
|
||||||
from: "bulk user gene request in progress",
|
|
||||||
to: "bulk user gene request error in progress",
|
|
||||||
action: skip
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "bulk user defined gene error",
|
|
||||||
from: "bulk user gene request error in progress",
|
|
||||||
to: "done",
|
|
||||||
action: cancelPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "bulk user defined gene complete",
|
|
||||||
from: "bulk user gene request in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Compute Differential Expression button user action */
|
|
||||||
{
|
|
||||||
event: "request differential expression started",
|
|
||||||
from: "init",
|
|
||||||
to: "diffexp in progress",
|
|
||||||
action: stashPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "request user defined gene error",
|
|
||||||
from: "diffexp in progress",
|
|
||||||
to: "done",
|
|
||||||
action: cancelPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "request differential expression success",
|
|
||||||
from: "diffexp in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Clear Differential Expression button user action */
|
|
||||||
{
|
|
||||||
event: "clear differential expression",
|
|
||||||
from: "init",
|
|
||||||
to: "CDE Button in progress",
|
|
||||||
action: stashPending
|
|
||||||
},
|
|
||||||
{
|
|
||||||
event: "clear scatterplot",
|
|
||||||
from: "CDE Button in progress",
|
|
||||||
to: "done",
|
|
||||||
action: applyPending
|
|
||||||
},
|
|
||||||
|
|
||||||
/* clear scatter plot button (eg, on scatterplot view) */
|
|
||||||
{
|
|
||||||
event: "clear scatterplot",
|
|
||||||
from: "init",
|
|
||||||
to: "done",
|
|
||||||
action: save
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createFsmTransitions;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { ControlsHelpers } from "../util/stateManager";
|
|
||||||
|
|
||||||
const Universe = (state = null, action, nextSharedState, prevSharedState) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load complete (universe exists)": {
|
|
||||||
const { universe } = action;
|
|
||||||
return universe;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "expression load success": {
|
|
||||||
let { varData } = state;
|
|
||||||
|
|
||||||
// Lazy load new expression data into the varData dataframe, if
|
|
||||||
// not already present.
|
|
||||||
//
|
|
||||||
Object.entries(action.expressionData).forEach(([key, val]) => {
|
|
||||||
// If not already in universe.varData, save entire expression column
|
|
||||||
if (!varData.hasCol(key)) {
|
|
||||||
varData = varData.withCol(key, val);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prune size of varData "cache" if getting out of hand....
|
|
||||||
//
|
|
||||||
const { userDefinedGenes, diffexpGenes } = prevSharedState;
|
|
||||||
const allTheGenesWeNeed = [
|
|
||||||
...new Set(
|
|
||||||
userDefinedGenes,
|
|
||||||
diffexpGenes,
|
|
||||||
Object.keys(action.expressionData)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
varData = ControlsHelpers.pruneVarDataCache(varData, allTheGenesWeNeed);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
varData
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Universe;
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import { World, ControlsHelpers } from "../util/stateManager";
|
|
||||||
import clip from "../util/clip";
|
|
||||||
import quantile from "../util/quantile";
|
|
||||||
|
|
||||||
const WorldReducer = (
|
|
||||||
state = null,
|
|
||||||
action,
|
|
||||||
nextSharedState,
|
|
||||||
prevSharedState
|
|
||||||
) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "initial data load complete (universe exists)": {
|
|
||||||
const { universe } = nextSharedState;
|
|
||||||
const world = World.createWorldFromEntireUniverse(universe);
|
|
||||||
return world;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "reset World to eq Universe": {
|
|
||||||
return prevSharedState.resetCache.world;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "set World to current selection": {
|
|
||||||
/* Set viewable world to be the currently selected data */
|
|
||||||
const world = World.createWorldBySelection(
|
|
||||||
action.universe,
|
|
||||||
action.world,
|
|
||||||
action.crossfilter
|
|
||||||
);
|
|
||||||
return world;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "set clip quantiles": {
|
|
||||||
const world = World.createWorldWithNewClip(
|
|
||||||
prevSharedState.universe,
|
|
||||||
state,
|
|
||||||
prevSharedState.crossfilter,
|
|
||||||
action.clipQuantiles
|
|
||||||
);
|
|
||||||
return world;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "expression load success": {
|
|
||||||
const { universe } = nextSharedState;
|
|
||||||
const universeVarData = universe.varData;
|
|
||||||
let unclippedVarData = state.unclipped.varData;
|
|
||||||
|
|
||||||
// Lazy load new expression data into the unclipped varData dataframe, if
|
|
||||||
// not already present.
|
|
||||||
//
|
|
||||||
Object.entries(action.expressionData).forEach(([key, val]) => {
|
|
||||||
// If not already in world.varData, save sliced expression column
|
|
||||||
if (!unclippedVarData.hasCol(key)) {
|
|
||||||
// Slice if world !== universe, else just use whole column.
|
|
||||||
// Use the obsAnnotation index as the cut key, as we keep
|
|
||||||
// all world dataframes in sync.
|
|
||||||
let worldValSlice = val;
|
|
||||||
if (!World.worldEqUniverse(state, universe)) {
|
|
||||||
worldValSlice = universeVarData
|
|
||||||
.subset(state.obsAnnotations.rowIndex.keys(), [key], null)
|
|
||||||
.icol(0)
|
|
||||||
.asArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now build world's varData dataframe
|
|
||||||
unclippedVarData = unclippedVarData.withCol(
|
|
||||||
key,
|
|
||||||
worldValSlice,
|
|
||||||
state.obsAnnotations.rowIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prune size of varData unclipped dataframe if getting out of hand....
|
|
||||||
//
|
|
||||||
const { userDefinedGenes, diffexpGenes } = prevSharedState;
|
|
||||||
const allTheGenesWeNeed = [
|
|
||||||
...new Set(
|
|
||||||
userDefinedGenes,
|
|
||||||
diffexpGenes,
|
|
||||||
Object.keys(action.expressionData)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
unclippedVarData = ControlsHelpers.pruneVarDataCache(
|
|
||||||
unclippedVarData,
|
|
||||||
allTheGenesWeNeed
|
|
||||||
);
|
|
||||||
|
|
||||||
// at this point, we have the unclipped data in unclippedVarData.
|
|
||||||
// Now create clipped.
|
|
||||||
// - Drop columns no longer needed
|
|
||||||
// - Add new columns
|
|
||||||
//
|
|
||||||
let clippedVarData = state.varData;
|
|
||||||
const keysToDrop = clippedVarData.colIndex
|
|
||||||
.keys()
|
|
||||||
.filter(k => !unclippedVarData.hasCol(k));
|
|
||||||
const keysToAdd = unclippedVarData.colIndex
|
|
||||||
.keys()
|
|
||||||
.filter(k => !clippedVarData.hasCol(k));
|
|
||||||
keysToDrop.forEach(k => {
|
|
||||||
clippedVarData = clippedVarData.dropCol(k);
|
|
||||||
});
|
|
||||||
keysToAdd.forEach(k => {
|
|
||||||
const data = unclippedVarData.col(k).asArray();
|
|
||||||
const q = [state.clipQuantiles.min, state.clipQuantiles.max];
|
|
||||||
const [qMinVal, qMaxVal] = quantile(q, data);
|
|
||||||
const clippedData = clip(data, qMinVal, qMaxVal, Number.NaN);
|
|
||||||
clippedVarData = clippedVarData.withCol(
|
|
||||||
k,
|
|
||||||
clippedData,
|
|
||||||
state.obsAnnotations.rowIndex
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
varData: clippedVarData,
|
|
||||||
unclipped: {
|
|
||||||
...state.unclipped,
|
|
||||||
varData: unclippedVarData
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WorldReducer;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user