Redux refactor (#1571)

* refactor categorical controls state

* lint

* fix race condition in tests

* fix typo

* add missing update on subset

* remove obsolete code

* update jest and puppeteer major version; update all minors

* update when label changes

* remove lint from tests; increase timeouts in e2e tests

* initial refactoring to new async annomatrix

* refine error handling

* fix bad merge

* add continuous legend

* lint

* fix memoization in color table creators

* partial implementation of user defined annotations

* add new annotations action creator file

* first pass at user annotations

* additional user annotation bug fixes

* user annotation auto-save

* unit test cleanup

* lint

* refactor into multiple files

* cleanup

* add column GC

* fix several bugs in user annotations

* remove debug code

* no anonymous functions

* undo redo cleanup

* file cleanup

* scatterplot

* performance

* cleanup

* remove old code

* render in parallel with load

* fix race condition

* simply graph rendering

* render throttle DRY

* fix category label order

* fix typo in e2e test setup

* re-fix the e2e test setup

* be more tolerant of races

* anno matrix unit tests

* temp disable reembedding

* pilot port continuous histo to react-async

* name change

* lint

* fix repaint bug

* typo fix

* update snap to match new ids

* world/universe name cleanup

* move annoMatrix to src dir

* use private underscore naming convention

* fix corner case in all selected

* name cleanup

* add layout control

* init edge case

* lint

* port scatterplot

* fix label indexing bug and improve tests

* port category to react-async

* fix user annotation labelling while subset

* select all of prev layout on layout switch

* fix race with crossfilter update

* prettier lint

* fix misleading comment

* fix url composition in loader

* first pass at crossfilter tests

* lint

* lint

* fix typo

* improved error handling for network errors

* fix memoization bug

* add memo

* refactor for performnce

* add missing single-value handling in select exact parser

* small bugs discovered by tests

* lint

* additional crossfilter unit tests

* remove extraneous comment

* add support for automatic category determination

* lint

* fix render bug in category

* take advantage of schema categories guarantee

* lint

* do not clear history when resetting

* enhanced annomatrix gc

* lint

* finish renaming to follow conventions; fix clone race bug

* lint

* add priority based loading to improve initial data load UX

* crossfilter cache perf

* perf tuning

* remove timers

* documentation

* PR review changes

* PR review changes

* more PR review edits

* improve clarity of comment

* more PR review fixes

* port centroidLabels to use react-async

* remove dead code

* pr review updates

* oops, remove logging
This commit is contained in:
Bruce Martin
2020-07-14 13:53:33 -07:00
committed by GitHub
parent f69d141336
commit 1269e188be
95 changed files with 18051 additions and 5382 deletions
@@ -0,0 +1,316 @@
// these TWO statements MUST be first in the file, before any other imports
import { enableFetchMocks } from "jest-fetch-mock";
import * as serverMocks from "./serverMocks";
// OK, continue on!
import {
AnnoMatrixLoader,
clip,
isubset,
isubsetMask,
} from "../../../src/annoMatrix";
import { Dataframe } from "../../../src/util/dataframe";
enableFetchMocks();
describe("AnnoMatrix", () => {
let annoMatrix;
beforeEach(async () => {
fetch.resetMocks(); // reset all fetch mocking state
annoMatrix = new AnnoMatrixLoader(
serverMocks.baseDataURL,
serverMocks.schema.schema
);
});
describe("basics", () => {
test("annomatrix static checks", () => {
expect(annoMatrix).toBeDefined();
expect(annoMatrix.schema).toMatchObject(serverMocks.schema.schema);
expect(annoMatrix.nObs).toEqual(serverMocks.schema.schema.dataframe.nObs);
expect(annoMatrix.nVar).toEqual(serverMocks.schema.schema.dataframe.nVar);
expect(annoMatrix.isView).toBeFalsy();
expect(annoMatrix.viewOf).toBeUndefined();
expect(annoMatrix.rowIndex).toBeDefined();
});
test("simple single column fetch", async () => {
fetch.once(serverMocks.annotationsObs(["name_0"]));
const df = await annoMatrix.fetch("obs", "name_0");
expect(df).toBeInstanceOf(Dataframe);
expect(df.colIndex.labels()).toEqual(["name_0"]);
expect(df.dims).toEqual([annoMatrix.nObs, 1]);
});
test("simple multi column fetch", async () => {
fetch
.once(serverMocks.annotationsObs(["name_0"]))
.once(serverMocks.annotationsObs(["n_genes"]));
await expect(
annoMatrix.fetch("obs", ["name_0", "n_genes"])
).resolves.toBeInstanceOf(Dataframe);
});
describe("fetch from field", () => {
const getLastTwo = async (field) => {
const columnNames = annoMatrix.getMatrixColumns(field).slice(-2);
fetch.mockResponses(...columnNames.map(() => serverMocks.responder));
await expect(
annoMatrix.fetch(field, columnNames)
).resolves.toBeInstanceOf(Dataframe);
};
test("obs", async () => getLastTwo("obs"));
test("var", async () => getLastTwo("var"));
test("emb", async () => getLastTwo("emb"));
});
test("fetch - test all query forms", async () => {
// single string is a column name
fetch.once(serverMocks.annotationsObs(["n_genes"]));
await expect(annoMatrix.fetch("obs", "n_genes")).resolves.toBeInstanceOf(
Dataframe
);
// array of column names, expecting n_genes to be cached.
fetch.once(serverMocks.annotationsObs(["percent_mito"]));
await expect(
annoMatrix.fetch("obs", ["n_genes", "percent_mito"])
).resolves.toBeInstanceOf(Dataframe);
// more complex value filter query, enumerated
fetch.once(serverMocks.responder);
await expect(
annoMatrix.fetch("X", {
field: "var",
column: annoMatrix.schema.annotations.var.index,
value: "TYMP",
})
).resolves.toBeInstanceOf(Dataframe);
// more complex value filter query, range
const varIndex = annoMatrix.schema.annotations.var.index;
fetch
.once(
serverMocks.withExpected("/data/var", [[`var:${varIndex}`, "SUMO3"]])
)
.once(
serverMocks.withExpected("/data/var", [[`var:${varIndex}`, "TYMP"]])
);
await expect(
annoMatrix.fetch("X", [
{
field: "var",
column: varIndex,
value: "SUMO3",
},
{
field: "var",
column: varIndex,
value: "TYMP",
},
])
).resolves.toBeInstanceOf(Dataframe);
// XXX inspect the wherecache?
});
test("push and pop views", async () => {
const am1 = clip(annoMatrix, 0.1, 0.9);
expect(am1.viewOf).toBe(annoMatrix);
expect(am1.nObs).toEqual(annoMatrix.nObs);
expect(am1.nVar).toEqual(annoMatrix.nVar);
expect(am1.rowIndex).toBe(annoMatrix.rowIndex);
const am2 = clip(annoMatrix, 0.1, 0.9);
expect(am2.viewOf).toBe(annoMatrix);
expect(am2).not.toBe(am1);
expect(am2.rowIndex).toBe(annoMatrix.rowIndex);
});
test("schema accessors", () => {
expect(annoMatrix.getMatrixFields()).toEqual(
expect.arrayContaining(["X", "obs", "emb", "var"])
);
expect(annoMatrix.getMatrixColumns("obs")).toEqual(
expect.arrayContaining(["name_0", "n_genes", "louvain"])
);
expect(annoMatrix.getColumnSchema("emb", "umap")).toEqual({
name: "umap",
dims: ["umap_0", "umap_1"],
type: "float32",
});
expect(annoMatrix.getColumnDimensions("emb", "umap")).toEqual([
"umap_0",
"umap_1",
]);
});
/*
test the mask & label access to subset via isubset and isubsetMask
*/
test("isubset", async () => {
const rowList = [0, 10];
const rowMask = new Uint8Array(annoMatrix.nObs);
for (let i = 0; i < rowList.length; i += 1) {
rowMask[rowList[i]] = 1;
}
const am1 = isubset(annoMatrix, rowList);
const am2 = isubsetMask(annoMatrix, rowMask);
expect(am1).not.toBe(am2);
expect(am1.nObs).toEqual(2);
expect(am1.nObs).toEqual(am2.nObs);
expect(am1.nVar).toEqual(am2.nVar);
fetch
.once(serverMocks.annotationsObs(["n_genes"]))
.once(serverMocks.annotationsObs(["n_genes"]));
const ng1 = await am1.fetch("obs", "n_genes");
const ng2 = await am2.fetch("obs", "n_genes");
expect(ng1).toHaveLength(ng2.length);
expect(ng1.colIndex.labels()).toEqual(ng2.colIndex.labels());
expect(ng1.col("n_genes").asArray()).toEqual(
ng2.col("n_genes").asArray()
);
});
});
describe("add/drop column", () => {
async function addDrop(base) {
expect(base.getMatrixColumns("obs")).not.toContain("foo");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(base.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
/* add */
const am1 = base.addObsColumn(
{ name: "foo", type: "float32", writable: true },
Float32Array,
0
);
expect(base.getMatrixColumns("obs")).not.toContain("foo");
expect(am1.getMatrixColumns("obs")).toContain("foo");
const foo = await am1.fetch("obs", "foo");
expect(foo).toBeDefined();
expect(foo).toBeInstanceOf(Dataframe);
expect(foo).toHaveLength(am1.nObs);
expect(foo.col("foo").asArray()).toEqual(
new Float32Array(am1.nObs).fill(0)
);
/* drop */
const am2 = am1.dropObsColumn("foo");
expect(base.getMatrixColumns("obs")).not.toContain("foo");
expect(am2.getMatrixColumns("obs")).not.toContain("foo");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(am2.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
}
test("add/drop column, without view", async () => {
await addDrop(annoMatrix);
});
test("add/drop column, with view", async () => {
const am1 = clip(annoMatrix, 0.1, 0.9);
await addDrop(am1);
const am2 = isubset(am1, [0, 1, 2, 20, 30, 400]);
await addDrop(am2);
const am3 = isubset(annoMatrix, [10, 0, 7, 3]);
await addDrop(am3);
const am4 = clip(am3, 0, 1);
await addDrop(am4);
fetch.mockResponse(serverMocks.responder);
await am1.fetch("obs", am1.getMatrixColumns("obs"));
await am2.fetch("obs", am2.getMatrixColumns("obs"));
await am3.fetch("obs", am3.getMatrixColumns("obs"));
await am4.fetch("obs", am4.getMatrixColumns("obs"));
fetch.resetMocks();
await addDrop(am1);
await addDrop(am2);
await addDrop(am3);
await addDrop(am4);
});
});
describe("setObsColumnValues", () => {
async function addSetDrop(base) {
/* add column */
let am = base.addObsColumn(
{
name: "test",
type: "categorical",
categories: ["unassigned", "red", "green"],
writable: true,
},
Array,
"unassigned"
);
const testVal = await am.fetch("obs", "test");
expect(testVal.col("test").asArray()).toEqual(
new Array(am.nObs).fill("unassigned")
);
/* set values in column */
const whichRows = [1, 2, 10];
const am1 = await am.setObsColumnValues("test", whichRows, "yo");
const testVal1 = await am1.fetch("obs", "test");
const expt = new Array(am1.nObs).fill("unassigned");
for (let i = 0; i < whichRows.length; i += 1) {
const offset = am1.rowIndex.getOffset(whichRows[i]);
expt[offset] = "yo";
}
expect(testVal1).not.toBe(testVal);
expect(testVal1.col("test").asArray()).toEqual(expt);
expect(am1.getColumnSchema("obs", "test").type).toBe("categorical");
expect(am1.getColumnSchema("obs", "test").categories).toEqual(
expect.arrayContaining(["unassigned", "red", "green", "yo"])
);
/* drop column */
fetch.mockRejectOnce(new Error("unknown column name"));
am = am1.dropObsColumn("test");
await expect(am.fetch("obs", "test")).rejects.toThrow(
"unknown column name"
);
}
test("set, without a view", async () => {
await addSetDrop(annoMatrix);
});
test("set, with a view", async () => {
const am1 = clip(annoMatrix, 0.1, 0.9);
await addSetDrop(am1);
const am2 = isubset(am1, [0, 1, 2, 10, 20, 30, 400]);
await addSetDrop(am2);
const am3 = isubset(annoMatrix, [10, 1, 0, 30, 2]);
await addSetDrop(am3);
fetch.mockResponse(serverMocks.responder);
await am1.fetch("obs", am1.getMatrixColumns("obs"));
await am2.fetch("obs", am2.getMatrixColumns("obs"));
await am3.fetch("obs", am3.getMatrixColumns("obs"));
await addSetDrop(am1);
await addSetDrop(am2);
await addSetDrop(am3);
});
});
});
@@ -0,0 +1,688 @@
// these TWO statements MUST be first in the file, before any other imports
import { enableFetchMocks } from "jest-fetch-mock";
import * as serverMocks from "./serverMocks";
// OK, continue on!
import obsLouvain from "./louvain.json";
import obsNGenes from "./n_genes.json";
import embUmap from "./umap.json";
import {
AnnoMatrixLoader,
AnnoMatrixObsCrossfilter,
isubsetMask,
} from "../../../src/annoMatrix";
import { rangeFill } from "../../../src/util/range";
enableFetchMocks();
describe("AnnoMatrixCrossfilter", () => {
let annoMatrix;
let crossfilter;
beforeEach(async () => {
fetch.resetMocks(); // reset all fetch mocking state
annoMatrix = new AnnoMatrixLoader(
serverMocks.baseDataURL,
serverMocks.schema.schema
);
crossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
});
test("initial state of crossfilter", () => {
const { nObs } = annoMatrix;
expect(crossfilter).toBeDefined();
expect(crossfilter.size()).toEqual(nObs);
expect(crossfilter.annoMatrix).toBe(annoMatrix);
/* by default, everything should be selected, even if no data in cache */
expect(crossfilter.countSelected()).toEqual(nObs);
expect(crossfilter.allSelectedLabels()).toEqual(
rangeFill(new Int32Array(nObs))
);
expect(crossfilter.allSelectedMask()).toEqual(new Uint8Array(nObs).fill(1));
expect(crossfilter.fillByIsSelected(new Uint8Array(nObs), 2, 1)).toEqual(
new Uint8Array(nObs).fill(2)
);
});
describe("select", () => {
/*
test the selection state via crossfilter proxy
*/
test("select loads index", async () => {
/*
Select should transparently load/create dimension index.
Internal dimension names are field/col:col:col..., eg,
obs:louvain
emb:umap_0:umap_1
*/
expect(crossfilter.obsCrossfilter.dimensionNames()).toEqual([]);
expect(
crossfilter.obsCrossfilter.hasDimension("obs/louvain")
).toBeFalsy();
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
let newCrossfilter = await crossfilter.select("obs", "louvain", {
mode: "none",
});
expect(newCrossfilter.countSelected()).toEqual(0);
expect(
newCrossfilter.obsCrossfilter.hasDimension("obs/louvain")
).toBeTruthy();
expect(fetch.mock.calls).toHaveLength(1);
newCrossfilter = await crossfilter.select("obs", "louvain", {
mode: "all",
});
expect(newCrossfilter.countSelected()).toEqual(annoMatrix.nObs);
});
test("simple column select", async () => {
let xfltr;
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
xfltr = await crossfilter.select("obs", "louvain", {
mode: "exact",
values: ["NK cells", "B cells"],
});
expect(xfltr).toBeDefined();
expect(xfltr.countSelected()).toEqual(496);
expect(xfltr.allSelectedMask()).toEqual(
Uint8Array.from(
obsLouvain.map((val) =>
val === "NK cells" || val === "B cells" ? 1 : 0
)
)
);
expect(xfltr.allSelectedLabels()).toEqual(
Int32Array.from(
obsLouvain.reduce((acc, val, idx) => {
if (val === "NK cells" || val === "B cells") acc.push(idx);
return acc;
}, [])
)
);
expect(
xfltr.fillByIsSelected(new Uint8Array(annoMatrix.nObs), 3, 1)
).toEqual(
Uint8Array.from(
obsLouvain.map((val) =>
val === "NK cells" || val === "B cells" ? 3 : 1
)
)
);
const df = await annoMatrix.fetch("obs", "louvain");
const values = df.col("louvain").asArray();
const selected = xfltr.allSelectedMask();
values.every(
(val, idx) => !["NK cells", "B cells"].includes(val) !== !selected[idx]
);
fetch.once(
serverMocks.dataframeResponse(["n_genes"], [new Int32Array(obsNGenes)])
);
xfltr = await xfltr.select("obs", "n_genes", {
mode: "range",
lo: 0,
hi: 500,
inclusive: false,
});
expect(xfltr.countSelected()).toEqual(33);
expect(xfltr.allSelectedLabels()).toEqual(
Int32Array.from(
obsNGenes.reduce((acc, val, idx) => {
const louvain = obsLouvain[idx];
if (
val >= 0 &&
val < 500 &&
(louvain === "NK cells" || louvain === "B cells")
)
acc.push(idx);
return acc;
}, [])
)
);
xfltr = await xfltr.selectAll();
expect(xfltr.countSelected()).toEqual(annoMatrix.nObs);
});
test("join column select", async () => {
const varIndex = annoMatrix.schema.annotations.var.index;
const { nObs } = annoMatrix.schema.dataframe;
fetch.once(
serverMocks.dataframeResponse(
["TEST"],
[rangeFill(new Float32Array(nObs), 0, 0.1)]
)
);
const xfltr = await crossfilter.select(
"X",
{
field: "var",
column: varIndex,
value: "TYMP",
},
{
mode: "range",
lo: 0,
hi: 50,
inclusive: true,
}
);
expect(xfltr).toBeDefined();
expect(xfltr.countSelected()).toEqual(501);
const df = await annoMatrix.fetch("X", {
field: "var",
column: varIndex,
value: "TYMP",
});
const values = df.icol(0).asArray();
const selected = xfltr.allSelectedMask();
values.every((val, idx) => !(val >= 0 && val <= 50) !== !selected[idx]);
expect(selected.reduce((acc, val) => (val ? acc + 1 : acc), 0)).toEqual(
xfltr.countSelected()
);
});
test("spatial column select", async () => {
fetch.once(
serverMocks.dataframeResponse(
["umap_0", "umap_1"],
[Float32Array.from(embUmap[0]), Float32Array.from(embUmap[1])]
)
);
const xfltr = await crossfilter.select("emb", "umap", {
mode: "within-rect",
minX: 0,
minY: 0,
maxX: 0.5,
maxY: 0.5,
});
expect(xfltr.countSelected()).toEqual(16);
});
test("select on subset", async () => {
const mask = new Uint8Array(annoMatrix.nObs).fill(0);
for (let i = 0; i < mask.length; i += 2) {
mask[i] = true;
}
const annoMatrixSubset = isubsetMask(annoMatrix, mask);
expect(annoMatrixSubset.nObs).toEqual(Math.floor(annoMatrix.nObs / 2));
let xfltr = new AnnoMatrixObsCrossfilter(annoMatrixSubset);
expect(xfltr.countSelected()).toEqual(annoMatrixSubset.nObs);
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
xfltr = await xfltr.select("obs", "louvain", {
mode: "exact",
values: ["NK cells", "B cells"],
});
expect(xfltr).toBeDefined();
expect(xfltr.countSelected()).toEqual(240);
const df = await annoMatrixSubset.fetch("obs", "louvain");
const values = df.col("louvain").asArray();
const selected = xfltr.allSelectedMask();
values.every(
(val, idx) => !["NK cells", "B cells"].includes(val) !== !selected[idx]
);
});
test("select catches errors", async () => {
await expect(crossfilter.select("NADA", "foo")).rejects.toThrow(
"Unknown field name"
);
await expect(crossfilter.select("var", "foo")).rejects.toThrow(
"unable to obsSelect upon the var dimension"
);
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(crossfilter.select("obs", "foo")).rejects.toThrow(
"unknown column name"
);
});
});
describe("mutate matrix", () => {
/*
test the matrix mutators via crossfilter proxy
*/
async function helperAddTestCol(cf, colName, colSchema = null) {
expect(
cf.annoMatrix.getMatrixColumns("obs").includes(colName)
).toBeFalsy();
if (colSchema === null) {
colSchema = {
name: colName,
type: "categorical",
categories: ["toasty"],
};
}
colSchema.name = colName;
const initValue = colSchema.categories[0];
const xfltr = cf.addObsColumn(colSchema, Array, initValue);
expect(
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
(v) => v.name === colName
)
).toHaveLength(1);
const df = await xfltr.annoMatrix.fetch("obs", colName);
expect(df.hasCol(colName)).toBeTruthy();
return xfltr;
}
test("addObsColumn", async () => {
expect(crossfilter.countSelected()).toBe(annoMatrix.nObs);
expect(
crossfilter.annoMatrix.getMatrixColumns("obs").includes("foo")
).toBeFalsy();
const xfltr = crossfilter.addObsColumn(
{ name: "foo", type: "categorical", categories: ["A"] },
Array,
"A"
);
// check schema updates correctly.
expect(xfltr.countSelected()).toBe(annoMatrix.nObs);
expect(
xfltr.annoMatrix.getMatrixColumns("obs").includes("foo")
).toBeTruthy();
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toMatchObject({
name: "foo",
type: "categorical",
});
expect(
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
(v) => v.name === "foo"
)
).toHaveLength(1);
// check data update.
const df = await xfltr.annoMatrix.fetch("obs", "foo");
expect(
df
.col("foo")
.asArray()
.every((v) => v === "A")
).toBeTruthy();
// check that we catch dups
expect(() =>
xfltr.addObsColumn(
{ name: "foo", type: "categorical" },
Array,
"toasty"
)
).toThrow("column already exists");
expect(() =>
xfltr.addObsColumn(
{ name: "louvain", type: "categorical" },
Array,
"toasty"
)
).toThrow("column already exists");
});
test("dropObsColumn", async () => {
let xfltr;
/* check that we catch attempt to drop readonly dimension */
expect(() => crossfilter.dropObsColumn("louvain")).toThrow(
"Unknown or readonly obs column"
);
/* non-existent column */
expect(() => crossfilter.dropObsColumn("does-not-exist")).toThrow(
"Unknown or readonly obs column"
);
// add a column, then drop it.
xfltr = await helperAddTestCol(crossfilter, "foo");
xfltr = xfltr.dropObsColumn("foo");
expect(
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
(v) => v.name === "foo"
)
).toHaveLength(0);
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toBeUndefined();
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.annoMatrix.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
// now same, but ensure we have built an index before doing the drop
xfltr = await helperAddTestCol(crossfilter, "bar");
xfltr = await xfltr.select("obs", "bar", {
mode: "exact",
values: "whatever",
});
xfltr = xfltr.dropObsColumn("bar");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.select("obs", "bar", { mode: "all" })).rejects.toThrow(
"unknown column name"
);
});
test("renameObsColumn", async () => {
let xfltr;
/* catch attempts to rename non-existent or readonly columns */
expect(() =>
crossfilter.renameObsColumn("does-not-exist", "foo")
).toThrow("Unknown or readonly obs column");
expect(() => crossfilter.renameObsColumn("louvain", "foo")).toThrow(
"Unknown or readonly obs column"
);
// add a column, then rename it.
xfltr = await helperAddTestCol(crossfilter, "foo");
xfltr = xfltr.renameObsColumn("foo", "bar");
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toBeUndefined();
expect(xfltr.annoMatrix.getColumnSchema("obs", "bar")).toMatchObject({
name: "bar",
type: "categorical",
});
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.annoMatrix.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
const df = await xfltr.annoMatrix.fetch("obs", "bar");
expect(df.hasCol("bar")).toBeTruthy();
// now same, but ensure we have built an index before doing the rename
xfltr = await helperAddTestCol(crossfilter, "bar");
xfltr = await xfltr.select("obs", "bar", {
mode: "exact",
values: "whatever",
});
xfltr = xfltr.renameObsColumn("bar", "xyz");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.select("obs", "bar", { mode: "all" })).rejects.toThrow(
"unknown column name"
);
await expect(
xfltr.select("obs", "xyz", { mode: "none" })
).resolves.toBeInstanceOf(AnnoMatrixObsCrossfilter);
});
test("addObsAnnoCategory", async () => {
let xfltr;
// catch unknown or readonly columns
expect(() => crossfilter.addObsAnnoCategory("louvain", "mumble")).toThrow(
"Unknown or readonly obs column"
);
expect(() =>
crossfilter.addObsAnnoCategory("undefined-name", "mumble")
).toThrow("Unknown or readonly obs column");
// add a column and then add category to it
xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned"],
});
xfltr = xfltr.addObsAnnoCategory("foo", "a-new-label");
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
});
// do it again, dup; should throw
expect(() => xfltr.addObsAnnoCategory("foo", "a-new-label")).toThrow(
"category already exists"
);
// now same, but ensure we have built an index before doing the operation
xfltr = await helperAddTestCol(crossfilter, "bar", {
name: "bar",
type: "categorical",
categories: ["unassigned"],
});
xfltr = await xfltr.select("obs", "bar", {
mode: "exact",
values: "something",
});
xfltr = xfltr.addObsAnnoCategory("bar", "a-new-label");
expect(xfltr.annoMatrix.getColumnSchema("obs", "bar")).toMatchObject({
name: "bar",
type: "categorical",
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
});
});
test("removeObsAnnoCategory", async () => {
let xfltr;
// catch unknown or readonly categories
await expect(() =>
crossfilter.removeObsAnnoCategory("louvain", "mumble", "unassigned")
).rejects.toThrow("Unknown or readonly obs column");
await expect(() =>
crossfilter.removeObsAnnoCategory("undefined-name", "mumble")
).rejects.toThrow("Unknown or readonly obs column");
xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned", "red", "green", "blue"],
});
xfltr = await xfltr.select("obs", "foo", { mode: "all" });
expect(
(await xfltr.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "unassigned")
).toBeTruthy();
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"red",
"green",
"blue",
]),
});
// remove an unused category
const xfltr1 = await xfltr.removeObsAnnoCategory("foo", "red", "mumble");
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "unassigned")
).toBeTruthy();
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"green",
"blue",
"mumble",
]),
});
// remove a used category
const xfltr2 = await xfltr.removeObsAnnoCategory(
"foo",
"unassigned",
"red"
);
expect(
(await xfltr2.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "red")
).toBeTruthy();
expect(xfltr2.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining(["green", "blue", "red"]),
});
});
test("setObsColumnValues", async () => {
// catch unknown or readonly categories
await expect(() =>
crossfilter.setObsColumnValues("louvain", [0, 1], "unassigned")
).rejects.toThrow("Unknown or readonly obs column");
await expect(() =>
crossfilter.setObsColumnValues("undefined-name", [0], "mumble")
).rejects.toThrow("Unknown or readonly obs column");
let xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned", "red", "green", "blue"],
});
xfltr = await xfltr.select("obs", "foo", { mode: "all" });
// catch unknown row label
await expect(() =>
xfltr.setObsColumnValues("foo", [-1], "red")
).rejects.toThrow("Unknown row label");
// set a few rows
expect(
(await xfltr.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "unassigned")
).toBeTruthy();
const xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every(
(v, i) =>
v === "unassigned" || (v === "purple" && (i === 0 || i === 10))
)
).toBeTruthy();
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"red",
"green",
"blue",
"purple",
]),
});
expect(xfltr1.countSelected()).toEqual(xfltr1.annoMatrix.nObs);
const xfltr2 = await xfltr1.select("obs", "foo", {
mode: "exact",
values: ["purple"],
});
expect(xfltr2.countSelected()).toEqual(2);
expect(xfltr2.allSelectedLabels()).toEqual(Int32Array.from([0, 10]));
});
test("resetObsColumnValues", async () => {
// catch unknown or readonly categories
await expect(() =>
crossfilter.resetObsColumnValues("louvain", "red", "blue")
).rejects.toThrow("Unknown or readonly obs column");
await expect(() =>
crossfilter.resetObsColumnValues("undefined-name", "red", "blue")
).rejects.toThrow("Unknown or readonly obs column");
let xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned", "red", "green", "blue"],
});
xfltr = await xfltr.select("obs", "foo", {
mode: "exact",
values: "red",
});
// catch unknown category name label
await expect(() =>
xfltr.resetObsColumnValues("foo", "unknown-label", "red")
).rejects.toThrow("unknown category");
let xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
xfltr1 = await xfltr1.select("obs", "foo", {
mode: "exact",
values: "purple",
});
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.filter((v) => v === "purple")
).toHaveLength(2);
xfltr1 = await xfltr1.resetObsColumnValues("foo", "purple", "magenta");
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.filter((v) => v === "magenta")
).toHaveLength(2);
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.filter((v) => v === "purple")
).toHaveLength(0);
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"red",
"green",
"blue",
"purple",
"magenta",
]),
});
});
});
describe("edge cases", () => {
test("transition from empty annoMatrix", async () => {
// select before fetch needs to work
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
const xfltr = await crossfilter.select("obs", "louvain", {
mode: "exact",
values: "B cells",
});
expect(fetch.mock.calls).toHaveLength(1);
expect(xfltr.obsCrossfilter.hasDimension("obs/louvain")).toBeTruthy();
expect(xfltr.obsCrossfilter.all()).toBe(xfltr.annoMatrix._cache.obs);
expect(xfltr.countSelected()).toEqual(
obsLouvain.reduce(
(count, v) => (v === "B cells" ? count + 1 : count),
0
)
);
});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
export const baseDataURL = "https://a.fake.url/api/v0.2";
window.CELLXGENE = {
API: {
prefix: baseDataURL,
version: "v0.2/",
},
};
export { schema } from "./schema";
export * from "./routes";
@@ -0,0 +1,211 @@
import { schema } from "./schema";
import { Dataframe, KeyIndex } from "../../../../src/util/dataframe";
import { encodeMatrixFBS } from "../../../../src/util/stateManager/matrix";
const indexedSchema = {
obsByName: Object.fromEntries(
schema.schema.annotations.obs.columns.map((v) => [v.name, v]) ?? []
),
varByName: Object.fromEntries(
schema.schema.annotations.var.columns.map((v) => [v.name, v]) ?? []
),
embByName: Object.fromEntries(
schema.schema.layout.obs.map((v) => [v.name, v]) ?? []
),
};
function makeMockColumn(s, length) {
const { type } = s;
switch (type) {
case "int32":
return new Int32Array(length).fill(Math.floor(99 * Math.random()));
case "string":
return new Array(length).fill("test");
case "float32":
return new Float32Array(length).fill(99 * Math.random());
case "boolean":
return new Array(length).fill(false);
case "categorical":
return new Array(length).fill(s.categories[0]);
default:
throw new Error("unkonwn type");
}
}
function getEncodedDataframe(colNames, length, colSchemas) {
const colIndex = new KeyIndex(colNames);
const columns = colSchemas.map((s) => makeMockColumn(s, length));
const df = new Dataframe([length, colNames.length], columns, null, colIndex);
const body = encodeMatrixFBS(df);
return body;
}
export function dataframeResponse(colNames, columns) {
const colIndex = new KeyIndex(colNames);
const df = new Dataframe(
[columns[0].length, colNames.length],
columns,
null,
colIndex
);
const body = encodeMatrixFBS(df);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return () => Promise.resolve({ body, init: { status: 200, headers } });
}
function annotationObsResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const names = params
.filter(([k]) => k === "annotation-name")
.map(([, v]) => v);
if (!names.every((n) => indexedSchema.obsByName[n])) {
return Promise.reject(new Error("bad obs annotation name in URL"));
}
const colSchemas = names.map((n) => indexedSchema.obsByName[n]);
const body = getEncodedDataframe(
names,
schema.schema.dataframe.nObs,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
function annotationVarResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const names = params
.filter(([k]) => k === "annotation-name")
.map(([, v]) => v);
if (!names.every((n) => indexedSchema.varByName[n])) {
return Promise.reject(new Error("bad var annotation name in URL"));
}
const colSchemas = names.map((n) => indexedSchema.varByName[n]);
const body = getEncodedDataframe(
names,
schema.schema.dataframe.nVar,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
function layoutObsResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const names = params.filter(([k]) => k === "layout-name").map(([, v]) => v);
if (!names.every((n) => indexedSchema.embByName[n])) {
return Promise.reject(new Error("bad layout name in URL"));
}
const dims = names.map((n) => indexedSchema.embByName[n].dims).flat();
const colSchemas = names
.map((n) => [indexedSchema.embByName[n], indexedSchema.embByName[n]])
.flat();
const body = getEncodedDataframe(
dims,
schema.schema.dataframe.nObs,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
function dataVarResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const colNames = params.map((v) => `${v[0]}/${v[1]}`);
const colSchemas = colNames.map(() => schema.schema.dataframe);
const body = getEncodedDataframe(
colNames,
schema.schema.dataframe.nObs,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
export function responder(request) {
const url = new URL(request.url);
const { pathname } = url;
if (pathname.endsWith("/annotations/obs")) {
return annotationObsResponse(request);
}
if (pathname.endsWith("/annotations/var")) {
return annotationVarResponse(request);
}
if (pathname.endsWith("/layout/obs")) {
return layoutObsResponse(request);
}
if (pathname.endsWith("/data/var")) {
return dataVarResponse(request);
}
return Promise.reject(new Error("bad URL"));
}
export function withExpected(expectedURL, expectedParams) {
/*
Do some additional error checking
*/
return (request) => {
// if URL is bogus, reject the promise
const url = new URL(request.url);
if (!url.pathname.endsWith(expectedURL)) {
return Promise.reject(new Error("Unexpected URL!"));
}
const params = Array.from(url.searchParams.entries()).sort(
(a, b) => a[0] < b[0]
);
expectedParams = expectedParams.slice().sort((a, b) => a[0] < b[0]);
if (
params.length !== expectedParams.length ||
!params.every(
(p, i) => p[0] === expectedParams[i][0] && p[1] === expectedParams[i][1]
)
) {
return Promise.reject(new Error("unexpected name requested in URL"));
}
return responder(request);
};
}
export function annotationsObs(names) {
return withExpected(
"/annotations/obs",
names.map((name) => ["annotation-name", name])
);
}
@@ -0,0 +1,80 @@
export const schema = {
schema: {
annotations: {
obs: {
columns: [
{
name: "name_0",
type: "string",
writable: false,
},
{
name: "n_genes",
type: "int32",
writable: false,
},
{
name: "percent_mito",
type: "float32",
writable: false,
},
{
name: "n_counts",
type: "float32",
writable: false,
},
{
name: "louvain",
type: "string",
writable: false,
},
],
index: "name_0",
},
var: {
columns: [
{
name: "name_0",
type: "string",
writable: false,
},
{
name: "n_cells",
type: "int32",
writable: false,
},
],
index: "name_0",
},
},
dataframe: {
nObs: 2638,
nVar: 1838,
type: "float32",
},
layout: {
obs: [
{
dims: ["draw_graph_fr_0", "draw_graph_fr_1"],
name: "draw_graph_fr",
type: "float32",
},
{
dims: ["pca_0", "pca_1"],
name: "pca",
type: "float32",
},
{
dims: ["tsne_0", "tsne_1"],
name: "tsne",
type: "float32",
},
{
dims: ["umap_0", "umap_1"],
name: "umap",
type: "float32",
},
],
},
},
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,184 @@
import {
_whereCacheGet,
_whereCacheCreate,
_whereCacheMerge,
} from "../../../src/annoMatrix/whereCache";
const schema = {};
describe("whereCache", () => {
test("whereCacheGet - missing cache values", () => {
expect(
_whereCacheGet({}, schema, "X", {
field: "var",
column: "foo",
value: "bar",
})
).toEqual([undefined]);
expect(
_whereCacheGet({ X: {} }, schema, "X", {
field: "var",
column: "foo",
value: "bar",
})
).toEqual([undefined]);
expect(
_whereCacheGet({ X: { var: new Map() } }, schema, "X", {
field: "var",
column: "foo",
value: "bar",
})
).toEqual([undefined]);
expect(
_whereCacheGet(
{ X: { var: new Map([["foo", new Map()]]) } },
schema,
"X",
{
field: "var",
column: "foo",
value: "bar",
}
)
).toEqual([undefined]);
});
test("whereCacheGet - varied lookups", () => {
const whereCache = {
X: {
var: new Map([
[
"foo",
new Map([
["bar", [0]],
["baz", [1, 2]],
]),
],
]),
},
};
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "foo",
value: "bar",
})
).toEqual([0]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "foo",
value: "baz",
})
).toEqual([1, 2]);
expect(_whereCacheGet(whereCache, schema, "Y", {})).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "whoknows",
column: "whatever",
value: "snork",
})
).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "whatever",
value: "snork",
})
).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "foo",
value: "snork",
})
).toEqual([undefined]);
});
test("whereCacheCreate", () => {
const query = {
field: "queryField",
column: "queryColumn",
value: "queryValue",
};
const wc = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "queryValue" },
[0, 1, 2]
);
expect(wc).toBeDefined();
expect(wc).toEqual(
expect.objectContaining({
field: {
queryField: expect.any(Map),
},
})
);
expect(wc.field.queryField.has("queryColumn")).toEqual(true);
expect(wc.field.queryField.get("queryColumn")).toBeInstanceOf(Map);
expect(wc.field.queryField.get("queryColumn").has("queryValue")).toEqual(
true
);
expect(_whereCacheGet(wc, schema, "field", query)).toEqual([0, 1, 2]);
});
test("whereCacheMerge", () => {
let wc;
// remember, will mutate dst
const src = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "foo" },
["foo"]
);
const dst1 = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "bar" },
["dst1"]
);
wc = _whereCacheMerge(dst1, src);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "foo",
})
).toEqual(["foo"]);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "bar",
})
).toEqual(["dst1"]);
const dst2 = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "bar" },
["dst2"]
);
wc = _whereCacheMerge(dst2, dst1, src);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "foo",
})
).toEqual(["foo"]);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "bar",
})
).toEqual(["dst1"]);
wc = _whereCacheMerge({}, src);
expect(wc).toEqual(src);
wc = _whereCacheMerge({ field: { queryField: new Map() } }, src);
expect(wc).toEqual(src);
});
});
+30 -28
View File
@@ -1,40 +1,36 @@
import _ from "lodash";
import calcCentroid from "../../src/util/centroid";
import quantile from "../../src/util/quantile";
import * as Universe from "../../src/util/stateManager/universe";
import { matrixFBSToDataframe } from "../../src/util/stateManager/matrix";
import * as World from "../../src/util/stateManager/world";
import * as REST from "./stateManager/sampleResponses";
import { indexEntireSchema } from "../../src/util/stateManager/schemaHelpers";
import { _normalizeCategoricalSchema } from "../../src/annoMatrix/schema";
describe("centroid", () => {
let world;
let schema;
let obsAnnotations;
let obsLayout;
beforeAll(() => {
// Create world + universe
let universe = Universe.createUniverseFromResponse(
_.cloneDeep(REST.config),
_.cloneDeep(REST.schema)
);
schema = indexEntireSchema(_.cloneDeep(REST.schema.schema));
obsAnnotations = matrixFBSToDataframe(REST.annotationsObs);
obsLayout = matrixFBSToDataframe(REST.layoutObs);
universe = {
...universe,
...Universe.addObsAnnotations(
universe,
matrixFBSToDataframe(REST.annotationsObs)
),
...Universe.addVarAnnotations(
universe,
matrixFBSToDataframe(REST.annotationsVar)
),
...Universe.addObsLayout(universe, matrixFBSToDataframe(REST.layoutObs)),
};
world = World.createWorldFromEntireUniverse(universe);
_normalizeCategoricalSchema(
schema.annotations.obsByName.field3,
obsAnnotations.col("field3")
);
});
test("field4 (categorical obsAnnotation)", () => {
const centroidResult = calcCentroid(world, "field4", ["umap_0", "umap_1"]);
const centroidResult = calcCentroid(
schema,
"field4",
obsAnnotations,
{ current: "umap", currentDimNames: ["umap_0", "umap_1"] },
obsLayout
);
// Check to see that a centroid has been calculated for every categorical value
const keysAsArray = Array.from(centroidResult.keys());
@@ -44,8 +40,8 @@ describe("centroid", () => {
// This expected result assumes that all cells belong in all categorical values inside of sample response
const expectedResult = [
quantile([0.5], world.obsLayout.col("umap_0").asArray())[0],
quantile([0.5], world.obsLayout.col("umap_1").asArray())[0],
quantile([0.5], obsLayout.col("umap_0").asArray())[0],
quantile([0.5], obsLayout.col("umap_1").asArray())[0],
];
centroidResult.forEach((coordinate) => {
@@ -54,7 +50,13 @@ describe("centroid", () => {
});
test("field3 (boolean obsAnnotation)", () => {
const centroidResult = calcCentroid(world, "field3", ["umap_0", "umap_1"]);
const centroidResult = calcCentroid(
schema,
"field3",
obsAnnotations,
{ current: "umap", currentDimNames: ["umap_0", "umap_1"] },
obsLayout
);
// Check to see that a centroid has been calculated for every categorical value
const keysAsArray = Array.from(centroidResult.keys());
@@ -62,8 +64,8 @@ describe("centroid", () => {
// This expected result assumes that all cells belong in all categorical values inside of sample response
const expectedResult = [
quantile([0.5], world.obsLayout.col("umap_0").asArray())[0],
quantile([0.5], world.obsLayout.col("umap_1").asArray())[0],
quantile([0.5], obsLayout.col("umap_0").asArray())[0],
quantile([0.5], obsLayout.col("umap_1").asArray())[0],
];
centroidResult.forEach((coordinate) => {
+315 -88
View File
@@ -918,115 +918,342 @@ describe("dataframe col", () => {
});
describe("label indexing", () => {
test("IdentityInt32Index", () => {
describe("isLabelIndex", () => {
expect(
Dataframe.isLabelIndex(new Dataframe.IdentityInt32Index(4))
).toBeTruthy();
expect(
Dataframe.isLabelIndex(new Dataframe.DenseInt32Index([2, 4, 99]))
).toBeTruthy();
expect(
Dataframe.isLabelIndex(new Dataframe.KeyIndex(["a", 4, "toasty"]))
).toBeTruthy();
expect(Dataframe.isLabelIndex(false)).toBeFalsy();
expect(Dataframe.isLabelIndex(undefined)).toBeFalsy();
expect(Dataframe.isLabelIndex(null)).toBeFalsy();
expect(Dataframe.isLabelIndex(true)).toBeFalsy();
expect(Dataframe.isLabelIndex([])).toBeFalsy();
expect(Dataframe.isLabelIndex({})).toBeFalsy();
expect(Dataframe.isLabelIndex(Dataframe.IdentityInt32Index)).toBeFalsy();
});
describe("IdentityInt32Index", () => {
const idx = new Dataframe.IdentityInt32Index(12); // [0, 12)
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
test("create", () => {
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
});
expect(idx.labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
);
expect(idx.getLabel(1)).toEqual(1);
expect(idx.getOffset(1)).toEqual(1);
expect(idx.getOffsets([1, 3])).toEqual([1, 3]);
expect(idx.getLabels([1, 3])).toEqual([1, 3]);
expect(idx.size()).toEqual(12);
test("labels", () => {
expect(idx.labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
);
expect(idx.getLabel(1)).toEqual(1);
expect(idx.getLabels([1, 3])).toEqual([1, 3]);
expect(idx.size()).toEqual(12);
});
expect(idx.subset([2]).labels()).toEqual([2]);
expect(idx.subset([2, 3, 4]).labels()).toEqual(new Int32Array([2, 3, 4]));
expect(idx.subset([0, 1, 2, 3]).labels()).toEqual(
new Int32Array([0, 1, 2, 3])
);
test("offsets", () => {
expect(idx.getOffset(1)).toEqual(1);
expect(idx.getOffsets([1, 3])).toEqual([1, 3]);
});
expect(idx.isubset([2]).labels()).toEqual([2]);
expect(idx.isubset([2, 3, 4]).labels()).toEqual(new Int32Array([2, 3, 4]));
expect(idx.isubset([0, 1, 2, 3]).labels()).toEqual(
new Int32Array([0, 1, 2, 3])
);
test("subset", () => {
expect(idx.subset([2]).labels()).toEqual([2]);
expect(idx.subset([2, 3, 4]).labels()).toEqual(new Int32Array([2, 3, 4]));
expect(idx.subset([0, 1, 2, 3]).labels()).toEqual(
new Int32Array([0, 1, 2, 3])
);
expect(idx.subset([0, 1, 2, 3, 4])).toBeInstanceOf(
Dataframe.IdentityInt32Index
);
expect(idx.subset([2, 1, 0])).toBeInstanceOf(
Dataframe.IdentityInt32Index
);
expect(idx.subset([1, 2, 3, 4])).toBeInstanceOf(
Dataframe.DenseInt32Index
);
expect(idx.subset([0, 1, 3, 4])).toBeInstanceOf(
Dataframe.DenseInt32Index
);
expect(idx.subset([0, 1, 2, 3, 10])).toBeInstanceOf(
Dataframe.DenseInt32Index
);
expect(idx.subset([4, 3, 2, 1])).toBeInstanceOf(
Dataframe.DenseInt32Index
);
expect(idx.subset([4])).toBeInstanceOf(Dataframe.KeyIndex);
});
expect(idx.subset([0, 1, 2, 3, 4])).toBeInstanceOf(
Dataframe.IdentityInt32Index
);
expect(idx.subset([2, 1, 0])).toBeInstanceOf(Dataframe.IdentityInt32Index);
expect(idx.subset([1, 2, 3, 4])).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(idx.subset([0, 1, 3, 4])).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(idx.subset([0, 1, 2, 3, 10])).toBeInstanceOf(
Dataframe.DenseInt32Index
);
expect(idx.subset([4, 3, 2, 1])).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(idx.subset([4])).toBeInstanceOf(Dataframe.KeyIndex);
test("isubset", () => {
expect(idx.isubset([2]).labels()).toEqual([2]);
expect(idx.isubset([2, 3, 4]).labels()).toEqual(
new Int32Array([2, 3, 4])
);
expect(idx.isubset([0, 1, 2, 3]).labels()).toEqual(
new Int32Array([0, 1, 2, 3])
);
expect(() => idx.isubset([-1001])).toThrow(RangeError);
expect(() => idx.isubset([1001])).toThrow(RangeError);
});
expect(idx.withLabel(99).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 99])
);
expect(idx.dropLabel(0).labels()).toEqual(
new Int32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
);
expect(idx.dropLabel(11).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
);
expect(idx.dropLabel(5).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11])
);
test("isubsetMask", () => {
expect(
idx
.isubsetMask([
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
])
.labels()
).toEqual(new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]));
expect(
idx
.isubsetMask([
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
])
.labels()
).toEqual(new Int32Array([]));
expect(
idx
.isubsetMask([
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
])
.labels()
).toEqual(new Int32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]));
expect(
idx
.isubsetMask([
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
])
.labels()
).toEqual(new Int32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 11]));
expect(
idx
.isubsetMask([
false,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
false,
])
.labels()
).toEqual(new Int32Array([1, 2, 4, 5, 6, 7, 8, 9, 10]));
expect(() => idx.isubsetMask([])).toThrow(RangeError);
});
test("withLabel", () => {
expect(idx.withLabel(99).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 99])
);
expect(idx.withLabel(12).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
);
expect(idx.withLabels([12, 13]).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])
);
});
test("dropLabel", () => {
expect(idx.dropLabel(0).labels()).toEqual(
new Int32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
);
expect(idx.dropLabel(11).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
);
expect(idx.dropLabel(5).labels()).toEqual(
new Int32Array([0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11])
);
});
});
test("DenseInt32Index", () => {
describe("DenseInt32Index", () => {
const idx = new Dataframe.DenseInt32Index([99, 1002, 48, 0, 22]);
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
test("create", () => {
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
});
expect(idx.labels()).toEqual(new Int32Array([99, 1002, 48, 0, 22]));
expect(idx.size()).toEqual(5);
expect(idx.getOffset(1002)).toEqual(1);
expect(idx.getOffset(0)).toEqual(3);
expect(idx.getLabel(0)).toEqual(99);
expect(idx.getLabels(new Int32Array([2, 4]))).toEqual(
new Int32Array([48, 22])
);
expect(idx.getLabels([2, 4])).toEqual([48, 22]);
expect(idx.getOffsets([0, 48])).toEqual([3, 2]);
test("labels", () => {
expect(idx.labels()).toEqual(new Int32Array([99, 1002, 48, 0, 22]));
expect(idx.size()).toEqual(5);
expect(idx.getLabel(0)).toEqual(99);
expect(idx.getLabels(new Int32Array([2, 4]))).toEqual(
new Int32Array([48, 22])
);
expect(idx.getLabels([2, 4])).toEqual([48, 22]);
});
expect(idx.subset([1002, 0, 99]).labels()).toEqual(
new Int32Array([1002, 0, 99])
);
expect(idx.getOffsets(idx.subset([1002, 0, 99]).labels())).toEqual(
new Int32Array([1, 3, 0])
);
expect(idx.isubset([4, 1, 2]).labels()).toEqual(
new Int32Array([22, 1002, 48])
);
test("offsets", () => {
expect(idx.getOffset(1002)).toEqual(1);
expect(idx.getOffset(0)).toEqual(3);
expect(idx.getOffsets([0, 48])).toEqual([3, 2]);
});
expect(idx.withLabel(88).labels()).toEqual(
new Int32Array([99, 1002, 48, 0, 22, 88])
);
expect(idx.withLabel(88).getOffset(88)).toEqual(5);
expect(idx.dropLabel(48).labels()).toEqual(
new Int32Array([99, 1002, 0, 22])
);
test("subset", () => {
expect(idx.subset([1002, 0, 99]).labels()).toEqual(
new Int32Array([1002, 0, 99])
);
expect(idx.getOffsets(idx.subset([1002, 0, 99]).labels())).toEqual(
new Int32Array([1, 3, 0])
);
expect(() => idx.subset([-1])).toThrow(RangeError);
});
test("isubset", () => {
expect(idx.isubset([4, 1, 2]).labels()).toEqual(
new Int32Array([22, 1002, 48])
);
expect(() => idx.isubset([-1001])).toThrow(RangeError);
expect(() => idx.isubset([1001])).toThrow(RangeError);
});
test("isubsetMask", () => {
expect(idx.isubsetMask([true, true, true, true, true]).labels()).toEqual(
new Int32Array([99, 1002, 48, 0, 22])
);
expect(
idx.isubsetMask([false, false, false, false, false]).labels()
).toEqual(new Int32Array([]));
expect(idx.isubsetMask([true, true, false, true, true]).labels()).toEqual(
new Int32Array([99, 1002, 0, 22])
);
expect(
idx.isubsetMask([false, true, true, true, false]).labels()
).toEqual(new Int32Array([1002, 48, 0]));
expect(() => idx.isubsetMask([])).toThrow(RangeError);
});
test("withLabel", () => {
expect(idx.withLabel(88).labels()).toEqual(
new Int32Array([99, 1002, 48, 0, 22, 88])
);
expect(idx.withLabel(88).getOffset(88)).toEqual(5);
expect(idx.withLabels([88, 99]).labels()).toEqual(
new Int32Array([99, 1002, 48, 0, 22, 88, 99])
);
});
test("dropLabel", () => {
expect(idx.dropLabel(48).labels()).toEqual(
new Int32Array([99, 1002, 0, 22])
);
});
});
test("KeyIndex", () => {
describe("KeyIndex", () => {
const idx = new Dataframe.KeyIndex(["red", "green", "blue"]);
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
test("create", () => {
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
expect(() => new Dataframe.KeyIndex(["dup", "dup"])).toThrow(Error);
expect(new Dataframe.KeyIndex().size()).toEqual(0);
});
expect(idx.labels()).toEqual(["red", "green", "blue"]);
expect(idx.size()).toEqual(3);
expect(idx.getOffset("blue")).toEqual(2);
expect(idx.getLabel(1)).toEqual("green");
test("labels", () => {
expect(idx.labels()).toEqual(["red", "green", "blue"]);
expect(idx.size()).toEqual(3);
expect(idx.getLabel(1)).toEqual("green");
expect(idx.getLabels([2, 0])).toEqual(["blue", "red"]);
});
expect(idx.subset(["green"]).labels()).toEqual(["green"]);
expect(idx.subset(["green", "red"]).labels()).toEqual(["green", "red"]);
expect(idx.isubset([2, 1, 0]).labels()).toEqual(["blue", "green", "red"]);
test("offsets", () => {
expect(idx.getOffset("blue")).toEqual(2);
});
expect(idx.withLabel("yo").labels()).toEqual([
"red",
"green",
"blue",
"yo",
]);
expect(idx.withLabel("yo").getOffset("yo")).toEqual(3);
expect(idx.dropLabel("blue").labels()).toEqual(["red", "green"]);
test("subset", () => {
expect(idx.subset(["green"]).labels()).toEqual(["green"]);
expect(idx.subset(["green", "red"]).labels()).toEqual(["green", "red"]);
});
test("isubset", () => {
expect(idx.isubset([2, 1, 0]).labels()).toEqual(["blue", "green", "red"]);
expect(() => idx.isubset([-1001])).toThrow(RangeError);
expect(() => idx.isubset([1001])).toThrow(RangeError);
});
test("isubsetMask", () => {
expect(idx.isubsetMask([true, true, true]).labels()).toEqual([
"red",
"green",
"blue",
]);
expect(idx.isubsetMask([false, false, false]).labels()).toEqual([]);
expect(idx.isubsetMask([true, false, true]).labels()).toEqual([
"red",
"blue",
]);
expect(() => idx.isubsetMask([])).toThrow(RangeError);
});
test("withLabel", () => {
expect(idx.withLabel("yo").labels()).toEqual([
"red",
"green",
"blue",
"yo",
]);
expect(idx.withLabel("yo").getOffset("yo")).toEqual(3);
expect(idx.withLabels(["hey", "there"]).labels()).toEqual([
"red",
"green",
"blue",
"hey",
"there",
]);
});
test("dropLabel", () => {
expect(idx.dropLabel("blue").labels()).toEqual(["red", "green"]);
});
});
});
@@ -71,4 +71,26 @@ describe("PromiseLimit", () => {
]);
expect(result).toEqual(["OK", "not OK", "OK", "not OK"]);
});
test("priority queue", async () => {
const plimit = new PromiseLimit(1);
let finishOrder = 0;
const callback = () => async () => {
await delay(100);
const result = finishOrder;
finishOrder += 1;
return result;
};
const result = await Promise.all([
plimit.add(callback()),
plimit.priorityAdd(4, callback()),
plimit.priorityAdd(0, callback()),
plimit.priorityAdd(1, callback()),
plimit.priorityAdd(-1, callback()),
]);
expect(result).toEqual([0, 4, 2, 3, 1]);
});
});
@@ -1,90 +0,0 @@
import * as Universe from "../../../src/util/stateManager/universe";
import { matrixFBSToDataframe } from "../../../src/util/stateManager/matrix";
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;
let universe = Universe.createUniverseFromResponse(
REST.config,
REST.schema
);
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),
})
);
universe = {
...universe,
...Universe.addObsAnnotations(
universe,
matrixFBSToDataframe(REST.annotationsObs)
),
...Universe.addVarAnnotations(
universe,
matrixFBSToDataframe(REST.annotationsVar)
),
...Universe.addObsLayout(universe, matrixFBSToDataframe(REST.layoutObs)),
};
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.labels()).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,202 +0,0 @@
import _ from "lodash";
import * as Universe from "../../../src/util/stateManager/universe";
import { matrixFBSToDataframe } from "../../../src/util/stateManager/matrix";
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 */
let universe = Universe.createUniverseFromResponse(
_.cloneDeep(REST.config),
_.cloneDeep(REST.schema)
);
universe = {
...universe,
...Universe.addObsAnnotations(
universe,
matrixFBSToDataframe(REST.annotationsObs)
),
...Universe.addVarAnnotations(
universe,
matrixFBSToDataframe(REST.annotationsVar)
),
...Universe.addObsLayout(universe, matrixFBSToDataframe(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(
_.cloneDeep(REST.config),
_.cloneDeep(REST.schema),
matrixFBSToDataframe(_.cloneDeep(REST.annotationsObs)),
matrixFBSToDataframe(_.cloneDeep(REST.annotationsVar)),
matrixFBSToDataframe(_.cloneDeep(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.labels()).toEqual(
new Int32Array(matchingIndices)
);
expect(world.obsAnnotations.colIndex.labels()).toEqual(
universe.obsAnnotations.colIndex.labels()
);
expect(world.obsLayout.rowIndex.labels()).toEqual(
new Int32Array(matchingIndices)
);
expect(world.obsLayout.colIndex.labels()).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);
});
@@ -253,6 +253,11 @@ describe("ImmutableTypedCrossfilter", () => {
p.select("quantity", { mode: "exact", values: v }).countSelected()
).toEqual(_.filter(someData, (d) => v.includes(d.quantity)).length)
);
test("single value exact", () => {
expect(
p.select("quantity", { mode: "exact", values: 2 }).countSelected()
).toEqual(_.filter(someData, (d) => d.quantity === 2).length);
});
test.each([
[0, 1],
[1, 2],
@@ -295,6 +300,11 @@ describe("ImmutableTypedCrossfilter", () => {
p.select("type", { mode: "exact", values: v }).countSelected()
).toEqual(_.filter(someData, (d) => v.includes(d.type)).length)
);
test("single value exact", () => {
expect(
p.select("type", { mode: "exact", values: "tab" }).countSelected()
).toEqual(_.filter(someData, (d) => d.type === "tab").length);
});
test("range", () => {
expect(() => p.select("type", { mode: "range", lo: 0, hi: 9 })).toThrow(
Error