mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-28 06:48:11 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e3721846 | ||
|
|
a876740a3c | ||
|
|
e7ad6f5d1c | ||
|
|
2f1facaafb | ||
|
|
ffd6273419 | ||
|
|
2bae696986 | ||
|
|
6b33315cbe | ||
|
|
57c4e9ff33 | ||
|
|
4e67c645f8 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.6.0
|
current_version = 0.7.0
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
[bumpversion:file:setup.py]
|
||||||
search = version="{current_version}"
|
search = version="{current_version}"
|
||||||
|
|||||||
+15
-7
@@ -12,10 +12,18 @@ install:
|
|||||||
- make install
|
- make install
|
||||||
- pip install -r server/requirements-dev.txt
|
- pip install -r server/requirements-dev.txt
|
||||||
- docker build .
|
- docker build .
|
||||||
script:
|
|
||||||
- set -eo pipefail
|
jobs:
|
||||||
- flake8 server
|
include:
|
||||||
- black --check
|
- name: "Branch Tests"
|
||||||
- npm run --prefix client/ build
|
script:
|
||||||
- npm run --prefix client/ test
|
- set -eo pipefail
|
||||||
- pytest -s server/test
|
- flake8 server
|
||||||
|
- black --check
|
||||||
|
- npm run --prefix client/ build
|
||||||
|
- npm run --prefix client/ unit-test
|
||||||
|
- pytest -s server/test
|
||||||
|
- name: "Smoke Tests"
|
||||||
|
if: branch = master AND type = cron
|
||||||
|
script:
|
||||||
|
- npm run --prefix client/ smoke-test
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import puppeteer from "puppeteer";
|
||||||
|
|
||||||
|
const jest_env = process.env.JEST_ENV || "dev";
|
||||||
|
const appPort = process.env.JEST_CXG_PORT || 3000;
|
||||||
|
const appUrlBase = `http://localhost:${appPort}`;
|
||||||
|
const DEV = jest_env === "dev";
|
||||||
|
|
||||||
|
let browser;
|
||||||
|
let page;
|
||||||
|
const browserViewport = { width: 1280, height: 960 };
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const browser_params = DEV
|
||||||
|
? { headless: false, slowMo: 100, devtools: true }
|
||||||
|
: {};
|
||||||
|
browser = await puppeteer.launch(browser_params);
|
||||||
|
page = await browser.newPage();
|
||||||
|
page.setViewport(browserViewport);
|
||||||
|
if (DEV) page.on("console", msg => console.log("PAGE LOG:", msg.text()));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (!DEV) {
|
||||||
|
browser.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getOneElementInnerHTML = async function(selector) {
|
||||||
|
let text = await page.$eval(selector, el => el.innerHTML);
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drag = async function(el_box, start, end, lasso = false) {
|
||||||
|
const x1 = el_box.content[0].x + start.x;
|
||||||
|
const x2 = el_box.content[0].x + end.x;
|
||||||
|
const y1 = el_box.content[0].y + start.y;
|
||||||
|
const y2 = el_box.content[0].y + end.y;
|
||||||
|
await page.mouse.move(x1, y1);
|
||||||
|
await page.mouse.down();
|
||||||
|
if (lasso) {
|
||||||
|
await page.mouse.move(x2, y1);
|
||||||
|
await page.mouse.move(x2, y2);
|
||||||
|
await page.mouse.move(x1, y2);
|
||||||
|
await page.mouse.move(x1, y1);
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(x2, y2);
|
||||||
|
}
|
||||||
|
await page.mouse.up();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("did launch", () => {
|
||||||
|
test("page launched", async () => {
|
||||||
|
await page.goto(appUrlBase);
|
||||||
|
let el = await getOneElementInnerHTML("[data-testid='header']");
|
||||||
|
expect(el).toBe("cellxgene: pbmc3k");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("search for genes", () => {
|
||||||
|
test("search for known gene and add to metadata", async () => {
|
||||||
|
await page.goto(appUrlBase);
|
||||||
|
await page.waitForSelector("[ data-testid='gene-search']");
|
||||||
|
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
|
||||||
|
await page.click("[data-testid='gene-search']");
|
||||||
|
await page.waitFor(200);
|
||||||
|
await page.type("[data-testid='gene-search']", "ACD");
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
await page.waitForSelector("[data-testid='histogram-ACD']");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("select cells and diffexp", () => {
|
||||||
|
test("selects cells from layout and adds to cell set 1", async () => {
|
||||||
|
await page.goto(appUrlBase);
|
||||||
|
const layout = await page.waitForSelector("[data-testid='layout']");
|
||||||
|
const size = await layout.boxModel();
|
||||||
|
const cellset1 = {
|
||||||
|
start: {
|
||||||
|
x: Math.floor(size.width * 0.25),
|
||||||
|
y: Math.floor(size.height * 0.25)
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: Math.floor(size.width * 0.35),
|
||||||
|
y: Math.floor(size.height * 0.35)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await drag(size, cellset1.start, cellset1.end, true);
|
||||||
|
await page.click("[data-testid='cellset-button-1");
|
||||||
|
let button = await getOneElementInnerHTML("[data-testid='cellset-button-1");
|
||||||
|
expect(button).toMatch(/26 cells/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selects cells from layout and adds to cell set 2", async () => {
|
||||||
|
await page.goto(appUrlBase);
|
||||||
|
const layout = await page.waitForSelector("[data-testid='layout']");
|
||||||
|
const size = await layout.boxModel();
|
||||||
|
const cellset2 = {
|
||||||
|
start: {
|
||||||
|
x: Math.floor(size.width * 0.45),
|
||||||
|
y: Math.floor(size.height * 0.45)
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: Math.floor(size.width * 0.55),
|
||||||
|
y: Math.floor(size.height * 0.55)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await drag(size, cellset2.start, cellset2.end, true);
|
||||||
|
await page.click("[data-testid='cellset-button-2");
|
||||||
|
let button = await getOneElementInnerHTML("[data-testid='cellset-button-2");
|
||||||
|
expect(button).toMatch(/49 cells/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selects cells, saves them and performs diffexp", async () => {
|
||||||
|
await page.goto(appUrlBase);
|
||||||
|
const layout = await page.waitForSelector("[data-testid='layout']");
|
||||||
|
const size = await layout.boxModel();
|
||||||
|
const cellset1 = {
|
||||||
|
start: {
|
||||||
|
x: Math.floor(size.width * 0.25),
|
||||||
|
y: Math.floor(size.height * 0.25)
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: Math.floor(size.width * 0.35),
|
||||||
|
y: Math.floor(size.height * 0.35)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await drag(size, cellset1.start, cellset1.end, true);
|
||||||
|
await page.click("[data-testid='cellset-button-1");
|
||||||
|
const cellset2 = {
|
||||||
|
start: {
|
||||||
|
x: Math.floor(size.width * 0.45),
|
||||||
|
y: Math.floor(size.height * 0.45)
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: Math.floor(size.width * 0.55),
|
||||||
|
y: Math.floor(size.height * 0.55)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await drag(size, cellset2.start, cellset2.end, true);
|
||||||
|
await page.click("[data-testid='cellset-button-2");
|
||||||
|
await page.click("[data-testid='diffexp-button");
|
||||||
|
await page.waitForSelector("[data-testclass='histogram-diffexp']");
|
||||||
|
const diffexps = await page.$$eval(
|
||||||
|
"[data-testclass='histogram-diffexp']",
|
||||||
|
divs => {
|
||||||
|
return divs.map(div =>
|
||||||
|
div.id.substring("histogram-".length, div.id.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(diffexps).toMatchObject([
|
||||||
|
"HLA-DPA1",
|
||||||
|
"HLA-DQA1",
|
||||||
|
"HLA-DRB1",
|
||||||
|
"HLA-DMA",
|
||||||
|
"CST3",
|
||||||
|
"HLA-DPB1",
|
||||||
|
"HLA-DQB1",
|
||||||
|
"LGALS2",
|
||||||
|
"FCER1A",
|
||||||
|
"LTB"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("brushable histogram", () => {
|
||||||
|
test("can brush historgram", async () => {
|
||||||
|
await page.goto(appUrlBase);
|
||||||
|
const hist = await page.waitForSelector(
|
||||||
|
"[data-testid='histogram_n_genes_svg-brush'] > .overlay"
|
||||||
|
);
|
||||||
|
const hist_size = await hist.boxModel();
|
||||||
|
const draghist = {
|
||||||
|
start: {
|
||||||
|
x: Math.floor(hist_size.width * 0.25),
|
||||||
|
y: Math.floor(hist_size.height * 0.5)
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: Math.floor(hist_size.width * 0.55),
|
||||||
|
y: Math.floor(hist_size.height * 0.5)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await drag(hist_size, draghist.start, draghist.end);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,616 @@
|
|||||||
|
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("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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
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,249 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import * as kvCache from "../../../src/util/stateManager/keyvalcache";
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is PRIVATE to keyvalcache and must be kept in sync with
|
|
||||||
any changs ot that module. Need to Know - to enable error handling test
|
|
||||||
*/
|
|
||||||
const cachePrivateKey = "__kvcachekey__";
|
|
||||||
|
|
||||||
/*
|
|
||||||
helper function - promisify setTimeout()
|
|
||||||
*/
|
|
||||||
function timeout(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("kvcache API", () => {
|
|
||||||
/*
|
|
||||||
test the happy path create/set/get API
|
|
||||||
*/
|
|
||||||
|
|
||||||
test("simple create", () => {
|
|
||||||
/* with defaults */
|
|
||||||
const kvc = kvCache.create();
|
|
||||||
expect(kvc).toBeDefined();
|
|
||||||
expect(kvc).toEqual(expect.objectContaining({}));
|
|
||||||
expect(kvCache.get(kvc, "test")).toBeUndefined();
|
|
||||||
|
|
||||||
/* with params */
|
|
||||||
const kvc1 = kvCache.create(/* lowWatermark */ 99, /* minTTL */ 0);
|
|
||||||
expect(kvc1).toBeDefined();
|
|
||||||
expect(kvc1).toEqual(expect.objectContaining({}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("set/get", () => {
|
|
||||||
/*
|
|
||||||
- check basic get/set functionality
|
|
||||||
- check set does not mutate source cache
|
|
||||||
*/
|
|
||||||
const keyName = "foo";
|
|
||||||
const kvc1 = kvCache.create();
|
|
||||||
expect(kvc1).toBeDefined();
|
|
||||||
expect(kvCache.get(kvc1, keyName)).toBeUndefined();
|
|
||||||
|
|
||||||
const val2 = [2];
|
|
||||||
const kvc2 = kvCache.set(kvc1, keyName, val2);
|
|
||||||
expect(kvc2).toBeDefined();
|
|
||||||
expect(kvc2).not.toBe(kvc1);
|
|
||||||
expect(kvCache.get(kvc1, keyName)).toBeUndefined();
|
|
||||||
expect(kvCache.get(kvc2, keyName)).toBe(val2);
|
|
||||||
|
|
||||||
const val3 = [3];
|
|
||||||
const kvc3 = kvCache.set(kvc2, keyName, val3);
|
|
||||||
expect(kvc3).toBeDefined();
|
|
||||||
expect(kvc3).not.toBe(kvc1);
|
|
||||||
expect(kvc3).not.toBe(kvc2);
|
|
||||||
expect(kvCache.get(kvc1, keyName)).toBeUndefined();
|
|
||||||
expect(kvCache.get(kvc2, keyName)).toBe(val2);
|
|
||||||
expect(kvCache.get(kvc3, keyName)).toBe(val3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("common error handling", () => {
|
|
||||||
/*
|
|
||||||
Test common error handlers
|
|
||||||
*/
|
|
||||||
|
|
||||||
test("set() protection from namespace pollution", () => {
|
|
||||||
/*
|
|
||||||
Test that set() will not allow use of the private cache key
|
|
||||||
*/
|
|
||||||
const kvc = kvCache.create();
|
|
||||||
expect(() => {
|
|
||||||
kvCache.set(kvc, cachePrivateKey, {});
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("create() does not accept bogus config", () => {
|
|
||||||
expect(() => {
|
|
||||||
kvCache.create([], {});
|
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
kvCache.create(-99, 0);
|
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
kvCache.create(100, -1);
|
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
kvCache.create(1000, "foobar");
|
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
kvCache.create(null, 8);
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("map", () => {
|
|
||||||
/*
|
|
||||||
Test kvCache.map() - create new cache that is a transformation of an
|
|
||||||
existing cache
|
|
||||||
*/
|
|
||||||
test("map of empty cache", () => {
|
|
||||||
const kvc = kvCache.create();
|
|
||||||
const callback = jest.fn();
|
|
||||||
const kvcMapped = kvCache.map(kvc, callback);
|
|
||||||
expect(callback).not.toHaveBeenCalled();
|
|
||||||
expect(kvcMapped).toBeDefined();
|
|
||||||
expect(kvcMapped).not.toBe(kvc); // immutable operation
|
|
||||||
expect(kvcMapped).toEqual(kvc);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("map of non-empty cache", () => {
|
|
||||||
const key = "aKey";
|
|
||||||
const val = [0, 1, 2];
|
|
||||||
let kvc = kvCache.create();
|
|
||||||
kvc = kvCache.set(kvc, key, val);
|
|
||||||
const mockCB = jest.fn().mockImplementation(v => [...v]);
|
|
||||||
const kvcMapped = kvCache.map(kvc, mockCB);
|
|
||||||
|
|
||||||
expect(kvcMapped).toBeDefined();
|
|
||||||
expect(kvcMapped).not.toBe(kvc); // immutable operation
|
|
||||||
expect(_.isEqual(kvc, kvcMapped)).toBe(true);
|
|
||||||
|
|
||||||
expect(mockCB).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockCB).toHaveBeenLastCalledWith(val, key);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("flush", () => {
|
|
||||||
/*
|
|
||||||
test various cache flush behavior
|
|
||||||
*/
|
|
||||||
test("flush - lowWatermark, disable minTTL", () => {
|
|
||||||
/*
|
|
||||||
verify lowWatermark functions correctly
|
|
||||||
*/
|
|
||||||
|
|
||||||
// set lowWatermark to 2, set three times - only the final two
|
|
||||||
// should remain.
|
|
||||||
let kvc = kvCache.create(2, 0);
|
|
||||||
["a", "b", "c"].forEach(k => {
|
|
||||||
kvc = kvCache.set(kvc, k, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
b: expect.arrayContaining([]),
|
|
||||||
c: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.not.objectContaining({
|
|
||||||
a: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("flush - minTTL, disable lowWatermark", async () => {
|
|
||||||
/*
|
|
||||||
verify minTTL functions correctly
|
|
||||||
*/
|
|
||||||
|
|
||||||
// set minTTL to 1 ms
|
|
||||||
let kvc = kvCache.create(0, 10);
|
|
||||||
kvc = kvCache.set(kvc, "a", []);
|
|
||||||
await timeout(20);
|
|
||||||
["b", "c"].forEach(k => {
|
|
||||||
kvc = kvCache.set(kvc, k, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
b: expect.arrayContaining([]),
|
|
||||||
c: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.not.objectContaining({
|
|
||||||
a: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("flush - minTTL and lowWatermark", async () => {
|
|
||||||
/*
|
|
||||||
verify minTTL functions correctly
|
|
||||||
*/
|
|
||||||
|
|
||||||
// set lowwatermark to 3, minTTL to 1 ms
|
|
||||||
let kvc = kvCache.create(3, 10);
|
|
||||||
kvc = kvCache.set(kvc, "a", []);
|
|
||||||
// delay
|
|
||||||
await timeout(20);
|
|
||||||
["b", "c"].forEach(k => {
|
|
||||||
kvc = kvCache.set(kvc, k, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
a: expect.arrayContaining([]),
|
|
||||||
b: expect.arrayContaining([]),
|
|
||||||
c: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
kvc = kvCache.set(kvc, "d", []);
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
b: expect.arrayContaining([]),
|
|
||||||
c: expect.arrayContaining([]),
|
|
||||||
d: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.not.objectContaining({
|
|
||||||
a: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("manual flush", async () => {
|
|
||||||
let kvc = kvCache.create(1, 10);
|
|
||||||
["a", "b", "c", "d"].forEach(k => {
|
|
||||||
kvc = kvCache.set(kvc, k, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Before TTL has expired, should have all values in cache.
|
|
||||||
expect(kvc).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
a: expect.arrayContaining([]),
|
|
||||||
b: expect.arrayContaining([]),
|
|
||||||
c: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// let TTL expire
|
|
||||||
await timeout(10);
|
|
||||||
|
|
||||||
// manually flush
|
|
||||||
const postFlushKvc = kvCache.flush(kvc);
|
|
||||||
expect(postFlushKvc).toBeDefined();
|
|
||||||
expect(postFlushKvc).not.toBe(kvc);
|
|
||||||
expect(postFlushKvc).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
d: expect.arrayContaining([])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -157,16 +157,6 @@ const anAnnotationsVarFBSResponse = (() => {
|
|||||||
return encodeMatrix(columns, anAnnotationsVarJSONResponse.names);
|
return encodeMatrix(columns, anAnnotationsVarJSONResponse.names);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const aLayoutJSONResponse = {
|
|
||||||
layout: {
|
|
||||||
ndims: 2,
|
|
||||||
coordinates: _()
|
|
||||||
.range(nObs)
|
|
||||||
.map(idx => [idx, Math.random(), Math.random()])
|
|
||||||
.value()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const aLayoutFBSResponse = (() => {
|
const aLayoutFBSResponse = (() => {
|
||||||
const coords = [
|
const coords = [
|
||||||
new Float32Array(nObs).fill(Math.random()),
|
new Float32Array(nObs).fill(Math.random()),
|
||||||
@@ -190,7 +180,7 @@ const aLayoutFBSResponse = (() => {
|
|||||||
|
|
||||||
NetEncoding.Matrix.startMatrix(builder);
|
NetEncoding.Matrix.startMatrix(builder);
|
||||||
NetEncoding.Matrix.addNRows(builder, nObs);
|
NetEncoding.Matrix.addNRows(builder, nObs);
|
||||||
NetEncoding.Matrix.addNCols(builder, nVar);
|
NetEncoding.Matrix.addNCols(builder, coords.length);
|
||||||
NetEncoding.Matrix.addColumns(builder, columns);
|
NetEncoding.Matrix.addColumns(builder, columns);
|
||||||
const matrix = NetEncoding.Matrix.endMatrix(builder);
|
const matrix = NetEncoding.Matrix.endMatrix(builder);
|
||||||
builder.finish(matrix);
|
builder.finish(matrix);
|
||||||
|
|||||||
@@ -1,280 +0,0 @@
|
|||||||
import summarizeAnnotations from "../../../src/util/stateManager/summarizeAnnotations";
|
|
||||||
|
|
||||||
describe("summarizeAnnotations", () => {
|
|
||||||
const schema = {
|
|
||||||
annotations: {
|
|
||||||
obs: [
|
|
||||||
{ name: "name", type: "string" },
|
|
||||||
{ name: "nameString", type: "string" },
|
|
||||||
{ name: "nameBoolean", type: "boolean" },
|
|
||||||
{ name: "nameFloat32", type: "float32" },
|
|
||||||
{ name: "nameInt32", type: "int32" },
|
|
||||||
{
|
|
||||||
name: "nameCategorical",
|
|
||||||
type: "categorical",
|
|
||||||
categories: [true, false, 1, 0, 0.00001, 4383.4833, "test", "", "0"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
var: [{ name: "name", type: "string" }]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test("empty test", () => {
|
|
||||||
const summary = summarizeAnnotations(schema, [], []);
|
|
||||||
expect(summary).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
obs: {
|
|
||||||
nameString: {
|
|
||||||
categorical: true,
|
|
||||||
categories: [],
|
|
||||||
categoryCounts: new Map(),
|
|
||||||
numCategories: 0
|
|
||||||
},
|
|
||||||
nameBoolean: {
|
|
||||||
categorical: true,
|
|
||||||
categories: [],
|
|
||||||
categoryCounts: new Map(),
|
|
||||||
numCategories: 0
|
|
||||||
},
|
|
||||||
nameFloat32: {
|
|
||||||
categorical: false,
|
|
||||||
range: {
|
|
||||||
max: undefined,
|
|
||||||
min: undefined,
|
|
||||||
nan: 0,
|
|
||||||
ninf: 0,
|
|
||||||
pinf: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nameInt32: {
|
|
||||||
categorical: false,
|
|
||||||
range: {
|
|
||||||
max: undefined,
|
|
||||||
min: undefined,
|
|
||||||
nan: 0,
|
|
||||||
ninf: 0,
|
|
||||||
pinf: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nameCategorical: {
|
|
||||||
categorical: true,
|
|
||||||
categories: [],
|
|
||||||
categoryCounts: new Map(),
|
|
||||||
numCategories: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
var: {}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("simple test", () => {
|
|
||||||
const obsAnnotations = [
|
|
||||||
{
|
|
||||||
__index__: 0,
|
|
||||||
name: "n1",
|
|
||||||
nameString: "hi",
|
|
||||||
nameBoolean: true,
|
|
||||||
nameFloat32: 39.3,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: 1
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const varAnnotations = [];
|
|
||||||
|
|
||||||
const summary = summarizeAnnotations(
|
|
||||||
schema,
|
|
||||||
obsAnnotations,
|
|
||||||
varAnnotations
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(summary).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
obs: {
|
|
||||||
nameString: {
|
|
||||||
categorical: true,
|
|
||||||
categories: ["hi"],
|
|
||||||
categoryCounts: new Map([["hi", 1]]),
|
|
||||||
numCategories: 1
|
|
||||||
},
|
|
||||||
nameBoolean: {
|
|
||||||
categorical: true,
|
|
||||||
categories: [true],
|
|
||||||
categoryCounts: new Map([[true, 1]]),
|
|
||||||
numCategories: 1
|
|
||||||
},
|
|
||||||
nameFloat32: {
|
|
||||||
categorical: false,
|
|
||||||
range: { min: 39.3, max: 39.3, nan: 0, ninf: 0, pinf: 0 }
|
|
||||||
},
|
|
||||||
nameInt32: {
|
|
||||||
categorical: false,
|
|
||||||
range: { min: 99, max: 99, nan: 0, ninf: 0, pinf: 0 }
|
|
||||||
},
|
|
||||||
nameCategorical: {
|
|
||||||
categorical: true,
|
|
||||||
categories: [1],
|
|
||||||
categoryCounts: new Map([[1, 1]]),
|
|
||||||
numCategories: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
var: {}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("multi test", () => {
|
|
||||||
const obsAnnotations = [
|
|
||||||
{
|
|
||||||
__index__: 0,
|
|
||||||
name: "n0",
|
|
||||||
nameString: "hi",
|
|
||||||
nameBoolean: false,
|
|
||||||
nameFloat32: 39.3,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
__index__: 1,
|
|
||||||
name: "n1",
|
|
||||||
nameString: "hi",
|
|
||||||
nameBoolean: true,
|
|
||||||
nameFloat32: 39.3,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
__index__: 2,
|
|
||||||
name: "n2",
|
|
||||||
nameString: "bye",
|
|
||||||
nameBoolean: true,
|
|
||||||
nameFloat32: 0,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: "0"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const varAnnotations = [];
|
|
||||||
|
|
||||||
const summary = summarizeAnnotations(
|
|
||||||
schema,
|
|
||||||
obsAnnotations,
|
|
||||||
varAnnotations
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(summary).toMatchObject(
|
|
||||||
expect.objectContaining({
|
|
||||||
obs: {
|
|
||||||
nameString: {
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining(["hi", "bye"]),
|
|
||||||
categoryCounts: new Map([["hi", 2], ["bye", 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
},
|
|
||||||
nameBoolean: {
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([true, false]),
|
|
||||||
categoryCounts: new Map([[true, 2], [false, 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
},
|
|
||||||
nameFloat32: {
|
|
||||||
categorical: false,
|
|
||||||
range: { min: 0, max: 39.3, nan: 0, ninf: 0, pinf: 0 }
|
|
||||||
},
|
|
||||||
nameInt32: {
|
|
||||||
categorical: false,
|
|
||||||
range: { min: 99, max: 99, nan: 0, ninf: 0, pinf: 0 }
|
|
||||||
},
|
|
||||||
nameCategorical: {
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([1, false, "0"]),
|
|
||||||
categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]),
|
|
||||||
numCategories: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
var: {}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("non-finite numbers", () => {
|
|
||||||
const obsAnnotations = [
|
|
||||||
{
|
|
||||||
__index__: 0,
|
|
||||||
name: "n0",
|
|
||||||
nameString: "hi",
|
|
||||||
nameBoolean: false,
|
|
||||||
nameFloat32: 39.3,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
__index__: 1,
|
|
||||||
name: "n1",
|
|
||||||
nameString: "hi",
|
|
||||||
nameBoolean: true,
|
|
||||||
nameFloat32: Number.NEGATIVE_INFINITY,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
__index__: 2,
|
|
||||||
name: "n2",
|
|
||||||
nameString: "bye",
|
|
||||||
nameBoolean: true,
|
|
||||||
nameFloat32: Number.NaN,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
__index__: 3,
|
|
||||||
name: "n2",
|
|
||||||
nameString: "bye",
|
|
||||||
nameBoolean: true,
|
|
||||||
nameFloat32: Number.POSITIVE_INFINITY,
|
|
||||||
nameInt32: 99,
|
|
||||||
nameCategorical: "0"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const varAnnotations = [];
|
|
||||||
|
|
||||||
const summary = summarizeAnnotations(
|
|
||||||
schema,
|
|
||||||
obsAnnotations,
|
|
||||||
varAnnotations
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(summary).toMatchObject(
|
|
||||||
expect.objectContaining({
|
|
||||||
obs: {
|
|
||||||
nameString: {
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining(["hi", "bye"]),
|
|
||||||
categoryCounts: new Map([["hi", 2], ["bye", 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
},
|
|
||||||
nameBoolean: {
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([true, false]),
|
|
||||||
categoryCounts: new Map([[true, 2], [false, 1]]),
|
|
||||||
numCategories: 2
|
|
||||||
},
|
|
||||||
nameFloat32: {
|
|
||||||
categorical: false,
|
|
||||||
range: { min: 39.3, max: 39.3, nan: 1, ninf: 1, pinf: 1 }
|
|
||||||
},
|
|
||||||
nameInt32: {
|
|
||||||
categorical: false,
|
|
||||||
range: { min: 99, max: 99, nan: 0, ninf: 0, pinf: 0 }
|
|
||||||
},
|
|
||||||
nameCategorical: {
|
|
||||||
categorical: true,
|
|
||||||
categories: expect.arrayContaining([1, false, "0"]),
|
|
||||||
categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]),
|
|
||||||
numCategories: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
var: {}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import * as Universe from "../../../src/util/stateManager/universe";
|
import * as Universe from "../../../src/util/stateManager/universe";
|
||||||
|
import * as Dataframe from "../../../src/util/dataframe";
|
||||||
import * as REST from "./sampleResponses";
|
import * as REST from "./sampleResponses";
|
||||||
|
|
||||||
describe("createUniverseFromRestV02Response", () => {
|
describe("createUniverseFromResponse", () => {
|
||||||
/*
|
/*
|
||||||
test createUniverseFromRestV02Response - this function converts
|
test createUniverseFromResponse - this function converts
|
||||||
a set of REST 0.2 responses into a "new" Universe.
|
a set of REST 0.2 responses into a "new" Universe.
|
||||||
|
|
||||||
createUniverseFromRestV02Response(
|
createUniverseFromResponse(
|
||||||
configResponse,
|
configResponse,
|
||||||
schemaResponse,
|
schemaResponse,
|
||||||
annotationsObsResponse,
|
annotationsObsResponse,
|
||||||
@@ -30,7 +30,7 @@ describe("createUniverseFromRestV02Response", () => {
|
|||||||
create a universe from sample data nad validate its shape & contents
|
create a universe from sample data nad validate its shape & contents
|
||||||
*/
|
*/
|
||||||
const { nObs, nVar } = REST.schema.schema.dataframe;
|
const { nObs, nVar } = REST.schema.schema.dataframe;
|
||||||
const universe = Universe.createUniverseFromRestV02Response(
|
const universe = Universe.createUniverseFromResponse(
|
||||||
REST.config,
|
REST.config,
|
||||||
REST.schema,
|
REST.schema,
|
||||||
REST.annotationsObs,
|
REST.annotationsObs,
|
||||||
@@ -41,27 +41,26 @@ describe("createUniverseFromRestV02Response", () => {
|
|||||||
expect(universe).toBeDefined();
|
expect(universe).toBeDefined();
|
||||||
expect(universe).toMatchObject(
|
expect(universe).toMatchObject(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
api: "0.2",
|
|
||||||
nObs,
|
nObs,
|
||||||
nVar,
|
nVar,
|
||||||
schema: REST.schema.schema,
|
schema: REST.schema.schema,
|
||||||
obsAnnotations: expect.any(Array),
|
obsAnnotations: expect.any(Dataframe.Dataframe),
|
||||||
varAnnotations: expect.any(Array),
|
varAnnotations: expect.any(Dataframe.Dataframe),
|
||||||
obsNameToIndexMap: expect.any(Object),
|
obsLayout: expect.any(Dataframe.Dataframe),
|
||||||
varNameToIndexMap: expect.any(Object),
|
varData: expect.any(Dataframe.Dataframe)
|
||||||
obsLayout: expect.objectContaining({
|
|
||||||
X: expect.any(Float32Array),
|
|
||||||
Y: expect.any(Float32Array)
|
|
||||||
}),
|
|
||||||
varDataCache: expect.any(Object)
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(universe.obsAnnotations).toHaveLength(nObs);
|
expect(universe.obsAnnotations.dims).toEqual([
|
||||||
expect(_.keys(universe.obsNameToIndexMap)).toHaveLength(nObs);
|
nObs,
|
||||||
expect(universe.obsLayout.X).toHaveLength(nObs);
|
REST.schema.schema.annotations.obs.length
|
||||||
expect(universe.obsLayout.Y).toHaveLength(nObs);
|
]);
|
||||||
expect(universe.varAnnotations).toHaveLength(nVar);
|
expect(universe.obsLayout.dims).toEqual([nObs, 2]);
|
||||||
expect(_.keys(universe.varNameToIndexMap)).toHaveLength(nVar);
|
expect(universe.obsLayout.colIndex.keys()).toEqual(["X", "Y"]);
|
||||||
|
expect(universe.varAnnotations.dims).toEqual([
|
||||||
|
nVar,
|
||||||
|
REST.schema.schema.annotations.var.length
|
||||||
|
]);
|
||||||
|
expect(universe.varData.isEmpty()).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import * as Universe from "../../../src/util/stateManager/universe";
|
import * as Universe from "../../../src/util/stateManager/universe";
|
||||||
import * as World from "../../../src/util/stateManager/world";
|
import * as World from "../../../src/util/stateManager/world";
|
||||||
|
import * as Dataframe from "../../../src/util/dataframe";
|
||||||
import Crossfilter from "../../../src/util/typedCrossfilter";
|
import Crossfilter from "../../../src/util/typedCrossfilter";
|
||||||
import * as REST from "./sampleResponses";
|
import * as REST from "./sampleResponses";
|
||||||
import {
|
import {
|
||||||
obsAnnoDimensionName,
|
obsAnnoDimensionName,
|
||||||
layoutDimensionName
|
layoutDimensionName
|
||||||
} from "../../../src/util/nameCreators";
|
} from "../../../src/util/nameCreators";
|
||||||
import * as kvCache from "../../../src/util/stateManager/keyvalcache";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Helper - creates universe, world, corssfilter and dimensionMap from
|
Helper - creates universe, world, corssfilter and dimensionMap from
|
||||||
@@ -16,7 +16,7 @@ the default REST test response.
|
|||||||
const defaultBigBang = () => {
|
const defaultBigBang = () => {
|
||||||
/* create unverse, world, crossfilter and dimensionMap */
|
/* create unverse, world, crossfilter and dimensionMap */
|
||||||
/* create universe */
|
/* create universe */
|
||||||
const universe = Universe.createUniverseFromRestV02Response(
|
const universe = Universe.createUniverseFromResponse(
|
||||||
REST.config,
|
REST.config,
|
||||||
REST.schema,
|
REST.schema,
|
||||||
REST.annotationsObs,
|
REST.annotationsObs,
|
||||||
@@ -40,7 +40,7 @@ const defaultBigBang = () => {
|
|||||||
|
|
||||||
describe("createWorldFromEntireUniverse", () => {
|
describe("createWorldFromEntireUniverse", () => {
|
||||||
test("create from REST sample", () => {
|
test("create from REST sample", () => {
|
||||||
const universe = Universe.createUniverseFromRestV02Response(
|
const universe = Universe.createUniverseFromResponse(
|
||||||
REST.config,
|
REST.config,
|
||||||
REST.schema,
|
REST.schema,
|
||||||
REST.annotationsObs,
|
REST.annotationsObs,
|
||||||
@@ -54,31 +54,13 @@ describe("createWorldFromEntireUniverse", () => {
|
|||||||
|
|
||||||
expect(world).toMatchObject(
|
expect(world).toMatchObject(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
api: "0.2",
|
|
||||||
nObs: universe.nObs,
|
nObs: universe.nObs,
|
||||||
nVar: universe.nVar,
|
nVar: universe.nVar,
|
||||||
schema: universe.schema,
|
schema: universe.schema,
|
||||||
obsAnnotations: universe.obsAnnotations,
|
obsAnnotations: universe.obsAnnotations,
|
||||||
varAnnotations: universe.varAnnotations,
|
varAnnotations: universe.varAnnotations,
|
||||||
obsLayout: universe.obsLayout,
|
obsLayout: universe.obsLayout,
|
||||||
|
varData: expect.any(Dataframe.Dataframe)
|
||||||
summary: expect.objectContaining({
|
|
||||||
obs: _(REST.schema.schema.annotations.obs)
|
|
||||||
.filter(v => v.name !== "name")
|
|
||||||
.keyBy("name")
|
|
||||||
.mapValues(() => expect.any(Object))
|
|
||||||
.value(),
|
|
||||||
var: _(REST.schema.schema.annotations.var)
|
|
||||||
.filter(v => v.name !== "name")
|
|
||||||
.keyBy("name")
|
|
||||||
.mapValues(() => expect.any(Object))
|
|
||||||
.value()
|
|
||||||
}),
|
|
||||||
|
|
||||||
varDataCache: expect.any(Object),
|
|
||||||
|
|
||||||
obsIndex: null, // null indicating full universe
|
|
||||||
obsBackIndex: null
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -111,51 +93,38 @@ describe("createWorldFromCurrentSelection", () => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* matchFilter must match the dimension filters above */
|
/* matchFilter must match the dimension filters above */
|
||||||
const matchFilter = val => val.field1 >= 0 && val.field1 < 5 && !val.field3;
|
const matchFilter = (df, row) => {
|
||||||
const universeIndices = _()
|
const field1 = df.at(row, "field1");
|
||||||
.range(universe.nObs)
|
const field3 = df.at(row, "field3");
|
||||||
.filter(idx => matchFilter(universe.obsAnnotations[idx]))
|
return field1 >= 0 && field1 < 5 && !field3;
|
||||||
.value();
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
nObs: universeIndices.length,
|
|
||||||
obsAnnotations: _.map(universeIndices, i => universe.obsAnnotations[i]),
|
|
||||||
obsLayout: {
|
|
||||||
X: new Float32Array(
|
|
||||||
_.map(universeIndices, i => universe.obsLayout.X[i])
|
|
||||||
),
|
|
||||||
Y: new Float32Array(
|
|
||||||
_.map(universeIndices, i => universe.obsLayout.Y[i])
|
|
||||||
)
|
|
||||||
},
|
|
||||||
obsBackIndex: _.transform(
|
|
||||||
universeIndices,
|
|
||||||
(result, univIdx, worldIdx) => {
|
|
||||||
result[univIdx] = worldIdx;
|
|
||||||
},
|
|
||||||
new Uint32Array(universe.nObs).fill(-1)
|
|
||||||
),
|
|
||||||
obsIndex: new Uint32Array(universeIndices)
|
|
||||||
};
|
};
|
||||||
|
const matchingIndices = _()
|
||||||
|
.range(universe.nObs)
|
||||||
|
.filter(idx => matchFilter(universe.obsAnnotations, idx))
|
||||||
|
.value();
|
||||||
|
|
||||||
expect(world).toMatchObject(
|
expect(world).toMatchObject(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
api: "0.2",
|
nObs: matchingIndices.length,
|
||||||
nObs: expected.nObs,
|
|
||||||
nVar: universe.nVar,
|
nVar: universe.nVar,
|
||||||
schema: universe.schema,
|
schema: universe.schema,
|
||||||
obsAnnotations: expected.obsAnnotations,
|
obsAnnotations: expect.any(Dataframe.Dataframe),
|
||||||
varAnnotations: universe.varAnnotations,
|
varAnnotations: universe.varAnnotations,
|
||||||
obsLayout: expected.obsLayout,
|
obsLayout: expect.any(Dataframe.Dataframe),
|
||||||
summary: {
|
varData: expect.any(Dataframe.Dataframe)
|
||||||
obs: expect.any(Object) /* we could do better! */,
|
|
||||||
var: expect.any(Object) /* we could do better! */
|
|
||||||
},
|
|
||||||
varDataCache: expect.any(Object),
|
|
||||||
obsIndex: expected.obsIndex,
|
|
||||||
obsBackIndex: expected.obsBackIndex
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(["X", "Y"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,58 +162,14 @@ describe("createObsDimensionMap", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("subsetVarData", () => {
|
describe("createVarDataDimension", () => {
|
||||||
test("when world eq universe", () => {
|
|
||||||
const { universe, world } = defaultBigBang();
|
|
||||||
/* create a mock varData array for subsetting */
|
|
||||||
const sourceVarData = new Float32Array(universe.nObs);
|
|
||||||
|
|
||||||
/* expect literally the same object back */
|
|
||||||
const result = World.subsetVarData(world, universe, sourceVarData);
|
|
||||||
expect(result).toBe(sourceVarData);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when world neq universe", () => {
|
|
||||||
const { universe, world, crossfilter, dimensionMap } = defaultBigBang();
|
|
||||||
/* create a mock varData array for subsetting */
|
|
||||||
const sourceVarData = Float32Array.from(_.range(universe.nObs));
|
|
||||||
|
|
||||||
/* mock a selection */
|
|
||||||
dimensionMap[obsAnnoDimensionName("field1")].filterRange([0, 5]);
|
|
||||||
dimensionMap[obsAnnoDimensionName("field3")].filterExact(false);
|
|
||||||
|
|
||||||
/* create the world from the selection */
|
|
||||||
const newWorld = World.createWorldFromCurrentSelection(
|
|
||||||
universe,
|
|
||||||
world,
|
|
||||||
crossfilter
|
|
||||||
);
|
|
||||||
expect(newWorld.obsIndex).toMatchObject(new Uint32Array([0, 2]));
|
|
||||||
|
|
||||||
/* expect a subset */
|
|
||||||
const result = World.subsetVarData(newWorld, universe, sourceVarData);
|
|
||||||
expect(result).not.toBe(sourceVarData);
|
|
||||||
expect(result).toHaveLength(newWorld.nObs);
|
|
||||||
/* check that we have expected source var content */
|
|
||||||
expect(result).toMatchObject(new Float32Array([0, 2]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("createVarDimension", () => {
|
|
||||||
/* create default universe */
|
/* create default universe */
|
||||||
const { world, crossfilter } = defaultBigBang();
|
const { world, crossfilter } = defaultBigBang();
|
||||||
/* create a mock var data cache */
|
world.varData = world.varData.withCol(
|
||||||
const varDataCache = kvCache.set(
|
|
||||||
kvCache.create(),
|
|
||||||
"GENE",
|
"GENE",
|
||||||
Float32Array.from(_.range(world.nObs))
|
Float32Array.from(_.range(world.nObs))
|
||||||
);
|
);
|
||||||
const result = World.createVarDimension(
|
const result = World.createVarDataDimension(world, crossfilter, "GENE");
|
||||||
world,
|
|
||||||
varDataCache,
|
|
||||||
crossfilter,
|
|
||||||
"GENE"
|
|
||||||
);
|
|
||||||
expect(result).toBeInstanceOf(Crossfilter.ScalarDimension);
|
expect(result).toBeInstanceOf(Crossfilter.ScalarDimension);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,27 @@ import {
|
|||||||
countCategoryValues2D,
|
countCategoryValues2D,
|
||||||
clearCaches
|
clearCaches
|
||||||
} from "../../../src/util/stateManager/worldUtil";
|
} from "../../../src/util/stateManager/worldUtil";
|
||||||
|
import * as Dataframe from "../../../src/util/dataframe";
|
||||||
|
|
||||||
describe("WorldUtil cache management", () => {
|
describe("WorldUtil cache management", () => {
|
||||||
test("empty", () => {
|
test("empty", () => {
|
||||||
const count = countCategoryValues2D("a", "b", []);
|
const count = countCategoryValues2D(
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
new Dataframe.Dataframe([0, 0], [])
|
||||||
|
);
|
||||||
expect(count).toMatchObject(new Map());
|
expect(count).toMatchObject(new Map());
|
||||||
|
expect(count.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("simple couts", () => {
|
test("simple couts", () => {
|
||||||
const rows = [{ a: 0, b: false }, { a: 0, b: true }, { a: 1, b: false }];
|
const df = new Dataframe.Dataframe(
|
||||||
const count = countCategoryValues2D("a", "b", rows);
|
[3, 2],
|
||||||
|
[[0, 0, 1], [false, true, false]],
|
||||||
|
null,
|
||||||
|
new Dataframe.KeyIndex(["a", "b"])
|
||||||
|
);
|
||||||
|
const count = countCategoryValues2D("a", "b", df);
|
||||||
expect(count).toMatchObject(
|
expect(count).toMatchObject(
|
||||||
new Map([
|
new Map([
|
||||||
[0, new Map([[true, 1], [false, 1]])],
|
[0, new Map([[true, 1], [false, 1]])],
|
||||||
@@ -22,16 +33,22 @@ describe("WorldUtil cache management", () => {
|
|||||||
|
|
||||||
test("memo cache clear", () => {
|
test("memo cache clear", () => {
|
||||||
clearCaches();
|
clearCaches();
|
||||||
const row1 = [];
|
const df1 = new Dataframe.Dataframe([0, 0], []);
|
||||||
const row2 = [{ a: 0, b: false }, { a: 0, b: true }, { a: 1, b: false }];
|
const df2 = new Dataframe.Dataframe(
|
||||||
const count1 = countCategoryValues2D("a", "b", row1);
|
[3, 2],
|
||||||
const count2 = countCategoryValues2D("a", "b", row1);
|
[[0, 0, 1], [false, true, false]],
|
||||||
const count3 = countCategoryValues2D("a", "b", []);
|
null,
|
||||||
const count4 = countCategoryValues2D("a", "b", row2);
|
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();
|
clearCaches();
|
||||||
const count10 = countCategoryValues2D("a", "b", row1);
|
const count10 = countCategoryValues2D("a", "b", df1);
|
||||||
const count11 = countCategoryValues2D("a", "b", row2);
|
const count11 = countCategoryValues2D("a", "b", df2);
|
||||||
|
|
||||||
expect(count1).toEqual(count2);
|
expect(count1).toEqual(count2);
|
||||||
expect(count1).toEqual(count3);
|
expect(count1).toEqual(count3);
|
||||||
|
|||||||
@@ -118,16 +118,16 @@ describe("selectionCount", () => {
|
|||||||
const dim2 = ba.allocDimension();
|
const dim2 = ba.allocDimension();
|
||||||
expect(dim2).toBeDefined();
|
expect(dim2).toBeDefined();
|
||||||
|
|
||||||
expect(ba.selectionCount).toEqual(0);
|
expect(ba.selectionCount()).toEqual(0);
|
||||||
ba.selectAll(dim1);
|
ba.selectAll(dim1);
|
||||||
expect(ba.selectionCount).toEqual(0);
|
expect(ba.selectionCount()).toEqual(0);
|
||||||
ba.selectAll(dim2);
|
ba.selectAll(dim2);
|
||||||
expect(ba.selectionCount).toEqual(defaultTestLength);
|
expect(ba.selectionCount()).toEqual(defaultTestLength);
|
||||||
|
|
||||||
for (let i = 0; i < defaultTestLength; i += 1) {
|
for (let i = 0; i < defaultTestLength; i += 1) {
|
||||||
ba.deselectOne(dim1, i);
|
ba.deselectOne(dim1, i);
|
||||||
expect(ba.selectionCount).toEqual(defaultTestLength - i - 1);
|
expect(ba.selectionCount()).toEqual(defaultTestLength - i - 1);
|
||||||
expect(ba.selectionCount).toEqual(ba.countAllOnes());
|
expect(ba.selectionCount()).toEqual(ba.countAllOnes());
|
||||||
}
|
}
|
||||||
|
|
||||||
ba.freeDimension(dim1);
|
ba.freeDimension(dim1);
|
||||||
|
|||||||
@@ -119,14 +119,22 @@ function groupReduce(data, valueMap, valueReduce, valueInit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupCount(data, map) {
|
function groupCount(data, map) {
|
||||||
return groupReduce(data, map, (p, v) => p + 1, () => 0);
|
return groupReduce(data, map, p => p + 1, () => 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupSum(data, map) {
|
function groupSum(data, map) {
|
||||||
return groupReduce(data, map, (p, v) => (p += map(v)), () => 0);
|
return groupReduce(
|
||||||
|
data,
|
||||||
|
map,
|
||||||
|
(p, v) => {
|
||||||
|
p += map(v);
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
() => 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var payments = null;
|
let payments = null;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
payments = crossfilter(someData);
|
payments = crossfilter(someData);
|
||||||
});
|
});
|
||||||
@@ -139,7 +147,7 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
const quantity = payments.dimension(
|
const quantity = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.quantity,
|
(i, data) => data[i].quantity,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
expect(quantity).toBeDefined();
|
expect(quantity).toBeDefined();
|
||||||
@@ -154,20 +162,23 @@ describe("typedCrossfilter", () => {
|
|||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(
|
const quantity = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.quantity,
|
(i, data) => data[i].quantity,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const total = payments.dimension(
|
const total = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].total,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
|
|
||||||
expect(quantity).toBeDefined();
|
expect(quantity).toBeDefined();
|
||||||
expect(tip).toBeDefined();
|
expect(tip).toBeDefined();
|
||||||
@@ -214,20 +225,18 @@ describe("typedCrossfilter", () => {
|
|||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(
|
const quantity = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.quantity,
|
(i, data) => data[i].quantity,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const total = payments.dimension(
|
const type = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.EnumDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].type
|
||||||
Float32Array
|
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
|
||||||
|
|
||||||
quantity.filterExact(1);
|
quantity.filterExact(1);
|
||||||
expect(payments.countFiltered()).toEqual(
|
expect(payments.countFiltered()).toEqual(
|
||||||
@@ -250,20 +259,23 @@ describe("typedCrossfilter", () => {
|
|||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(
|
const quantity = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.quantity,
|
(i, data) => data[i].quantity,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const total = payments.dimension(
|
const total = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].total,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
|
|
||||||
tip.filterRange([0, 91]);
|
tip.filterRange([0, 91]);
|
||||||
expect(payments.allFiltered()).toEqual(
|
expect(payments.allFiltered()).toEqual(
|
||||||
@@ -291,20 +303,23 @@ describe("typedCrossfilter", () => {
|
|||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(
|
const quantity = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.quantity,
|
(i, data) => data[i].quantity,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const total = payments.dimension(
|
const total = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].total,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
|
|
||||||
type.filterEnum(["tab", "cash"]);
|
type.filterEnum(["tab", "cash"]);
|
||||||
expect(payments.allFiltered()).toEqual(
|
expect(payments.allFiltered()).toEqual(
|
||||||
@@ -326,27 +341,30 @@ describe("typedCrossfilter", () => {
|
|||||||
expect(payments).toBeDefined();
|
expect(payments).toBeDefined();
|
||||||
const quantity = payments.dimension(
|
const quantity = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.quantity,
|
(i, data) => data[i].quantity,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const total = payments.dimension(
|
const total = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].total,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
|
|
||||||
// Create a bunch of fake dimensions to ensure we can handle > 32
|
// Create a bunch of fake dimensions to ensure we can handle > 32
|
||||||
let dimMap = {};
|
let dimMap = {};
|
||||||
for (let i = 0; i < 65; i++) {
|
for (let i = 0; i < 65; i++) {
|
||||||
dimMap[i] = payments.dimension(
|
dimMap[i] = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => Math.random(),
|
() => Math.random(),
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
expect(dimMap[i]).toBeDefined();
|
expect(dimMap[i]).toBeDefined();
|
||||||
@@ -372,18 +390,21 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
const quantity = payments.dimension(
|
const quantity = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.quantity,
|
(i, data) => data[i].quantity,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
const total = payments.dimension(
|
const total = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].total,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -411,15 +432,18 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const totalX10 = payments.dimension(
|
const totalX10 = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total * 10,
|
(i, data) => data[i].total * 10,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
|
|
||||||
const paymentsByTip_A = tip.group();
|
const paymentsByTip_A = tip.group();
|
||||||
const paymentsByTip_B = tip.group(r => 10 * r);
|
const paymentsByTip_B = tip.group(r => 10 * r);
|
||||||
@@ -458,10 +482,13 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
const total = payments.dimension(
|
const total = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].total,
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
|
|
||||||
const paymentsByTotal = total.group();
|
const paymentsByTotal = total.group();
|
||||||
const paymentsByType = type.group();
|
const paymentsByType = type.group();
|
||||||
@@ -499,15 +526,18 @@ describe("typedCrossfilter", () => {
|
|||||||
|
|
||||||
const tip = payments.dimension(
|
const tip = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.tip,
|
(i, data) => data[i].tip,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const total = payments.dimension(
|
const total = payments.dimension(
|
||||||
crossfilter.ScalarDimension,
|
crossfilter.ScalarDimension,
|
||||||
r => r.total,
|
(i, data) => data[i].total,
|
||||||
Int32Array
|
Int32Array
|
||||||
);
|
);
|
||||||
const type = payments.dimension(crossfilter.EnumDimension, r => r.type);
|
const type = payments.dimension(
|
||||||
|
crossfilter.EnumDimension,
|
||||||
|
(i, data) => data[i].type
|
||||||
|
);
|
||||||
|
|
||||||
const paymentsByTip = tip.group();
|
const paymentsByTip = tip.group();
|
||||||
const paymentsByTotal = total.group();
|
const paymentsByTotal = total.group();
|
||||||
|
|||||||
Generated
+368
-12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cellxgene",
|
"name": "cellxgene",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1296,6 +1296,15 @@
|
|||||||
"integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==",
|
"integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"agent-base": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"es6-promisify": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ajv": {
|
"ajv": {
|
||||||
"version": "6.5.4",
|
"version": "6.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz",
|
||||||
@@ -2807,6 +2816,12 @@
|
|||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"check-more-types": {
|
||||||
|
"version": "2.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
|
||||||
|
"integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
|
||||||
@@ -4118,6 +4133,12 @@
|
|||||||
"is-obj": "^1.0.0"
|
"is-obj": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"duplexer": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"duplexer3": {
|
"duplexer3": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||||
@@ -4278,6 +4299,15 @@
|
|||||||
"integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==",
|
"integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"es6-promisify": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||||
|
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"es6-promise": "^4.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"escape-html": {
|
"escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
@@ -4767,6 +4797,21 @@
|
|||||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
|
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"event-stream": {
|
||||||
|
"version": "3.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
|
||||||
|
"integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"duplexer": "~0.1.1",
|
||||||
|
"from": "~0",
|
||||||
|
"map-stream": "~0.1.0",
|
||||||
|
"pause-stream": "0.0.11",
|
||||||
|
"split": "0.3",
|
||||||
|
"stream-combiner": "~0.0.4",
|
||||||
|
"through": "~2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"events": {
|
"events": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||||
@@ -4963,6 +5008,35 @@
|
|||||||
"is-extglob": "^1.0.0"
|
"is-extglob": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extract-zip": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
|
||||||
|
"integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"concat-stream": "1.6.2",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"mkdirp": "0.5.1",
|
||||||
|
"yauzl": "2.4.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"extsprintf": {
|
"extsprintf": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||||
@@ -5023,6 +5097,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fd-slicer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"pend": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"figures": {
|
"figures": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
|
||||||
@@ -5229,6 +5312,12 @@
|
|||||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
|
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"from": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
|
||||||
|
"integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"from2": {
|
"from2": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
|
||||||
@@ -5277,7 +5366,8 @@
|
|||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@@ -5298,12 +5388,14 @@
|
|||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -5318,17 +5410,20 @@
|
|||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -5445,7 +5540,8 @@
|
|||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@@ -5457,6 +5553,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -5471,6 +5568,7 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@@ -5478,12 +5576,14 @@
|
|||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@@ -5502,6 +5602,7 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@@ -5582,7 +5683,8 @@
|
|||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -5594,6 +5696,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@@ -5679,7 +5782,8 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@@ -5715,6 +5819,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@@ -5734,6 +5839,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
@@ -5777,12 +5883,14 @@
|
|||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6127,6 +6235,12 @@
|
|||||||
"minimalistic-crypto-utils": "^1.0.1"
|
"minimalistic-crypto-utils": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hoek": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"hoist-non-react-statics": {
|
"hoist-non-react-statics": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.1.0.tgz",
|
||||||
@@ -6295,6 +6409,16 @@
|
|||||||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"https-proxy-agent": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"agent-base": "^4.1.0",
|
||||||
|
"debug": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -6825,6 +6949,15 @@
|
|||||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"isemail": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"punycode": "2.x.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"isexe": {
|
"isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -9078,6 +9211,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"joi": {
|
||||||
|
"version": "13.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz",
|
||||||
|
"integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"hoek": "5.x.x",
|
||||||
|
"isemail": "3.x.x",
|
||||||
|
"topo": "3.x.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"js-levenshtein": {
|
"js-levenshtein": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz",
|
||||||
@@ -9240,6 +9384,12 @@
|
|||||||
"package-json": "^4.0.0"
|
"package-json": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lazy-ass": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"lcid": {
|
"lcid": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
|
||||||
@@ -9521,6 +9671,12 @@
|
|||||||
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"map-stream": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
|
||||||
|
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"map-visit": {
|
"map-visit": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
|
||||||
@@ -10533,6 +10689,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pause-stream": {
|
||||||
|
"version": "0.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
|
||||||
|
"integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"through": "~2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pbkdf2": {
|
"pbkdf2": {
|
||||||
"version": "3.0.17",
|
"version": "3.0.17",
|
||||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
|
||||||
@@ -10546,6 +10711,12 @@
|
|||||||
"sha.js": "^2.4.8"
|
"sha.js": "^2.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pend": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"performance-now": {
|
"performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
@@ -10791,12 +10962,27 @@
|
|||||||
"ipaddr.js": "1.8.0"
|
"ipaddr.js": "1.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proxy-from-env": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"prr": {
|
"prr": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||||
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
|
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"ps-tree": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"event-stream": "=3.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pseudomap": {
|
"pseudomap": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||||
@@ -10850,6 +11036,48 @@
|
|||||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"puppeteer": {
|
||||||
|
"version": "1.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.12.2.tgz",
|
||||||
|
"integrity": "sha512-xWSyCeD6EazGlfnQweMpM+Hs6X6PhUYhNTHKFj/axNZDq4OmrVERf70isBf7HsnFgB3zOC1+23/8+wCAZYg+Pg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"debug": "^4.1.0",
|
||||||
|
"extract-zip": "^1.6.6",
|
||||||
|
"https-proxy-agent": "^2.2.1",
|
||||||
|
"mime": "^2.0.3",
|
||||||
|
"progress": "^2.0.1",
|
||||||
|
"proxy-from-env": "^1.0.0",
|
||||||
|
"rimraf": "^2.6.1",
|
||||||
|
"ws": "^6.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "6.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
|
||||||
|
"integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"async-limiter": "~1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.5.2",
|
"version": "6.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||||
@@ -11817,6 +12045,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||||
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
|
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
|
||||||
},
|
},
|
||||||
|
"rx": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz",
|
||||||
|
"integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"rxjs": {
|
"rxjs": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
|
||||||
@@ -12637,6 +12871,15 @@
|
|||||||
"integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==",
|
"integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"split": {
|
||||||
|
"version": "0.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
|
||||||
|
"integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"through": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"split-string": {
|
"split-string": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||||
@@ -12684,6 +12927,63 @@
|
|||||||
"integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==",
|
"integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"start-server-and-test": {
|
||||||
|
"version": "1.7.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.7.11.tgz",
|
||||||
|
"integrity": "sha512-651SCOfhPT65Xjhecvx/ZMJs8UOd5VItjjmpYH95aM6Hr4P8N8UIcxEgDhY/aaVmthACH7qFTqs4EA/KHykjtw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"bluebird": "3.5.3",
|
||||||
|
"check-more-types": "2.24.0",
|
||||||
|
"debug": "3.2.6",
|
||||||
|
"execa": "0.11.0",
|
||||||
|
"lazy-ass": "1.6.0",
|
||||||
|
"ps-tree": "1.2.0",
|
||||||
|
"wait-on": "3.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"execa": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/execa/-/execa-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-k5AR22vCt1DcfeiRixW46U5tMLtBg44ssdJM9PiXw3D8Bn5qyxFCSnKY/eR22y+ctFDGPqafpaXg2G4Emyua4A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"cross-spawn": "^6.0.0",
|
||||||
|
"get-stream": "^4.0.0",
|
||||||
|
"is-stream": "^1.1.0",
|
||||||
|
"npm-run-path": "^2.0.0",
|
||||||
|
"p-finally": "^1.0.0",
|
||||||
|
"signal-exit": "^3.0.0",
|
||||||
|
"strip-eof": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get-stream": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"pump": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pump": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"static-extend": {
|
"static-extend": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
||||||
@@ -12727,6 +13027,15 @@
|
|||||||
"readable-stream": "^2.0.2"
|
"readable-stream": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"stream-combiner": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
|
||||||
|
"integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"duplexer": "~0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stream-each": {
|
"stream-each": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",
|
||||||
@@ -13174,6 +13483,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"topo": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"hoek": "6.x.x"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hoek": {
|
||||||
|
"version": "6.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz",
|
||||||
|
"integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"toposort": {
|
"toposort": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
|
||||||
@@ -13695,6 +14021,27 @@
|
|||||||
"browser-process-hrtime": "^0.1.2"
|
"browser-process-hrtime": "^0.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wait-on": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-QUGNKlKLDyY6W/qHdxaRlXUAgLPe+3mLL/tRByHpRNcHs/c7dZXbu+OnJWGNux6tU1WFh/Z8aEwvbuzSAu79Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"core-js": "^2.5.7",
|
||||||
|
"joi": "^13.0.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"request": "^2.88.0",
|
||||||
|
"rx": "^4.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"walker": {
|
"walker": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
|
||||||
@@ -14575,6 +14922,15 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"yauzl": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
|
||||||
|
"integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fd-slicer": "~1.0.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -1,17 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "cellxgene",
|
"name": "cellxgene",
|
||||||
"version": "0.6.0",
|
"version": "0.7.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
||||||
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"backend-dev": "python3.6 -m venv cellxgene && source cellxgene/bin/activate && yes | pip uninstall cellxgene || true && pip install -e .. && cellxgene launch ",
|
"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",
|
"build": "npm run clean && webpack --config configuration/webpack/webpack.config.prod.js",
|
||||||
"dev": "npm run clean && webpack --config configuration/webpack/webpack.config.dev.js",
|
|
||||||
"clean": "rimraf build",
|
"clean": "rimraf build",
|
||||||
"start": "node server/development.js",
|
"dev": "npm run clean && webpack --config configuration/webpack/webpack.config.dev.js",
|
||||||
|
"e2e": "jest e2e",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "jest"
|
"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",
|
||||||
|
"unit-test": "jest --testPathIgnorePatterns e2e"
|
||||||
},
|
},
|
||||||
"engineStrict": true,
|
"engineStrict": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -97,8 +100,10 @@
|
|||||||
"jest": "^24.1.0",
|
"jest": "^24.1.0",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"mini-css-extract-plugin": "^0.4.1",
|
"mini-css-extract-plugin": "^0.4.1",
|
||||||
|
"puppeteer": "^1.12.1",
|
||||||
"rimraf": "^2.6.3",
|
"rimraf": "^2.6.3",
|
||||||
"serve-favicon": "^2.3.0",
|
"serve-favicon": "^2.3.0",
|
||||||
|
"start-server-and-test": "^1.7.11",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^0.23.1",
|
||||||
"sw-precache-webpack-plugin": "^0.11.5",
|
"sw-precache-webpack-plugin": "^0.11.5",
|
||||||
"url-loader": "^1.1.0",
|
"url-loader": "^1.1.0",
|
||||||
|
|||||||
+23
-16
@@ -1,12 +1,11 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import * as globals from "../globals";
|
import * as globals from "../globals";
|
||||||
import { Universe, kvCache } from "../util/stateManager";
|
import { Universe } from "../util/stateManager";
|
||||||
import {
|
import {
|
||||||
catchErrorsWrap,
|
catchErrorsWrap,
|
||||||
doJsonRequest,
|
doJsonRequest,
|
||||||
doBinaryRequest,
|
doBinaryRequest,
|
||||||
rangeEncodeIndices,
|
|
||||||
dispatchNetworkErrorMessageToUser
|
dispatchNetworkErrorMessageToUser
|
||||||
} from "../util/actionHelpers";
|
} from "../util/actionHelpers";
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ const doInitialDataLoad = () =>
|
|||||||
/* set config defaults */
|
/* set config defaults */
|
||||||
const config = { ...globals.configDefaults, ...results[0].config };
|
const config = { ...globals.configDefaults, ...results[0].config };
|
||||||
const [, schema, obsAnno, varAnno, obsLayout] = [...results];
|
const [, schema, obsAnno, varAnno, obsLayout] = [...results];
|
||||||
const universe = Universe.createUniverseFromRestV02Response(
|
const universe = Universe.createUniverseFromResponse(
|
||||||
config,
|
config,
|
||||||
schema,
|
schema,
|
||||||
obsAnno,
|
obsAnno,
|
||||||
@@ -130,9 +129,9 @@ async function _doRequestExpressionData(dispatch, getState, genes) {
|
|||||||
let expressionData = _.transform(
|
let expressionData = _.transform(
|
||||||
genes,
|
genes,
|
||||||
(expData, g) => {
|
(expData, g) => {
|
||||||
const data = kvCache.get(universe.varDataCache, g);
|
const data = universe.varData.col(g);
|
||||||
if (data) {
|
if (data) {
|
||||||
expData[g] = data;
|
expData[g] = data.asArray();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -170,7 +169,7 @@ function requestSingleGeneExpressionCountsForColoringPOST(gene) {
|
|||||||
type: "color by expression",
|
type: "color by expression",
|
||||||
gene,
|
gene,
|
||||||
data: {
|
data: {
|
||||||
[gene]: kvCache.get(world.varDataCache, gene)
|
[gene]: world.varData.col(gene).asArray()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -193,7 +192,7 @@ const requestUserDefinedGene = gene => async (dispatch, getState) => {
|
|||||||
type: "request user defined gene success",
|
type: "request user defined gene success",
|
||||||
data: {
|
data: {
|
||||||
genes: [gene],
|
genes: [gene],
|
||||||
expression: kvCache.get(world.varDataCache, gene)
|
expression: world.varData.col(gene).asArray()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -242,12 +241,18 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
*/
|
*/
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { universe } = state.controls;
|
const { universe } = state.controls;
|
||||||
const set1ByIndex = rangeEncodeIndices(
|
|
||||||
_.map(set1, s => universe.obsNameToIndexMap[s])
|
// Legal values are null, Array or TypedArray. Null is initial state.
|
||||||
);
|
if (!set1) set1 = [];
|
||||||
const set2ByIndex = rangeEncodeIndices(
|
if (!set2) set2 = [];
|
||||||
_.map(set2, s => universe.obsNameToIndexMap[s])
|
|
||||||
);
|
// 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(
|
const res = await fetch(
|
||||||
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
|
||||||
{
|
{
|
||||||
@@ -259,8 +264,8 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
mode: "topN",
|
mode: "topN",
|
||||||
count: num_genes,
|
count: num_genes,
|
||||||
set1: { filter: { obs: { index: set1ByIndex } } },
|
set1: { filter: { obs: { index: set1 } } },
|
||||||
set2: { filter: { obs: { index: set2ByIndex } } }
|
set2: { filter: { obs: { index: set2 } } }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -271,7 +276,9 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// result is [ [varIdx, ...], ... ]
|
// result is [ [varIdx, ...], ... ]
|
||||||
const topNGenes = _.map(data, r => universe.varAnnotations[r[0]].name);
|
const topNGenes = _.map(data, r =>
|
||||||
|
universe.varAnnotations.at(r[0], "name")
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Kick off secondary action to fetch all of the expression data for the
|
Kick off secondary action to fetch all of the expression data for the
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import memoize from "memoize-one";
|
import memoize from "memoize-one";
|
||||||
import { kvCache } from "../../util/stateManager";
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
import finiteExtent from "../../util/finiteExtent";
|
import finiteExtent from "../../util/finiteExtent";
|
||||||
@@ -21,7 +20,6 @@ import finiteExtent from "../../util/finiteExtent";
|
|||||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||||
crossfilter: state.controls.crossfilter,
|
crossfilter: state.controls.crossfilter,
|
||||||
differential: state.differential,
|
differential: state.differential,
|
||||||
initializeRanges: _.get(state.controls.world, "summary.obs"),
|
|
||||||
colorAccessor: state.controls.colorAccessor,
|
colorAccessor: state.controls.colorAccessor,
|
||||||
colorScale: state.controls.colorScale,
|
colorScale: state.controls.colorScale,
|
||||||
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null)
|
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null)
|
||||||
@@ -35,9 +33,11 @@ class HistogramBrush extends React.Component {
|
|||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.range([this.height - this.marginBottom, 0]);
|
.range([this.height - this.marginBottom, 0]);
|
||||||
|
|
||||||
if (obsAnnotations[0][field] !== undefined) {
|
if (obsAnnotations.hasCol(field)) {
|
||||||
// recalculate expensive stuff
|
// recalculate expensive stuff
|
||||||
const allValuesForContinuousFieldAsArray = _.map(obsAnnotations, field);
|
const allValuesForContinuousFieldAsArray = obsAnnotations
|
||||||
|
.col(field)
|
||||||
|
.asArray();
|
||||||
|
|
||||||
histogramCache.x = d3
|
histogramCache.x = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
@@ -50,9 +50,8 @@ class HistogramBrush extends React.Component {
|
|||||||
.thresholds(40)(allValuesForContinuousFieldAsArray);
|
.thresholds(40)(allValuesForContinuousFieldAsArray);
|
||||||
|
|
||||||
histogramCache.numValues = allValuesForContinuousFieldAsArray.length;
|
histogramCache.numValues = allValuesForContinuousFieldAsArray.length;
|
||||||
} else if (kvCache.get(world.varDataCache, field)) {
|
} else if (world.varData.hasCol(field)) {
|
||||||
/* it's not in observations, so it's a gene, but let's check to make sure */
|
const varValues = world.varData.col(field).asArray();
|
||||||
const varValues = kvCache.get(world.varDataCache, field);
|
|
||||||
|
|
||||||
histogramCache.x = d3
|
histogramCache.x = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
@@ -141,21 +140,15 @@ class HistogramBrush extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleColorAction() {
|
handleColorAction() {
|
||||||
const {
|
const { obsAnnotations, dispatch, field, world, ranges } = this.props;
|
||||||
obsAnnotations,
|
|
||||||
dispatch,
|
|
||||||
field,
|
|
||||||
world,
|
|
||||||
initializeRanges
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (obsAnnotations[0][field]) {
|
if (obsAnnotations.hasCol(field)) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "color by continuous metadata",
|
type: "color by continuous metadata",
|
||||||
colorAccessor: field,
|
colorAccessor: field,
|
||||||
rangeMaxForColorAccessor: initializeRanges[field].range.max
|
rangeForColorAccessor: ranges
|
||||||
});
|
});
|
||||||
} else if (kvCache.get(world.varDataCache, field)) {
|
} else if (world.varData.hasCol(field)) {
|
||||||
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
|
dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(field));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,6 +228,7 @@ class HistogramBrush extends React.Component {
|
|||||||
d3.select(svgRef)
|
d3.select(svgRef)
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("class", "brush")
|
.attr("class", "brush")
|
||||||
|
.attr("data-testid", `${svgRef.id}-brush`)
|
||||||
.call(
|
.call(
|
||||||
d3
|
d3
|
||||||
.brushX()
|
.brushX()
|
||||||
@@ -279,6 +273,8 @@ class HistogramBrush extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`histogram_${field}`}
|
id={`histogram_${field}`}
|
||||||
|
data-testid={`histogram-${field}`}
|
||||||
|
data-testclass={isDiffExp ? `histogram-diffexp` : ""}
|
||||||
style={{
|
style={{
|
||||||
padding: globals.leftSidebarSectionPadding,
|
padding: globals.leftSidebarSectionPadding,
|
||||||
backgroundColor: zebra ? globals.lightestGrey : "white"
|
backgroundColor: zebra ? globals.lightestGrey : "white"
|
||||||
|
|||||||
@@ -65,7 +65,11 @@ class CategoryValue extends React.Component {
|
|||||||
})[0].categories;
|
})[0].categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (colorAccessor && !isColorBy) {
|
if (
|
||||||
|
colorAccessor &&
|
||||||
|
!isColorBy &&
|
||||||
|
categoricalSelectionState[colorAccessor]
|
||||||
|
) {
|
||||||
occupancy = countCategoryValues2D(
|
occupancy = countCategoryValues2D(
|
||||||
metadataField,
|
metadataField,
|
||||||
colorAccessor,
|
colorAccessor,
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import * as globals from "../../globals";
|
|||||||
import HistogramBrush from "../brushableHistogram";
|
import HistogramBrush from "../brushableHistogram";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
ranges: _.get(state.controls.world, "summary.obs", null),
|
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null),
|
||||||
metadata: _.get(state.controls.world, "obsAnnotations", null),
|
|
||||||
colorAccessor: state.controls.colorAccessor,
|
colorAccessor: state.controls.colorAccessor,
|
||||||
colorScale: state.controls.colorScale,
|
colorScale: state.controls.colorScale,
|
||||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null),
|
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null),
|
||||||
@@ -29,17 +28,18 @@ class Continuous extends React.Component {
|
|||||||
|
|
||||||
handleColorAction(key) {
|
handleColorAction(key) {
|
||||||
return () => {
|
return () => {
|
||||||
const { dispatch, ranges } = this.props;
|
const { dispatch, obsAnnotations } = this.props;
|
||||||
|
const summary = obsAnnotations.col(key).summarize();
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "color by continuous metadata",
|
type: "color by continuous metadata",
|
||||||
colorAccessor: key,
|
colorAccessor: key,
|
||||||
rangeMaxForColorAccessor: ranges[key].range.max
|
rangeForColorAccessor: summary
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ranges, obsAnnotations, schema } = this.props;
|
const { obsAnnotations, schema } = this.props;
|
||||||
if (schema && !this.continuousChecked) {
|
if (schema && !this.continuousChecked) {
|
||||||
this.hasContinuous = _.some(
|
this.hasContinuous = _.some(
|
||||||
schema.annotations.obs,
|
schema.annotations.obs,
|
||||||
@@ -63,24 +63,34 @@ class Continuous extends React.Component {
|
|||||||
Continuous metadata
|
Continuous metadata
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{_.map(ranges, (value, key) => {
|
{obsAnnotations
|
||||||
const isColorField = key.includes("color") || key.includes("Color");
|
? _.map(obsAnnotations.colIndex.keys(), key => {
|
||||||
zebra += 1;
|
const summary = obsAnnotations.col(key).summarize();
|
||||||
if (value.range && key !== "name" && !isColorField) {
|
const isColorField =
|
||||||
return (
|
key.includes("color") || key.includes("Color");
|
||||||
<HistogramBrush
|
const nonFiniteExtent =
|
||||||
key={key}
|
summary.min === undefined || summary.max === undefined;
|
||||||
field={key}
|
zebra += 1;
|
||||||
isObs
|
if (
|
||||||
zebra={zebra % 2 === 0}
|
!summary.categorical &&
|
||||||
fieldValues={obsAnnotations}
|
key !== "name" &&
|
||||||
ranges={value.range}
|
!isColorField &&
|
||||||
handleColorAction={this.handleColorAction(key).bind(this)}
|
!nonFiniteExtent
|
||||||
/>
|
) {
|
||||||
);
|
return (
|
||||||
}
|
<HistogramBrush
|
||||||
return null;
|
key={key}
|
||||||
})}
|
field={key}
|
||||||
|
isObs
|
||||||
|
zebra={zebra % 2 === 0}
|
||||||
|
ranges={summary}
|
||||||
|
handleColorAction={this.handleColorAction(key).bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
|
||||||
import { AnchorButton, Tooltip } from "@blueprintjs/core";
|
import { AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { World } from "../../util/stateManager";
|
||||||
|
|
||||||
@connect()
|
@connect()
|
||||||
class CellSetButton extends React.Component {
|
class CellSetButton extends React.Component {
|
||||||
@@ -14,7 +14,11 @@ class CellSetButton extends React.Component {
|
|||||||
eitherCellSetOneOrTwo
|
eitherCellSetOneOrTwo
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const set = _.map(crossfilter.allFiltered(), "name");
|
// 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) {
|
if (!differential.diffExp) {
|
||||||
/* diffexp needs to be cleared before we store a new set */
|
/* diffexp needs to be cleared before we store a new set */
|
||||||
@@ -38,6 +42,7 @@ class CellSetButton extends React.Component {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={differential.diffExp}
|
disabled={differential.diffExp}
|
||||||
onClick={this.set.bind(this)}
|
onClick={this.set.bind(this)}
|
||||||
|
data-testid={`cellset-button-${eitherCellSetOneOrTwo}`}
|
||||||
>
|
>
|
||||||
{eitherCellSetOneOrTwo}
|
{eitherCellSetOneOrTwo}
|
||||||
{": "}
|
{": "}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class Expression extends React.Component {
|
|||||||
style={{ marginTop: 10 }}
|
style={{ marginTop: 10 }}
|
||||||
disabled={!haveBothCellSets}
|
disabled={!haveBothCellSets}
|
||||||
intent="primary"
|
intent="primary"
|
||||||
|
data-testid="diffexp-button"
|
||||||
loading={differential.loading}
|
loading={differential.loading}
|
||||||
fill
|
fill
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ const renderGene = (fuzzySortResult, { handleClick, modifiers, query }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
/* the fuzzysort wraps the object with other properties, like a score */
|
/* the fuzzysort wraps the object with other properties, like a score */
|
||||||
const gene = fuzzySortResult.obj;
|
const geneName = fuzzySortResult.target;
|
||||||
const text = gene.name;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -39,39 +38,30 @@ const renderGene = (fuzzySortResult, { handleClick, modifiers, query }) => {
|
|||||||
// Use of annotations in this way is incorrect and dataset specific.
|
// Use of annotations in this way is incorrect and dataset specific.
|
||||||
// See https://github.com/chanzuckerberg/cellxgene/issues/483
|
// See https://github.com/chanzuckerberg/cellxgene/issues/483
|
||||||
// label={gene.n_counts}
|
// label={gene.n_counts}
|
||||||
key={gene.name}
|
key={geneName}
|
||||||
onClick={g => {
|
onClick={g =>
|
||||||
/* this fires when user clicks a menu item */
|
/* this fires when user clicks a menu item */
|
||||||
handleClick(g);
|
handleClick(g)
|
||||||
}}
|
}
|
||||||
text={text}
|
text={geneName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterGenes = (query, genes) => {
|
const filterGenes = (query, genes) =>
|
||||||
/* fires on load, once, and then for each character typed into the input */
|
/* fires on load, once, and then for each character typed into the input */
|
||||||
return fuzzysort.go(query, genes, {
|
fuzzysort.go(query, genes, {
|
||||||
key: "name",
|
|
||||||
limit: 5,
|
limit: 5,
|
||||||
threshold: -10000 // don't return bad results
|
threshold: -10000 // don't return bad results
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
@connect(state => {
|
@connect(state => {
|
||||||
const metadata = _.get(state.controls.world, "obsAnnotations", null);
|
|
||||||
const ranges = _.get(state.controls.world, "summary.obs", null);
|
|
||||||
const initializeRanges = _.get(state.controls.world, "summary.obs");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ranges,
|
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null),
|
||||||
metadata,
|
|
||||||
initializeRanges,
|
|
||||||
userDefinedGenes: state.controls.userDefinedGenes,
|
userDefinedGenes: state.controls.userDefinedGenes,
|
||||||
userDefinedGenesLoading: state.controls.userDefinedGenesLoading,
|
userDefinedGenesLoading: state.controls.userDefinedGenesLoading,
|
||||||
world: state.controls.world,
|
world: state.controls.world,
|
||||||
colorAccessor: state.controls.colorAccessor,
|
colorAccessor: state.controls.colorAccessor,
|
||||||
allGeneNames: state.controls.allGeneNames,
|
|
||||||
differential: state.differential
|
differential: state.differential
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -84,6 +74,37 @@ class GeneExpression extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 geneNames = varAnnotations.col("name").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) {
|
handleClick(g) {
|
||||||
const { world, dispatch, userDefinedGenes } = this.props;
|
const { world, dispatch, userDefinedGenes } = this.props;
|
||||||
const gene = g.target;
|
const gene = g.target;
|
||||||
@@ -93,7 +114,7 @@ class GeneExpression extends React.Component {
|
|||||||
postUserErrorToast(
|
postUserErrorToast(
|
||||||
"That's too many genes, you can have at most 15 user defined genes"
|
"That's too many genes, you can have at most 15 user defined genes"
|
||||||
);
|
);
|
||||||
} else if (!_.find(world.varAnnotations, { name: gene })) {
|
} else if (world.varAnnotations.col("name").indexOf(gene) === undefined) {
|
||||||
postUserErrorToast("That doesn't appear to be a valid gene name.");
|
postUserErrorToast("That doesn't appear to be a valid gene name.");
|
||||||
} else {
|
} else {
|
||||||
dispatch(actions.requestUserDefinedGene(gene));
|
dispatch(actions.requestUserDefinedGene(gene));
|
||||||
@@ -116,9 +137,13 @@ class GeneExpression extends React.Component {
|
|||||||
const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), "");
|
const genes = _.pull(_.uniq(bulkAdd.split(/[ ,]+/)), "");
|
||||||
|
|
||||||
genes.forEach(gene => {
|
genes.forEach(gene => {
|
||||||
if (userDefinedGenes.indexOf(gene) !== -1) {
|
if (gene.length === 0) {
|
||||||
|
keepAroundErrorToast("Must enter a gene name.");
|
||||||
|
} else if (userDefinedGenes.indexOf(gene) !== -1) {
|
||||||
keepAroundErrorToast("That gene already exists");
|
keepAroundErrorToast("That gene already exists");
|
||||||
} else if (!_.find(world.varAnnotations, { name: gene })) {
|
} else if (
|
||||||
|
world.varAnnotations.col("name").indexOf(gene) === undefined
|
||||||
|
) {
|
||||||
keepAroundErrorToast(
|
keepAroundErrorToast(
|
||||||
`${gene} doesn't appear to be a valid gene name.`
|
`${gene} doesn't appear to be a valid gene name.`
|
||||||
);
|
);
|
||||||
@@ -207,6 +232,7 @@ class GeneExpression extends React.Component {
|
|||||||
/* this happens on 'enter' */
|
/* this happens on 'enter' */
|
||||||
this.handleClick(g);
|
this.handleClick(g);
|
||||||
}}
|
}}
|
||||||
|
inputProps={{ "data-testid": "gene-search" }}
|
||||||
inputValueRenderer={g => {
|
inputValueRenderer={g => {
|
||||||
return "";
|
return "";
|
||||||
}}
|
}}
|
||||||
@@ -214,13 +240,14 @@ class GeneExpression extends React.Component {
|
|||||||
itemRenderer={renderGene.bind(this)}
|
itemRenderer={renderGene.bind(this)}
|
||||||
items={
|
items={
|
||||||
world && world.varAnnotations
|
world && world.varAnnotations
|
||||||
? world.varAnnotations
|
? world.varAnnotations.col("name").asArray()
|
||||||
: [{ name: "No genes" }]
|
: ["No genes"]
|
||||||
}
|
}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="bp3-button bp3-intent-primary"
|
className="bp3-button bp3-intent-primary"
|
||||||
|
data-testid={"add-gene"}
|
||||||
loading={userDefinedGenesLoading}
|
loading={userDefinedGenesLoading}
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
@@ -245,7 +272,7 @@ class GeneExpression extends React.Component {
|
|||||||
this.setState({ bulkAdd: e.target.value });
|
this.setState({ bulkAdd: e.target.value });
|
||||||
}}
|
}}
|
||||||
id="text-input-bulk-add"
|
id="text-input-bulk-add"
|
||||||
placeholder="Apod, Cd74, ..."
|
placeholder={this.placeholderGeneNames()}
|
||||||
value={bulkAdd}
|
value={bulkAdd}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -262,16 +289,17 @@ class GeneExpression extends React.Component {
|
|||||||
) : null}
|
) : null}
|
||||||
{world && userDefinedGenes.length > 0
|
{world && userDefinedGenes.length > 0
|
||||||
? _.map(userDefinedGenes, (geneName, index) => {
|
? _.map(userDefinedGenes, (geneName, index) => {
|
||||||
const values = world.varDataCache[geneName];
|
const values = world.varData.col(geneName);
|
||||||
if (!values) {
|
if (!values) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const summary = values.summarize();
|
||||||
return (
|
return (
|
||||||
<HistogramBrush
|
<HistogramBrush
|
||||||
key={geneName}
|
key={geneName}
|
||||||
field={geneName}
|
field={geneName}
|
||||||
zebra={index % 2 === 0}
|
zebra={index % 2 === 0}
|
||||||
ranges={finiteExtent(values)}
|
ranges={summary}
|
||||||
isUserDefined
|
isUserDefined
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -290,18 +318,18 @@ class GeneExpression extends React.Component {
|
|||||||
<ExpressionButtons />
|
<ExpressionButtons />
|
||||||
{differential.diffExp
|
{differential.diffExp
|
||||||
? _.map(differential.diffExp, (value, index) => {
|
? _.map(differential.diffExp, (value, index) => {
|
||||||
const annotations = world.varAnnotations[value[0]];
|
const name = world.varAnnotations.at(value[0], "name");
|
||||||
const { name } = annotations;
|
const values = world.varData.col(name);
|
||||||
const values = world.varDataCache[name];
|
|
||||||
if (!values) {
|
if (!values) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const summary = values.summarize();
|
||||||
return (
|
return (
|
||||||
<HistogramBrush
|
<HistogramBrush
|
||||||
key={name}
|
key={name}
|
||||||
field={name}
|
field={name}
|
||||||
zebra={index % 2 === 0}
|
zebra={index % 2 === 0}
|
||||||
ranges={finiteExtent(values)}
|
ranges={summary}
|
||||||
isDiffExp
|
isDiffExp
|
||||||
logFoldChange={value[1]}
|
logFoldChange={value[1]}
|
||||||
pval={value[2]}
|
pval={value[2]}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ class Graph extends React.Component {
|
|||||||
this.graphPaddingRight = globals.leftSidebarWidth;
|
this.graphPaddingRight = globals.leftSidebarWidth;
|
||||||
this.renderCache = {
|
this.renderCache = {
|
||||||
positions: null,
|
positions: null,
|
||||||
colors: null
|
colors: null,
|
||||||
|
sizes: null
|
||||||
};
|
};
|
||||||
this.state = {
|
this.state = {
|
||||||
svg: null,
|
svg: null,
|
||||||
@@ -83,12 +84,13 @@ class Graph extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
const { renderCache } = this;
|
||||||
const {
|
const {
|
||||||
world,
|
world,
|
||||||
crossfilter,
|
crossfilter,
|
||||||
selectionUpdate,
|
|
||||||
colorRGB,
|
colorRGB,
|
||||||
responsive
|
responsive,
|
||||||
|
selectionUpdate
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
reglRender,
|
reglRender,
|
||||||
@@ -109,35 +111,27 @@ class Graph extends React.Component {
|
|||||||
|
|
||||||
if (regl && world) {
|
if (regl && world) {
|
||||||
/* update the regl state */
|
/* update the regl state */
|
||||||
const { obsLayout } = world;
|
const { obsLayout, nObs } = world;
|
||||||
const cellCount = crossfilter.size();
|
const X = obsLayout.col("X").asArray();
|
||||||
|
const Y = obsLayout.col("Y").asArray();
|
||||||
|
|
||||||
// X/Y positions for each point - a cached value that only
|
// X/Y positions for each point - a cached value that only
|
||||||
// changes if we have loaded entirely new cell data
|
// changes if we have loaded entirely new cell data
|
||||||
//
|
//
|
||||||
if (
|
if (!renderCache.positions || world !== prevProps.world) {
|
||||||
!this.renderCache.positions ||
|
renderCache.positions = new Float32Array(2 * nObs);
|
||||||
selectionUpdate !== prevProps.selectionUpdate
|
|
||||||
) {
|
|
||||||
if (!this.renderCache.positions) {
|
|
||||||
this.renderCache.positions = new Float32Array(2 * cellCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
||||||
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
||||||
|
|
||||||
const offset = [d3.mean(obsLayout.X) - 0.5, d3.mean(obsLayout.Y) - 0.5];
|
const offset = [d3.mean(X) - 0.5, d3.mean(Y) - 0.5];
|
||||||
|
|
||||||
for (
|
for (let i = 0, { positions } = renderCache; i < nObs; i += 1) {
|
||||||
let i = 0, { positions } = this.renderCache;
|
positions[2 * i] = glScaleX(X[i] - offset[0]);
|
||||||
i < cellCount;
|
positions[2 * i + 1] = glScaleY(Y[i] - offset[1]);
|
||||||
i += 1
|
|
||||||
) {
|
|
||||||
positions[2 * i] = glScaleX(obsLayout.X[i] - offset[0]);
|
|
||||||
positions[2 * i + 1] = glScaleY(obsLayout.Y[i] - offset[1]);
|
|
||||||
}
|
}
|
||||||
pointBuffer({
|
pointBuffer({
|
||||||
data: this.renderCache.positions,
|
data: renderCache.positions,
|
||||||
dimension: 2
|
dimension: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,30 +146,28 @@ class Graph extends React.Component {
|
|||||||
// could have changed for some other reason, but for now color is
|
// could have changed for some other reason, but for now color is
|
||||||
// the only metadata that changes client-side. If this is problematic,
|
// the only metadata that changes client-side. If this is problematic,
|
||||||
// we could add some sort of color-specific indicator to the app state.
|
// we could add some sort of color-specific indicator to the app state.
|
||||||
if (!this.renderCache.colors || colorRGB !== prevProps.colorRGB) {
|
if (!renderCache.colors || colorRGB !== prevProps.colorRGB) {
|
||||||
const rgb = colorRGB;
|
const rgb = colorRGB;
|
||||||
if (!this.renderCache.colors) {
|
if (!renderCache.colors) {
|
||||||
this.renderCache.colors = new Float32Array(3 * rgb.length);
|
renderCache.colors = new Float32Array(3 * rgb.length);
|
||||||
}
|
}
|
||||||
for (let i = 0, { colors } = this.renderCache; i < rgb.length; i += 1) {
|
for (let i = 0, { colors } = renderCache; i < rgb.length; i += 1) {
|
||||||
colors.set(rgb[i], 3 * i);
|
colors.set(rgb[i], 3 * i);
|
||||||
}
|
}
|
||||||
colorBuffer({ data: this.renderCache.colors, dimension: 3 });
|
colorBuffer({ data: renderCache.colors, dimension: 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sizes for each point - this is presumed to change each time the
|
// Sizes for each point - updates are triggered only when selected
|
||||||
// component receives new props. Almost always a true assumption, as
|
// obs change
|
||||||
// most property upates are due to changes driving a crossfilter
|
if (!renderCache.sizes || selectionUpdate !== prevProps.selectionUpdate) {
|
||||||
// selection set change.
|
if (!renderCache.sizes) {
|
||||||
//
|
renderCache.sizes = new Float32Array(nObs);
|
||||||
if (!this.renderCache.sizes) {
|
}
|
||||||
this.renderCache.sizes = new Float32Array(cellCount);
|
crossfilter.fillByIsFiltered(renderCache.sizes, 4, 0.2);
|
||||||
|
sizeBuffer({ data: renderCache.sizes, dimension: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
crossfilter.fillByIsFiltered(this.renderCache.sizes, 4, 0.2);
|
this.count = nObs;
|
||||||
sizeBuffer({ data: this.renderCache.sizes, dimension: 1 });
|
|
||||||
|
|
||||||
this.count = cellCount;
|
|
||||||
|
|
||||||
regl._refresh();
|
regl._refresh();
|
||||||
this.reglDraw(
|
this.reglDraw(
|
||||||
@@ -485,6 +477,7 @@ class Graph extends React.Component {
|
|||||||
<canvas
|
<canvas
|
||||||
width={responsive.width - this.graphPaddingRight}
|
width={responsive.width - this.graphPaddingRight}
|
||||||
height={responsive.height - this.graphPaddingTop}
|
height={responsive.height - this.graphPaddingTop}
|
||||||
|
data-testid="layout"
|
||||||
ref={canvas => {
|
ref={canvas => {
|
||||||
this.reglCanvas = canvas;
|
this.reglCanvas = canvas;
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default (
|
|||||||
const svg = d3
|
const svg = d3
|
||||||
.select("#graphAttachPoint")
|
.select("#graphAttachPoint")
|
||||||
.append("svg")
|
.append("svg")
|
||||||
|
.attr("data-testid", "layout-overlay")
|
||||||
.attr("width", responsive.width - graphPaddingRight)
|
.attr("width", responsive.width - graphPaddingRight)
|
||||||
.attr("height", responsive.height)
|
.attr("height", responsive.height)
|
||||||
.attr("class", `${styles.graphSVG}`);
|
.attr("class", `${styles.graphSVG}`);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class LeftSideBar extends React.Component {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
|
data-testid="header"
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
top: globals.cellxgeneTitleTopPadding,
|
top: globals.cellxgeneTitleTopPadding,
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import _drawPoints from "./drawPointsRegl";
|
|||||||
import scaleLinear from "../../util/scaleLinear";
|
import scaleLinear from "../../util/scaleLinear";
|
||||||
|
|
||||||
import { margin, width, height } from "./util";
|
import { margin, width, height } from "./util";
|
||||||
import { kvCache } from "../../util/stateManager";
|
|
||||||
import finiteExtent from "../../util/finiteExtent";
|
import finiteExtent from "../../util/finiteExtent";
|
||||||
|
|
||||||
@connect(state => {
|
@connect(state => {
|
||||||
@@ -30,12 +29,16 @@ import finiteExtent from "../../util/finiteExtent";
|
|||||||
scatterplotYYaccessor
|
scatterplotYYaccessor
|
||||||
} = state.controls;
|
} = state.controls;
|
||||||
const expressionX =
|
const expressionX =
|
||||||
world && scatterplotXXaccessor
|
world &&
|
||||||
? kvCache.get(world.varDataCache, scatterplotXXaccessor)
|
scatterplotXXaccessor &&
|
||||||
|
world.varData.hasCol(scatterplotXXaccessor)
|
||||||
|
? world.varData.col(scatterplotXXaccessor).asArray()
|
||||||
: null;
|
: null;
|
||||||
const expressionY =
|
const expressionY =
|
||||||
world && scatterplotYYaccessor
|
world &&
|
||||||
? kvCache.get(world.varDataCache, scatterplotYYaccessor)
|
scatterplotYYaccessor &&
|
||||||
|
world.varData.hasCol(scatterplotYYaccessor)
|
||||||
|
? world.varData.col(scatterplotYYaccessor).asArray()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -65,12 +68,17 @@ class Scatterplot extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.count = 0;
|
this.count = 0;
|
||||||
this.axes = false;
|
this.axes = false;
|
||||||
this.state = {
|
this.renderCache = {
|
||||||
svg: null,
|
positions: null,
|
||||||
minimized: null,
|
colors: null,
|
||||||
|
sizes: null,
|
||||||
xScale: null,
|
xScale: null,
|
||||||
yScale: null
|
yScale: null
|
||||||
};
|
};
|
||||||
|
this.state = {
|
||||||
|
svg: null,
|
||||||
|
minimized: null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -81,6 +89,7 @@ class Scatterplot extends React.Component {
|
|||||||
if (svg && expressionX && expressionY) {
|
if (svg && expressionX && expressionY) {
|
||||||
scales = Scatterplot.setupScales(expressionX, expressionY);
|
scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||||
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
|
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
|
||||||
|
this.renderCache = { ...this.renderCache, ...scales };
|
||||||
}
|
}
|
||||||
|
|
||||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
||||||
@@ -113,8 +122,6 @@ class Scatterplot extends React.Component {
|
|||||||
pointBuffer,
|
pointBuffer,
|
||||||
colorBuffer,
|
colorBuffer,
|
||||||
svg,
|
svg,
|
||||||
xScale: scales ? scales.xScale : null,
|
|
||||||
yScale: scales ? scales.yScale : null,
|
|
||||||
reglRender,
|
reglRender,
|
||||||
camera,
|
camera,
|
||||||
drawPoints
|
drawPoints
|
||||||
@@ -129,12 +136,11 @@ class Scatterplot extends React.Component {
|
|||||||
scatterplotYYaccessor,
|
scatterplotYYaccessor,
|
||||||
expressionX,
|
expressionX,
|
||||||
expressionY,
|
expressionY,
|
||||||
colorRGB
|
colorRGB,
|
||||||
|
selectionUpdate
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
const {
|
||||||
reglRender,
|
reglRender,
|
||||||
xScale,
|
|
||||||
yScale,
|
|
||||||
regl,
|
regl,
|
||||||
pointBuffer,
|
pointBuffer,
|
||||||
colorBuffer,
|
colorBuffer,
|
||||||
@@ -145,17 +151,12 @@ class Scatterplot extends React.Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
world &&
|
scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||||
svg &&
|
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor // was CLU now FTH1 etc
|
||||||
xScale &&
|
|
||||||
yScale &&
|
|
||||||
scatterplotXXaccessor &&
|
|
||||||
scatterplotYYaccessor &&
|
|
||||||
(scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
|
||||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
|
|
||||||
!this.axes) // clicked off the tab and back again, rerender
|
|
||||||
) {
|
) {
|
||||||
this.drawAxesSVG(xScale, yScale, svg);
|
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||||
|
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
|
||||||
|
this.renderCache = { ...this.renderCache, ...scales };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reglRender && this.reglRenderState === "rendering") {
|
if (reglRender && this.reglRenderState === "rendering") {
|
||||||
@@ -172,35 +173,51 @@ class Scatterplot extends React.Component {
|
|||||||
expressionX &&
|
expressionX &&
|
||||||
expressionY &&
|
expressionY &&
|
||||||
scatterplotXXaccessor &&
|
scatterplotXXaccessor &&
|
||||||
scatterplotYYaccessor &&
|
scatterplotYYaccessor
|
||||||
xScale &&
|
|
||||||
yScale
|
|
||||||
) {
|
) {
|
||||||
|
const { renderCache } = this;
|
||||||
|
const { xScale, yScale } = this.renderCache;
|
||||||
const cellCount = expressionX.length;
|
const cellCount = expressionX.length;
|
||||||
const positionsBuf = new Float32Array(2 * cellCount);
|
|
||||||
const colorsBuf = new Float32Array(3 * cellCount);
|
|
||||||
const sizesBuf = new Float32Array(cellCount);
|
|
||||||
|
|
||||||
const glScaleX = scaleLinear([0, width], [-0.95, 0.95]);
|
// Points change when expressionX or expressionY change.
|
||||||
const glScaleY = scaleLinear([0, height], [-1, 1]);
|
if (
|
||||||
|
!renderCache.positions ||
|
||||||
/*
|
expressionX !== prevProps.expressionX ||
|
||||||
Construct Vectors
|
expressionY !== prevProps.expressionY
|
||||||
*/
|
) {
|
||||||
for (let i = 0; i < cellCount; i += 1) {
|
if (!renderCache.positions) {
|
||||||
positionsBuf[2 * i] = glScaleX(xScale(expressionX[i]));
|
renderCache.positions = new Float32Array(2 * cellCount);
|
||||||
positionsBuf[2 * i + 1] = glScaleY(yScale(expressionY[i]));
|
}
|
||||||
|
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < cellCount; i += 1) {
|
// Colors for each point - change only when props.colorsRGB change.
|
||||||
colorsBuf.set(colorRGB[i], 3 * i);
|
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
crossfilter.fillByIsFiltered(sizesBuf, 4, 0.2);
|
// Sizes for each point - updates are triggered only when selected
|
||||||
|
// obs change
|
||||||
|
if (!renderCache.sizes || selectionUpdate !== prevProps.selctionUpdate) {
|
||||||
|
if (!renderCache.sizes) {
|
||||||
|
renderCache.sizes = new Float32Array(cellCount);
|
||||||
|
}
|
||||||
|
crossfilter.fillByIsFiltered(renderCache.sizes, 4, 0.2);
|
||||||
|
sizeBuffer({ data: renderCache.sizes, dimension: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
pointBuffer({ data: positionsBuf, dimension: 2 });
|
|
||||||
colorBuffer({ data: colorsBuf, dimension: 3 });
|
|
||||||
sizeBuffer({ data: sizesBuf, dimension: 1 });
|
|
||||||
this.count = cellCount;
|
this.count = cellCount;
|
||||||
|
|
||||||
regl._refresh();
|
regl._refresh();
|
||||||
@@ -213,16 +230,6 @@ class Scatterplot extends React.Component {
|
|||||||
camera
|
camera
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
expressionX &&
|
|
||||||
expressionY &&
|
|
||||||
(scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
|
||||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
|
|
||||||
) {
|
|
||||||
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
|
||||||
this.setState(scales);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static setupScales(expressionX, expressionY) {
|
static setupScales(expressionX, expressionY) {
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ const updateCellColorsMiddleware = store => next => action => {
|
|||||||
action.type === "color by continuous metadata" ||
|
action.type === "color by continuous metadata" ||
|
||||||
action.type === "color by categorical metadata";
|
action.type === "color by categorical metadata";
|
||||||
|
|
||||||
if (!filterJustChanged || !s.controls.world.obsAnnotations) {
|
const obsAnnotations = _.get(s.controls, "world.obsAnnotations", null);
|
||||||
|
if (!filterJustChanged || !obsAnnotations) {
|
||||||
return next(
|
return next(
|
||||||
action
|
action
|
||||||
); /* if the cells haven't loaded or the action wasn't a color change, bail */
|
); /* if the cells haven't loaded or the action wasn't a color change, bail */
|
||||||
}
|
}
|
||||||
|
|
||||||
const { obsAnnotations } = s.controls.world;
|
|
||||||
let colorScale;
|
let colorScale;
|
||||||
const colorsByRGB = new Array(obsAnnotations.length);
|
const colorsByRGB = new Array(obsAnnotations.length);
|
||||||
|
|
||||||
@@ -73,16 +73,16 @@ const updateCellColorsMiddleware = store => next => action => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const key = action.colorAccessor;
|
const key = action.colorAccessor;
|
||||||
|
const col = obsAnnotations.col(key).asArray();
|
||||||
for (let i = 0, len = obsAnnotations.length; i < len; i += 1) {
|
for (let i = 0, len = obsAnnotations.length; i < len; i += 1) {
|
||||||
const obs = obsAnnotations[i];
|
const cat = col[i];
|
||||||
const cat = obs[key];
|
|
||||||
colorsByRGB[i] = colors[cat];
|
colorsByRGB[i] = colors[cat];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === "color by continuous metadata") {
|
if (action.type === "color by continuous metadata") {
|
||||||
const colorBins = 100;
|
const colorBins = 100;
|
||||||
const [min, max] = [0, action.rangeMaxForColorAccessor];
|
const { min, max } = action.rangeForColorAccessor;
|
||||||
colorScale = d3
|
colorScale = d3
|
||||||
.scaleQuantile()
|
.scaleQuantile()
|
||||||
.domain([min, max])
|
.domain([min, max])
|
||||||
@@ -96,8 +96,9 @@ const updateCellColorsMiddleware = store => next => action => {
|
|||||||
|
|
||||||
const key = action.colorAccessor;
|
const key = action.colorAccessor;
|
||||||
const nonFiniteColor = parseRGB(globals.nonFiniteCellColor);
|
const nonFiniteColor = parseRGB(globals.nonFiniteCellColor);
|
||||||
|
const col = obsAnnotations.col(key).asArray();
|
||||||
for (let i = 0, len = obsAnnotations.length; i < len; i += 1) {
|
for (let i = 0, len = obsAnnotations.length; i < len; i += 1) {
|
||||||
const val = obsAnnotations[i][key];
|
const val = col[i];
|
||||||
if (Number.isFinite(val)) {
|
if (Number.isFinite(val)) {
|
||||||
const c = colorScale(val);
|
const c = colorScale(val);
|
||||||
colorsByRGB[i] = colors[c];
|
colorsByRGB[i] = colors[c];
|
||||||
|
|||||||
Vendored
+118
-205
@@ -1,9 +1,8 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { polygonContains } from "d3";
|
|
||||||
|
|
||||||
import { World, kvCache, WorldUtil } from "../util/stateManager";
|
import { World, WorldUtil, ControlsHelper } from "../util/stateManager";
|
||||||
import parseRGB from "../util/parseRGB";
|
import parseRGB from "../util/parseRGB";
|
||||||
import Crossfilter from "../util/typedCrossfilter";
|
import Crossfilter from "../util/typedCrossfilter";
|
||||||
import * as globals from "../globals";
|
import * as globals from "../globals";
|
||||||
@@ -14,91 +13,6 @@ import {
|
|||||||
diffexpDimensionName,
|
diffexpDimensionName,
|
||||||
makeContinuousDimensionName
|
makeContinuousDimensionName
|
||||||
} from "../util/nameCreators";
|
} from "../util/nameCreators";
|
||||||
import { fillRange } from "../util/typedCrossfilter/util";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Selection state for categoricals are tracked in an Object that
|
|
||||||
has two main components for each category:
|
|
||||||
1. mapping of option value to an index
|
|
||||||
2. array of bool selection state by index
|
|
||||||
Remember that option values can be ANY js type, except undefined/null.
|
|
||||||
|
|
||||||
{
|
|
||||||
_category_name_1: {
|
|
||||||
// map of option value to index
|
|
||||||
categoryIndices: Map([
|
|
||||||
catval1: index,
|
|
||||||
...
|
|
||||||
])
|
|
||||||
|
|
||||||
// index->selection true/false state
|
|
||||||
categorySelected: [ true/false, true/false, ... ]
|
|
||||||
|
|
||||||
// number of options
|
|
||||||
numCategories: number,
|
|
||||||
|
|
||||||
// isTruncated - true if the options for selection has
|
|
||||||
// been truncated (ie, was too large to implement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
function topNCategories(summary) {
|
|
||||||
const counts = _.map(summary.categories, cat =>
|
|
||||||
summary.categoryCounts.get(cat)
|
|
||||||
);
|
|
||||||
const sortIndex = fillRange(new Array(summary.numCategories)).sort(
|
|
||||||
(a, b) => counts[b] - counts[a]
|
|
||||||
);
|
|
||||||
const sortedCategories = _.map(sortIndex, i => summary.categories[i]);
|
|
||||||
const sortedCounts = _.map(sortIndex, i => counts[i]);
|
|
||||||
const N = globals.maxCategoricalOptionsToDisplay;
|
|
||||||
|
|
||||||
if (sortedCategories.length < N) {
|
|
||||||
return [sortedCategories, sortedCounts];
|
|
||||||
}
|
|
||||||
return [sortedCategories.slice(0, N), sortedCounts.slice(0, N)];
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCategoricalSelectionState(state, world) {
|
|
||||||
const res = {};
|
|
||||||
_.forEach(world.summary.obs, (value, key) => {
|
|
||||||
if (value.categories) {
|
|
||||||
const isColorField = key.includes("color") || key.includes("Color");
|
|
||||||
const isSelectableCategory =
|
|
||||||
!isColorField &&
|
|
||||||
key !== "name" &&
|
|
||||||
value.categories.length < state.maxCategoryItems;
|
|
||||||
if (isSelectableCategory) {
|
|
||||||
const [categoryValues, categoryCounts] = topNCategories(value);
|
|
||||||
const categoryIndices = new Map(categoryValues.map((v, i) => [v, i]));
|
|
||||||
const numCategories = categoryIndices.size;
|
|
||||||
const categorySelected = new Array(numCategories).fill(true);
|
|
||||||
const isTruncated = categoryValues.length < value.numCategories;
|
|
||||||
res[key] = {
|
|
||||||
categoryValues, // array: of natively typed category values
|
|
||||||
categoryIndices, // map: category value (native type) -> category index
|
|
||||||
categorySelected, // array: t/f selection state
|
|
||||||
numCategories, // number: of categories
|
|
||||||
isTruncated, // bool: true if list was truncated
|
|
||||||
categoryCounts // array: cardinality of each category
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
given a categoricalSelectionState, return the list of all category values
|
|
||||||
where selection state is true (ie, they are selected).
|
|
||||||
*/
|
|
||||||
function selectedValuesForCategory(categorySelectionState) {
|
|
||||||
const selectedValues = _([...categorySelectionState.categoryIndices])
|
|
||||||
.filter(tuple => categorySelectionState.categorySelected[tuple[1]])
|
|
||||||
.map(tuple => tuple[0])
|
|
||||||
.value();
|
|
||||||
return selectedValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Controls = (
|
const Controls = (
|
||||||
state = {
|
state = {
|
||||||
@@ -111,6 +25,7 @@ const Controls = (
|
|||||||
|
|
||||||
// the whole big bang
|
// the whole big bang
|
||||||
universe: null,
|
universe: null,
|
||||||
|
fullUniverseCache: null,
|
||||||
|
|
||||||
// all of the data + selection state
|
// all of the data + selection state
|
||||||
world: null,
|
world: null,
|
||||||
@@ -164,16 +79,14 @@ const Controls = (
|
|||||||
case "initial data load start": {
|
case "initial data load start": {
|
||||||
return { ...state, loading: true };
|
return { ...state, loading: true };
|
||||||
}
|
}
|
||||||
case "initial data load complete (universe exists)":
|
case "initial data load complete (universe exists)": {
|
||||||
case "reset World to eq Universe": {
|
|
||||||
const { userDefinedGenes, diffexpGenes } = state;
|
|
||||||
/* first light - create world & other data-driven defaults */
|
/* first light - create world & other data-driven defaults */
|
||||||
const { universe } = action;
|
const { universe } = action;
|
||||||
const world = World.createWorldFromEntireUniverse(universe);
|
const world = World.createWorldFromEntireUniverse(universe);
|
||||||
const colorRGB = new Array(universe.nObs).fill(
|
const colorRGB = new Array(universe.nObs).fill(
|
||||||
parseRGB(globals.defaultCellColor)
|
parseRGB(globals.defaultCellColor)
|
||||||
);
|
);
|
||||||
const categoricalSelectionState = createCategoricalSelectionState(
|
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
|
||||||
state,
|
state,
|
||||||
world
|
world
|
||||||
);
|
);
|
||||||
@@ -181,51 +94,58 @@ const Controls = (
|
|||||||
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
||||||
WorldUtil.clearCaches();
|
WorldUtil.clearCaches();
|
||||||
|
|
||||||
const worldVarDataCache = world.varDataCache;
|
|
||||||
|
|
||||||
// dimensionMap = {
|
|
||||||
// layout_X: dim-for-X,
|
|
||||||
// obsAnno_name: dim for an annotation,
|
|
||||||
// varData_userDefined_genename: dim for user defined expression,
|
|
||||||
// varData_diffexp_genename: dim for diff-exp added gene expression
|
|
||||||
// }
|
|
||||||
/* var dimensions */
|
|
||||||
if (userDefinedGenes.length > 0) {
|
|
||||||
/*
|
|
||||||
verbose & slightly confusing that we also access this as an object
|
|
||||||
in controls rather than an array, should be abstracted into
|
|
||||||
util ie., createDimensionsFromBothListsOfGenes(userGenes, diffExp)
|
|
||||||
*/
|
|
||||||
_.forEach(userDefinedGenes, gene => {
|
|
||||||
dimensionMap[
|
|
||||||
userDefinedDimensionName(gene)
|
|
||||||
] = World.createVarDimension(
|
|
||||||
/* "__var__" + */
|
|
||||||
world,
|
|
||||||
worldVarDataCache,
|
|
||||||
crossfilter,
|
|
||||||
gene
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffexpGenes.length > 0) {
|
|
||||||
_.forEach(diffexpGenes, gene => {
|
|
||||||
dimensionMap[diffexpDimensionName(gene)] = World.createVarDimension(
|
|
||||||
/* "__var__" + */
|
|
||||||
world,
|
|
||||||
worldVarDataCache,
|
|
||||||
crossfilter,
|
|
||||||
gene
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
universe,
|
universe,
|
||||||
|
fullUniverseCache: { world, crossfilter, dimensionMap },
|
||||||
|
world,
|
||||||
|
colorRGB,
|
||||||
|
categoricalSelectionState,
|
||||||
|
crossfilter,
|
||||||
|
dimensionMap,
|
||||||
|
colorAccessor: null,
|
||||||
|
resettingInterface: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "reset World to eq Universe": {
|
||||||
|
const {
|
||||||
|
userDefinedGenes,
|
||||||
|
diffexpGenes,
|
||||||
|
universe,
|
||||||
|
fullUniverseCache
|
||||||
|
} = state;
|
||||||
|
const { world, crossfilter } = fullUniverseCache;
|
||||||
|
// reset all crossfilter dimensions
|
||||||
|
_.forEach(fullUniverseCache.dimensionMap, dim => dim.filterAll());
|
||||||
|
const colorRGB = new Array(universe.nObs).fill(
|
||||||
|
parseRGB(globals.defaultCellColor)
|
||||||
|
);
|
||||||
|
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
|
||||||
|
state,
|
||||||
|
world
|
||||||
|
);
|
||||||
|
|
||||||
|
/* free dimensions not in cache (otherwise they leak) */
|
||||||
|
_.forEach(state.dimensionMap, (dim, dimName) => {
|
||||||
|
if (!fullUniverseCache.dimensionMap[dimName]) {
|
||||||
|
dim.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const dimensionMap = {
|
||||||
|
...fullUniverseCache.dimensionMap,
|
||||||
|
...ControlsHelper.createGenesDimMap(
|
||||||
|
userDefinedGenes,
|
||||||
|
diffexpGenes,
|
||||||
|
world,
|
||||||
|
crossfilter
|
||||||
|
)
|
||||||
|
};
|
||||||
|
WorldUtil.clearCaches();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
world,
|
world,
|
||||||
colorRGB,
|
colorRGB,
|
||||||
categoricalSelectionState,
|
categoricalSelectionState,
|
||||||
@@ -247,48 +167,22 @@ const Controls = (
|
|||||||
const colorRGB = new Array(world.nObs).fill(
|
const colorRGB = new Array(world.nObs).fill(
|
||||||
parseRGB(globals.defaultCellColor)
|
parseRGB(globals.defaultCellColor)
|
||||||
);
|
);
|
||||||
const categoricalSelectionState = createCategoricalSelectionState(
|
const categoricalSelectionState = ControlsHelper.createCategoricalSelectionState(
|
||||||
state,
|
state,
|
||||||
world
|
world
|
||||||
);
|
);
|
||||||
const crossfilter = Crossfilter(world.obsAnnotations);
|
const crossfilter = Crossfilter(world.obsAnnotations);
|
||||||
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
const dimensionMap = {
|
||||||
|
...World.createObsDimensionMap(crossfilter, world),
|
||||||
|
...ControlsHelper.createGenesDimMap(
|
||||||
|
userDefinedGenes,
|
||||||
|
diffexpGenes,
|
||||||
|
world,
|
||||||
|
crossfilter
|
||||||
|
)
|
||||||
|
};
|
||||||
WorldUtil.clearCaches();
|
WorldUtil.clearCaches();
|
||||||
|
|
||||||
const worldVarDataCache = world.varDataCache;
|
|
||||||
/* var dimensions */
|
|
||||||
|
|
||||||
if (userDefinedGenes.length > 0) {
|
|
||||||
/*
|
|
||||||
verbose & slightly confusing that we also access this as an object
|
|
||||||
in controls rather than an array, should be abstracted into
|
|
||||||
util ie., createDimensionsFromBothListsOfGenes(userGenes, diffExp)
|
|
||||||
*/
|
|
||||||
_.forEach(userDefinedGenes, gene => {
|
|
||||||
dimensionMap[
|
|
||||||
userDefinedDimensionName(gene)
|
|
||||||
] = World.createVarDimension(
|
|
||||||
/* "__var__" + */
|
|
||||||
world,
|
|
||||||
worldVarDataCache,
|
|
||||||
crossfilter,
|
|
||||||
gene
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffexpGenes.length > 0) {
|
|
||||||
_.forEach(diffexpGenes, gene => {
|
|
||||||
dimensionMap[diffexpDimensionName(gene)] = World.createVarDimension(
|
|
||||||
/* "__var__" + */
|
|
||||||
world,
|
|
||||||
worldVarDataCache,
|
|
||||||
crossfilter,
|
|
||||||
gene
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -303,28 +197,66 @@ const Controls = (
|
|||||||
}
|
}
|
||||||
case "expression load success": {
|
case "expression load success": {
|
||||||
const { world, universe } = state;
|
const { world, universe } = state;
|
||||||
let universeVarDataCache = universe.varDataCache;
|
let universeVarData = universe.varData;
|
||||||
let worldVarDataCache = world.varDataCache;
|
let worldVarData = world.varData;
|
||||||
|
|
||||||
|
// Load new expression data into the varData dataframes, if
|
||||||
|
// not already present.
|
||||||
_.forEach(action.expressionData, (val, key) => {
|
_.forEach(action.expressionData, (val, key) => {
|
||||||
universeVarDataCache = kvCache.set(universeVarDataCache, key, val);
|
// If not already in universe.varData, save entire expression column
|
||||||
if (kvCache.get(worldVarDataCache, key) === undefined) {
|
if (!universeVarData.hasCol(key)) {
|
||||||
worldVarDataCache = kvCache.set(
|
universeVarData = universeVarData.withCol(key, val);
|
||||||
worldVarDataCache,
|
}
|
||||||
|
|
||||||
|
// If not already in world.varData, save sliced expression column
|
||||||
|
if (!worldVarData.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(world, universe)) {
|
||||||
|
worldValSlice = universeVarData
|
||||||
|
.subset(world.obsAnnotations.rowIndex.keys(), [key], null)
|
||||||
|
.icol(0)
|
||||||
|
.asArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now build world's varData dataframe
|
||||||
|
worldVarData = worldVarData.withCol(
|
||||||
key,
|
key,
|
||||||
World.subsetVarData(world, universe, val)
|
worldValSlice,
|
||||||
|
world.obsAnnotations.rowIndex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Prune size of varData "cache" if getting out of hand....
|
||||||
|
const { userDefinedGenes, diffexpGenes } = state;
|
||||||
|
const allTheGenesWeNeed = _.uniq(
|
||||||
|
[].concat(
|
||||||
|
userDefinedGenes,
|
||||||
|
diffexpGenes,
|
||||||
|
Object.keys(action.expressionData)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
universeVarData = ControlsHelper.pruneVarDataCache(
|
||||||
|
universeVarData,
|
||||||
|
allTheGenesWeNeed
|
||||||
|
);
|
||||||
|
worldVarData = ControlsHelper.pruneVarDataCache(
|
||||||
|
worldVarData,
|
||||||
|
allTheGenesWeNeed
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
universe: {
|
universe: {
|
||||||
...universe,
|
...universe,
|
||||||
varDataCache: universeVarDataCache
|
varData: universeVarData
|
||||||
},
|
},
|
||||||
world: {
|
world: {
|
||||||
...world,
|
...world,
|
||||||
varDataCache: worldVarDataCache
|
varData: worldVarData
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -342,17 +274,12 @@ const Controls = (
|
|||||||
}
|
}
|
||||||
case "request user defined gene success": {
|
case "request user defined gene success": {
|
||||||
const { world, crossfilter, dimensionMap, userDefinedGenes } = state;
|
const { world, crossfilter, dimensionMap, userDefinedGenes } = state;
|
||||||
const worldVarDataCache = world.varDataCache;
|
|
||||||
const _userDefinedGenes = userDefinedGenes.slice();
|
const _userDefinedGenes = userDefinedGenes.slice();
|
||||||
const gene = action.data.genes[0];
|
const gene = action.data.genes[0];
|
||||||
|
|
||||||
dimensionMap[userDefinedDimensionName(gene)] = World.createVarDimension(
|
dimensionMap[
|
||||||
/* "__var__" + */
|
userDefinedDimensionName(gene)
|
||||||
world,
|
] = World.createVarDataDimension(world, crossfilter, gene);
|
||||||
worldVarDataCache,
|
|
||||||
crossfilter,
|
|
||||||
gene
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -363,18 +290,15 @@ const Controls = (
|
|||||||
}
|
}
|
||||||
case "request differential expression success": {
|
case "request differential expression success": {
|
||||||
const { world, crossfilter, dimensionMap } = state;
|
const { world, crossfilter, dimensionMap } = state;
|
||||||
const worldVarDataCache = world.varDataCache;
|
|
||||||
const _diffexpGenes = [];
|
const _diffexpGenes = [];
|
||||||
|
|
||||||
action.data.forEach(d => {
|
action.data.forEach(d => {
|
||||||
_diffexpGenes.push(world.varAnnotations[d[0]].name);
|
_diffexpGenes.push(world.varAnnotations.at(d[0], "name"));
|
||||||
});
|
});
|
||||||
|
|
||||||
_.forEach(_diffexpGenes, gene => {
|
_.forEach(_diffexpGenes, gene => {
|
||||||
dimensionMap[diffexpDimensionName(gene)] = World.createVarDimension(
|
dimensionMap[diffexpDimensionName(gene)] = World.createVarDataDimension(
|
||||||
/* "__var__" + */
|
|
||||||
world,
|
world,
|
||||||
worldVarDataCache,
|
|
||||||
crossfilter,
|
crossfilter,
|
||||||
gene
|
gene
|
||||||
);
|
);
|
||||||
@@ -387,13 +311,10 @@ const Controls = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "clear differential expression": {
|
case "clear differential expression": {
|
||||||
const { world, universe, dimensionMap } = state;
|
const { world, dimensionMap } = state;
|
||||||
const _dimensionMap = dimensionMap;
|
const _dimensionMap = dimensionMap;
|
||||||
const universeVarDataCache = universe.varDataCache;
|
|
||||||
const worldVarDataCache = world.varDataCache;
|
|
||||||
|
|
||||||
_.forEach(action.diffExp, values => {
|
_.forEach(action.diffExp, values => {
|
||||||
const { name } = world.varAnnotations[values[0]];
|
const name = world.varAnnotations.at(values[0], "name");
|
||||||
// clean up crossfilter dimensions
|
// clean up crossfilter dimensions
|
||||||
const dimension = dimensionMap[diffexpDimensionName(name)];
|
const dimension = dimensionMap[diffexpDimensionName(name)];
|
||||||
dimension.dispose();
|
dimension.dispose();
|
||||||
@@ -402,15 +323,7 @@ const Controls = (
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
dimensionMap: _dimensionMap,
|
dimensionMap: _dimensionMap,
|
||||||
diffexpGenes: [],
|
diffexpGenes: []
|
||||||
universe: {
|
|
||||||
...universe,
|
|
||||||
varDataCache: universeVarDataCache
|
|
||||||
},
|
|
||||||
world: {
|
|
||||||
...world,
|
|
||||||
varDataCache: worldVarDataCache
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "user defined gene": {
|
case "user defined gene": {
|
||||||
@@ -563,7 +476,7 @@ const Controls = (
|
|||||||
// update the filter to match all selected options
|
// update the filter to match all selected options
|
||||||
const cat = newCategoricalSelectionState[action.metadataField];
|
const cat = newCategoricalSelectionState[action.metadataField];
|
||||||
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
||||||
selectedValuesForCategory(cat)
|
ControlsHelper.selectedValuesForCategory(cat)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -587,7 +500,7 @@ const Controls = (
|
|||||||
// update the filter to match all selected options
|
// update the filter to match all selected options
|
||||||
const cat = newCategoricalSelectionState[action.metadataField];
|
const cat = newCategoricalSelectionState[action.metadataField];
|
||||||
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
||||||
selectedValuesForCategory(cat)
|
ControlsHelper.selectedValuesForCategory(cat)
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,574 @@
|
|||||||
|
import { IdentityInt32Index, isLabelIndex } from "./labelIndex";
|
||||||
|
// weird cross-dependency that we should clean up someday...
|
||||||
|
import { sort } from "../typedCrossfilter/sort";
|
||||||
|
import { isTypedArray, isArrayOrTypedArray, callOnceLazy } from "./util";
|
||||||
|
import { summarizeContinuous, summarizeCategorical } from "./summarize";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dataframe is an immutable 2D matrix similiar to Python Pandas Dataframe,
|
||||||
|
but (currently) without all of the surrounding support functions.
|
||||||
|
Data is stored in column-major layout, and each column is monomorphic.
|
||||||
|
|
||||||
|
It supports:
|
||||||
|
* Relatively efficient creation, cloning and subsetting
|
||||||
|
* Very efficient columnar access (eg, sum down a column), and access
|
||||||
|
to the underlying column arrays.
|
||||||
|
* Data access by row/col offset or label. Labels are reasonably well
|
||||||
|
optimized for both numeric lables and arbitrary (eg, sting) labels.
|
||||||
|
|
||||||
|
It does not currently support:
|
||||||
|
* Views on matrix subset - for currently known access patterns,
|
||||||
|
it is more effiicent to copy on subsetting, optimizing for access
|
||||||
|
speed over memory use.
|
||||||
|
* JS iterators - they are too slow. Use explicit iteration over
|
||||||
|
offest or labels.
|
||||||
|
|
||||||
|
Important assumptions embedded in the API:
|
||||||
|
* Columns are implicitly categorical if they are a JS Array and numeric
|
||||||
|
(aka continuous) if they are a TypedArray.
|
||||||
|
|
||||||
|
There are three index types for row/col indexing:
|
||||||
|
* IdentityInt32Index - noop index, where the index label is the offset.
|
||||||
|
* KeyIndex - index arbitrary JS objects.
|
||||||
|
* DenseInt32Index - integer indexing. Optimization over KeyIndex as it uses
|
||||||
|
Int32Array as a back-map to offsets. This means that the index array
|
||||||
|
must be sized to [minLabel, maxLabel), so this is only useful when the label
|
||||||
|
range is relatively close the underlying offset range [minOffset, maxOffset).
|
||||||
|
|
||||||
|
All private functions/methods/fields are prefixed by '__', eg, __compile().
|
||||||
|
Don't use them outside of this file.
|
||||||
|
|
||||||
|
Simple example:
|
||||||
|
|
||||||
|
// default indexing is integer offset.
|
||||||
|
const df = Dataframe.create([2,2], [['a', 'b'], [0, 1]])
|
||||||
|
console.log(df.at(0,0)); // outputs: a
|
||||||
|
console.log(df.col(1).asArray()); // outputs: [0, 1]
|
||||||
|
|
||||||
|
// KeyIndex
|
||||||
|
const df = new Dataframe([1,2], [['a'], ['b']], null, new KeyIndex(['A', 'B']))
|
||||||
|
console.log(df.at(0, 'A')); // outputs: a
|
||||||
|
console.log(df.col('A').asArray(); // outputs: ['a']
|
||||||
|
|
||||||
|
Performance tuning is primarily focused on columnar access patterns, which is the
|
||||||
|
dominant pattern in cellxgene.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
Dataframe
|
||||||
|
**/
|
||||||
|
|
||||||
|
class Dataframe {
|
||||||
|
/**
|
||||||
|
Constructors & factories
|
||||||
|
**/
|
||||||
|
|
||||||
|
constructor(dims, columnarData, rowIndex = null, colIndex = null) {
|
||||||
|
/*
|
||||||
|
The base constructor is relatively hard to use - as an alternative,
|
||||||
|
see factory methods and clone/slice, below.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
* dims - 2D array describing intendend dimensionality: [nRows,nCols].
|
||||||
|
* columnarData - JS array, nCols in length, containing array
|
||||||
|
or TypedArray of length nRows.
|
||||||
|
* rowIndex/colIndex - null (create default index using offsets as key),
|
||||||
|
or a caller-provided index.
|
||||||
|
All columns and indices must have appropriate dimensionality.
|
||||||
|
*/
|
||||||
|
const [nRows, nCols] = dims;
|
||||||
|
if (nRows < 0 || nCols < 0) {
|
||||||
|
throw new RangeError("Dataframe dimensions must be positive");
|
||||||
|
}
|
||||||
|
if (!rowIndex) {
|
||||||
|
rowIndex = new IdentityInt32Index(nRows);
|
||||||
|
}
|
||||||
|
if (!colIndex) {
|
||||||
|
colIndex = new IdentityInt32Index(nCols);
|
||||||
|
}
|
||||||
|
Dataframe.__errorChecks(dims, columnarData, rowIndex, colIndex);
|
||||||
|
|
||||||
|
this.__columns = Array.from(columnarData);
|
||||||
|
this.dims = dims;
|
||||||
|
this.length = nRows; // convenience accessor for row dimension
|
||||||
|
this.rowIndex = rowIndex;
|
||||||
|
this.colIndex = colIndex;
|
||||||
|
|
||||||
|
this.__compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
static __errorChecks(dims, columnarData, rowIndex, colIndex) {
|
||||||
|
const [nRows, nCols] = dims;
|
||||||
|
|
||||||
|
/* check for expected types */
|
||||||
|
if (!Array.isArray(columnarData)) {
|
||||||
|
throw new TypeError("Dataframe constructor requires array of columns");
|
||||||
|
}
|
||||||
|
if (!columnarData.every(c => isArrayOrTypedArray(c))) {
|
||||||
|
throw new TypeError("Dataframe columns must all be Array or TypedArray");
|
||||||
|
}
|
||||||
|
if (!isLabelIndex(rowIndex)) {
|
||||||
|
throw new TypeError("Dataframe rowIndex is an unsupported type.");
|
||||||
|
}
|
||||||
|
if (!isLabelIndex(colIndex)) {
|
||||||
|
throw new TypeError("Dataframe colIndex is an unsupported type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check for expected dimensionality / size */
|
||||||
|
if (
|
||||||
|
nCols !== columnarData.length ||
|
||||||
|
!columnarData.every(c => c.length === nRows)
|
||||||
|
) {
|
||||||
|
throw new RangeError(
|
||||||
|
"Dataframe dimension does not match provided data shape"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nRows !== rowIndex.size()) {
|
||||||
|
throw new RangeError(
|
||||||
|
"Dataframe rowIndex must have same size as underlying data"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nCols !== colIndex.size()) {
|
||||||
|
throw new RangeError(
|
||||||
|
"Dataframe colIndex must have same size as underlying data"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__compile() {
|
||||||
|
/*
|
||||||
|
Compile data accessors for each column.
|
||||||
|
|
||||||
|
Each column accessor is a function which will lookup data by
|
||||||
|
index (ie, is equivalent to dataframe.get(row, col), where 'col'
|
||||||
|
is fixed.
|
||||||
|
|
||||||
|
In addition, each column accessor has several functions:
|
||||||
|
|
||||||
|
asArray() -- return the entire column as a native Array or TypedArray.
|
||||||
|
Crucially, this native array only supports label indexing.
|
||||||
|
Example:
|
||||||
|
const arr = df.col('a').asArray();
|
||||||
|
|
||||||
|
has(rlabel) -- return boolean indicating of the row label
|
||||||
|
is contained within the column. Example:
|
||||||
|
const isInColumn = df.col('a').includes(99)
|
||||||
|
For the default offset indexing, this is identical to:
|
||||||
|
const isInColumn = (99 > 0) && (99 < df.nRows);
|
||||||
|
|
||||||
|
ihas(roffset) -- same as has(), but accepts a row offset
|
||||||
|
instead of a row label.
|
||||||
|
|
||||||
|
indexOf(value) -- return the label (not offset) of the first instance of
|
||||||
|
'value' in the column. If you want the offset, just use the builtin JS
|
||||||
|
indexOf() function, available on both Array and TypedArray.
|
||||||
|
|
||||||
|
iget(offset) -- return the value at 'offset'
|
||||||
|
|
||||||
|
*/
|
||||||
|
const { getOffset, getLabel } = this.rowIndex;
|
||||||
|
this.__columnsAccessor = this.__columns.map(column => {
|
||||||
|
const { length } = column;
|
||||||
|
|
||||||
|
/* get value by row label */
|
||||||
|
const get = function get(rlabel) {
|
||||||
|
return column[getOffset(rlabel)];
|
||||||
|
};
|
||||||
|
|
||||||
|
/* get value by row offset */
|
||||||
|
const iget = function iget(roffset) {
|
||||||
|
return column[roffset];
|
||||||
|
};
|
||||||
|
|
||||||
|
/* full column array access */
|
||||||
|
const asArray = function asArray() {
|
||||||
|
return column;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* test for row label inclusion in column */
|
||||||
|
const has = function has(rlabel) {
|
||||||
|
const offset = getOffset(rlabel);
|
||||||
|
return offset >= 0 && offset < length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ihas = function ihas(offset) {
|
||||||
|
return offset >= 0 && offset < length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
return first label (index) at which the value is found in this column,
|
||||||
|
or undefined if not found.
|
||||||
|
|
||||||
|
NOTE: not found return is DIFFERENT than the default Array.indexOf as
|
||||||
|
-1 is a plausible Dataframe row/col label.
|
||||||
|
*/
|
||||||
|
const indexOf = function indexOf(value) {
|
||||||
|
const offset = column.indexOf(value);
|
||||||
|
if (offset === -1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getLabel(offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Summarize the column data. Lazy eval;
|
||||||
|
*/
|
||||||
|
const summarize = callOnceLazy(() =>
|
||||||
|
isTypedArray(column)
|
||||||
|
? summarizeContinuous(column)
|
||||||
|
: summarizeCategorical(column)
|
||||||
|
);
|
||||||
|
|
||||||
|
get.summarize = summarize;
|
||||||
|
get.asArray = asArray;
|
||||||
|
get.has = has;
|
||||||
|
get.ihas = ihas;
|
||||||
|
get.indexOf = indexOf;
|
||||||
|
get.iget = iget;
|
||||||
|
return get;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
/*
|
||||||
|
Clone this dataframe
|
||||||
|
*/
|
||||||
|
return new this.constructor(
|
||||||
|
this.dims,
|
||||||
|
[...this.__columns],
|
||||||
|
this.rowIndex,
|
||||||
|
this.colIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
withCol(label, colData, withRowIndex = null) {
|
||||||
|
/*
|
||||||
|
Create a new DF, which is `this` plus the new column. Example:
|
||||||
|
const newDf = df.withCol("foo", [1,2,3]);
|
||||||
|
|
||||||
|
Dimensionality of new column must match existing dataframe.
|
||||||
|
|
||||||
|
Special case: empty dataframe will accept any size column. Example:
|
||||||
|
const newDf = Dataframe.empty().withCol("foo", [1,2,3]);
|
||||||
|
|
||||||
|
If `withRowIndex` specified, the provided index will become the
|
||||||
|
rowIndex for the newly created dataframe. If not specified,
|
||||||
|
the rowIndex from `this` will be used (ie, the rowIndex is
|
||||||
|
unchanged).
|
||||||
|
*/
|
||||||
|
let dims;
|
||||||
|
let rowIndex;
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
dims = [colData.length, 1];
|
||||||
|
rowIndex = null;
|
||||||
|
} else {
|
||||||
|
dims = [this.dims[0], this.dims[1] + 1];
|
||||||
|
({ rowIndex } = this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withRowIndex) {
|
||||||
|
rowIndex = withRowIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [...this.__columns];
|
||||||
|
columns.push(colData);
|
||||||
|
const colIndex = this.colIndex.withLabel(label);
|
||||||
|
return new this.constructor(dims, columns, rowIndex, colIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
dropCol(label) {
|
||||||
|
/*
|
||||||
|
Create a new dataframe, omitting one columns.
|
||||||
|
|
||||||
|
const newDf = df.dropCol("colors");
|
||||||
|
*/
|
||||||
|
const dims = [this.dims[0], this.dims[1] - 1];
|
||||||
|
const coffset = this.colIndex.getOffset(label);
|
||||||
|
const columns = [...this.__columns];
|
||||||
|
columns.splice(coffset, 1);
|
||||||
|
const colIndex = this.colIndex.dropLabel(label);
|
||||||
|
return new this.constructor(dims, columns, this.rowIndex, colIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static empty(rowIndex = null, colIndex = null) {
|
||||||
|
return new Dataframe([0, 0], [], rowIndex, colIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(dims, columnarData) {
|
||||||
|
/*
|
||||||
|
Create a dataframe from raw columnar data. All column arrays
|
||||||
|
must have the same length. Identity indexing will be used.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
const df = Dataframe.create([2,2], [new Uint32Array(2), new Float32Array(2)]);
|
||||||
|
*/
|
||||||
|
return new Dataframe(dims, columnarData, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
__subset(rowOffsets, colOffsets, withRowIndex) {
|
||||||
|
const dims = [...this.dims];
|
||||||
|
|
||||||
|
const getSortedLabelAndOffsets = (offsets, index) => {
|
||||||
|
/*
|
||||||
|
Given offsets, return both offsets and associated lables,
|
||||||
|
sorted by offset.
|
||||||
|
*/
|
||||||
|
if (!offsets) {
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
const sortedOffsets = sort(offsets);
|
||||||
|
const sortedLabels = new Array(sortedOffsets.length);
|
||||||
|
for (let i = 0, l = sortedOffsets.length; i < l; i += 1) {
|
||||||
|
sortedLabels[i] = index.getLabel(sortedOffsets[i]);
|
||||||
|
}
|
||||||
|
return [sortedLabels, sortedOffsets];
|
||||||
|
};
|
||||||
|
|
||||||
|
let { colIndex } = this;
|
||||||
|
if (colOffsets) {
|
||||||
|
let colLabels;
|
||||||
|
[colLabels, colOffsets] = getSortedLabelAndOffsets(
|
||||||
|
colOffsets,
|
||||||
|
this.colIndex
|
||||||
|
);
|
||||||
|
dims[1] = colOffsets.length;
|
||||||
|
colIndex = this.colIndex.subsetLabels(colLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
let { rowIndex } = this;
|
||||||
|
if (withRowIndex) rowIndex = withRowIndex;
|
||||||
|
if (rowOffsets) {
|
||||||
|
let rowLabels;
|
||||||
|
[rowLabels, rowOffsets] = getSortedLabelAndOffsets(
|
||||||
|
rowOffsets,
|
||||||
|
this.rowIndex
|
||||||
|
);
|
||||||
|
dims[0] = rowLabels.length;
|
||||||
|
if (!withRowIndex) rowIndex = this.rowIndex.subsetLabels(rowLabels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* subset columns */
|
||||||
|
let columns = this.__columns;
|
||||||
|
if (colOffsets) {
|
||||||
|
columns = new Array(colOffsets.length);
|
||||||
|
for (let i = 0, l = colOffsets.length; i < l; i += 1) {
|
||||||
|
columns[i] = this.__columns[colOffsets[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* subset rows */
|
||||||
|
if (rowOffsets) {
|
||||||
|
columns = columns.map(col => {
|
||||||
|
const newCol = new col.constructor(rowOffsets.length);
|
||||||
|
for (let i = 0, l = rowOffsets.length; i < l; i += 1) {
|
||||||
|
newCol[i] = col[rowOffsets[i]];
|
||||||
|
}
|
||||||
|
return newCol;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new Dataframe(dims, columns, rowIndex, colIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
subset(rowLabels, colLabels = null, withRowIndex = null) {
|
||||||
|
/*
|
||||||
|
Subset by row/col labels.
|
||||||
|
|
||||||
|
withRowIndex allows assignment of new row index during subset operation.
|
||||||
|
If withRowIndex === null, it will reset the index to identity (offset)
|
||||||
|
indexing. if withRowIndex is a label index object, it will be used
|
||||||
|
for the new dataframe.
|
||||||
|
*/
|
||||||
|
const toOffsets = (labels, index) => {
|
||||||
|
if (!labels) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return labels.map(label => {
|
||||||
|
const off = index.getOffset(label);
|
||||||
|
if (off === undefined) {
|
||||||
|
throw new RangeError(`unknown label: ${label}`);
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowOffsets = toOffsets(rowLabels, this.rowIndex);
|
||||||
|
const colOffsets = toOffsets(colLabels, this.colIndex);
|
||||||
|
return this.__subset(rowOffsets, colOffsets, withRowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
isubset(rowOffsets, colOffsets = null, withRowIndex = null) {
|
||||||
|
/*
|
||||||
|
Subset by row/col offset.
|
||||||
|
|
||||||
|
withRowIndex allows assignment of new row index during subset operation.
|
||||||
|
If withRowIndex === null, it will reset the index to identity (offset)
|
||||||
|
indexing. if withRowIndex is a label index object, it will be used
|
||||||
|
for the new dataframe.
|
||||||
|
*/
|
||||||
|
return this.__subset(rowOffsets, colOffsets, withRowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
isubsetMask(rowMask, colMask = null, withRowIndex = null) {
|
||||||
|
/*
|
||||||
|
Subset on row/column based upon a truthy/falsey array (a mask).
|
||||||
|
|
||||||
|
withRowIndex allows assignment of new row index during subset operation.
|
||||||
|
If withRowIndex === null, it will reset the index to identity (offset)
|
||||||
|
indexing. if withRowIndex is a label index object, it will be used
|
||||||
|
for the new dataframe.
|
||||||
|
*/
|
||||||
|
const [nRows, nCols] = this.dims;
|
||||||
|
if (
|
||||||
|
(rowMask && rowMask.length !== nRows) ||
|
||||||
|
(colMask && colMask.length !== nCols)
|
||||||
|
) {
|
||||||
|
throw new RangeError("boolean arrays must match row/col dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* convert masks to lists - method wastes space, but is fast */
|
||||||
|
const toList = (mask, maxSize) => {
|
||||||
|
if (!mask) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const list = new Int32Array(maxSize);
|
||||||
|
let elems = 0;
|
||||||
|
for (let i = 0, l = mask.length; i < l; i += 1) {
|
||||||
|
if (mask[i]) {
|
||||||
|
list[elems] = i;
|
||||||
|
elems += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Int32Array(list.buffer, 0, elems);
|
||||||
|
};
|
||||||
|
const rowOffsets = toList(rowMask, nRows);
|
||||||
|
const colOffsets = toList(colMask, nCols);
|
||||||
|
return this.__subset(rowOffsets, colOffsets, withRowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Data access with row/col.
|
||||||
|
**/
|
||||||
|
|
||||||
|
col(columnLabel) {
|
||||||
|
/*
|
||||||
|
Return accessor bound to a column. Allows random row access
|
||||||
|
based upon the row indexing. Returns undefined if the
|
||||||
|
columnLabel is not present in the dataframe.
|
||||||
|
|
||||||
|
Example for a dataframe with string labeled columns, and
|
||||||
|
default (offset) indices for rows (eg, [0, 'foo'])
|
||||||
|
|
||||||
|
const getValue = df.col('foo');
|
||||||
|
for (let r = 0; r < df.nRows; r += 1) {
|
||||||
|
console.log(r, getValue(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
See __compile() for the functions available in a column accessor.
|
||||||
|
*/
|
||||||
|
const coff = this.colIndex.getOffset(columnLabel);
|
||||||
|
return this.__columnsAccessor[coff];
|
||||||
|
}
|
||||||
|
|
||||||
|
icol(columnOffset) {
|
||||||
|
/*
|
||||||
|
Return column accessor by offset.
|
||||||
|
*/
|
||||||
|
return this.__columnsAccessor[columnOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
at(r, c) {
|
||||||
|
/*
|
||||||
|
Access a single value, for a row/col label pair.
|
||||||
|
|
||||||
|
For performance reasons, there are no bounds or existance
|
||||||
|
checks on labels, and no defined behavior when these are supplied.
|
||||||
|
May return undefined, throw an Error, or do something else for
|
||||||
|
non-existant labels. If you want predictable out-of-bounds
|
||||||
|
behavior, use has(), eg,
|
||||||
|
|
||||||
|
const myVal = df.has(r,l) ? df.at(r,l) : undefined;
|
||||||
|
*/
|
||||||
|
const coff = this.colIndex.getOffset(c);
|
||||||
|
const roff = this.rowIndex.getOffset(r);
|
||||||
|
return this.__columns[coff][roff];
|
||||||
|
}
|
||||||
|
|
||||||
|
iat(r, c) {
|
||||||
|
/*
|
||||||
|
Access a single value, for a row/col offset (integer) position.
|
||||||
|
|
||||||
|
For performance reasons, there are no bounds checks on row/col offsets
|
||||||
|
or other well-defined behavior for out-of-bounds values. If you want
|
||||||
|
well-defined bounds checking, use ihas(), eg,
|
||||||
|
|
||||||
|
const myVal = df.ihas(r, c) ? df.iat(r, c) : undefined;
|
||||||
|
*/
|
||||||
|
return this.__columns[c][r];
|
||||||
|
}
|
||||||
|
|
||||||
|
has(r, c) {
|
||||||
|
/*
|
||||||
|
Test if row/col labels exist in the dataframe - returns true/false
|
||||||
|
*/
|
||||||
|
const [nRows, nCols] = this.dims;
|
||||||
|
const coff = this.colIndex.getOffset(c);
|
||||||
|
const roff = this.rowIndex.getOffset(r);
|
||||||
|
return coff >= 0 && coff < nCols && roff >= 0 && roff < nRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
ihas(r, c) {
|
||||||
|
/*
|
||||||
|
Test if row/col offset (integer) position exists in the
|
||||||
|
dataframe - returns true/false
|
||||||
|
*/
|
||||||
|
const [nRows, nCols] = this.dims;
|
||||||
|
return c >= 0 && c < nCols && r >= 0 && r < nRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCol(c) {
|
||||||
|
/*
|
||||||
|
Test if col label exists - return true/false
|
||||||
|
*/
|
||||||
|
return !!this.col(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
/*
|
||||||
|
Return true if this is an empty dataframe, ie, has dimensions [0,0]
|
||||||
|
*/
|
||||||
|
const [rows, cols] = this.dims;
|
||||||
|
return rows === 0 && cols === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
Functional (map/reduce/etc) data access
|
||||||
|
|
||||||
|
XXX: not yet implemented, as there is no clear use case. Can easily
|
||||||
|
add these as useful.
|
||||||
|
****/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Map & reduce of column or row
|
||||||
|
|
||||||
|
XXX TODO remainder of map/reduce functions: mapCol, mapRow, reduceRow, ...
|
||||||
|
*/
|
||||||
|
/* comment out until we have a use for this
|
||||||
|
|
||||||
|
reduceCol(clabel, callback, initialValue) {
|
||||||
|
const coff = this.colIndex.getOffset(clabel);
|
||||||
|
const column = this.__columns[coff];
|
||||||
|
let start = 0;
|
||||||
|
let acc = initialValue;
|
||||||
|
if (initialValue === undefined) {
|
||||||
|
acc = column[0];
|
||||||
|
start = 1;
|
||||||
|
}
|
||||||
|
for (let i = start, l = column.length; i < l; i += 1) {
|
||||||
|
acc = callback(acc, column[i]);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dataframe;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Dataframe } from "./dataframe";
|
||||||
|
export { DenseInt32Index, IdentityInt32Index, KeyIndex } from "./labelIndex";
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
Label indexing - map a label to & from an integer offset. See Dataframe
|
||||||
|
for how this is used.
|
||||||
|
**/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Private utility functions
|
||||||
|
*/
|
||||||
|
function extent(tarr) {
|
||||||
|
let min = 0x7fffffff;
|
||||||
|
let max = ~min; // eslint-disable-line no-bitwise
|
||||||
|
for (let i = 0, l = tarr.length; i < l; i += 1) {
|
||||||
|
const v = tarr[i];
|
||||||
|
if (v < min) {
|
||||||
|
min = v;
|
||||||
|
}
|
||||||
|
if (v > max) {
|
||||||
|
max = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillRange(arr, start = 0) {
|
||||||
|
const larr = arr;
|
||||||
|
for (let i = 0, l = larr.length; i < l; i += 1) {
|
||||||
|
larr[i] = i + start;
|
||||||
|
}
|
||||||
|
return larr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
class IdentityInt32Index {
|
||||||
|
/*
|
||||||
|
identity/noop index, with small assumptions that labels are int32
|
||||||
|
*/
|
||||||
|
constructor(maxOffset) {
|
||||||
|
this.maxOffset = maxOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
// memoize
|
||||||
|
const k = fillRange(new Int32Array(this.maxOffset));
|
||||||
|
this.keys = function keys() {
|
||||||
|
return k;
|
||||||
|
};
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOffset(i) {
|
||||||
|
// label to offset
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabel(i) {
|
||||||
|
// offset to label
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.maxOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
__promote(labelArray) {
|
||||||
|
/*
|
||||||
|
time/space decision - based on the resulting density
|
||||||
|
*/
|
||||||
|
const [minLabel, maxLabel] = extent(labelArray);
|
||||||
|
const labelSpaceSize = maxLabel - minLabel + 1;
|
||||||
|
const density = labelSpaceSize / this.maxOffset;
|
||||||
|
/* 0.1 is a magic number, that needs testing to optimize */
|
||||||
|
if (density < 0.1) {
|
||||||
|
return new KeyIndex(labelArray);
|
||||||
|
}
|
||||||
|
return new DenseInt32Index(labelArray, [minLabel, maxLabel]);
|
||||||
|
}
|
||||||
|
|
||||||
|
subsetLabels(labelArray) {
|
||||||
|
return this.__promote(labelArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
withLabel(label) {
|
||||||
|
if (label === this.maxOffset) {
|
||||||
|
return new IdentityInt32Index(label + 1);
|
||||||
|
}
|
||||||
|
return this.__promote([...this.keys(), label]);
|
||||||
|
}
|
||||||
|
|
||||||
|
dropLabel(label) {
|
||||||
|
if (label === this.maxOffset - 1) {
|
||||||
|
return new IdentityInt32Index(label);
|
||||||
|
}
|
||||||
|
const labelArray = [...this.keys()];
|
||||||
|
labelArray.splice(labelArray.indexOf(label), 1);
|
||||||
|
return this.__promote(labelArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable class-methods-use-this */
|
||||||
|
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
class DenseInt32Index {
|
||||||
|
/*
|
||||||
|
DenseInt32Index indexes integer labels, and uses Int32Array typed arrays
|
||||||
|
for both forward and reverse indexing. This means that the min/max range
|
||||||
|
of the forward index labels must be known a priori (so that the index
|
||||||
|
array can be pre-allocated).
|
||||||
|
*/
|
||||||
|
constructor(labels, labelRange = null) {
|
||||||
|
if (labels.constructor !== Int32Array) {
|
||||||
|
labels = new Int32Array(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labelRange) {
|
||||||
|
labelRange = extent(labels);
|
||||||
|
}
|
||||||
|
const [minLabel, maxLabel] = labelRange;
|
||||||
|
const labelSpaceSize = maxLabel - minLabel + 1;
|
||||||
|
const index = new Int32Array(labelSpaceSize).fill(-1);
|
||||||
|
for (let i = 0, l = labels.length; i < l; i += 1) {
|
||||||
|
const label = labels[i];
|
||||||
|
index[label - minLabel] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.minLabel = minLabel;
|
||||||
|
this.rindex = labels;
|
||||||
|
this.index = index;
|
||||||
|
this.__compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
__compile() {
|
||||||
|
const { minLabel, index, rindex } = this;
|
||||||
|
this.getOffset = function getOffset(l) {
|
||||||
|
return index[l - minLabel];
|
||||||
|
};
|
||||||
|
this.getLabel = function getLabel(i) {
|
||||||
|
return rindex[i];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return this.rindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.rindex.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
__promote(labelArray) {
|
||||||
|
/*
|
||||||
|
time/space decision - if we are going to use less than 10% of the
|
||||||
|
dense index space, switch to a KeyIndex (which is slower, but uses
|
||||||
|
less memory for sparse label spaces).
|
||||||
|
*/
|
||||||
|
const [minLabel, maxLabel] = extent(labelArray);
|
||||||
|
const labelSpaceSize = maxLabel - minLabel + 1;
|
||||||
|
const density = labelSpaceSize / this.rindex.length;
|
||||||
|
/* 0.1 is a magic number, that needs testing to optimize */
|
||||||
|
if (density < 0.1) {
|
||||||
|
return new KeyIndex(labelArray);
|
||||||
|
}
|
||||||
|
return new DenseInt32Index(labelArray, [minLabel, maxLabel]);
|
||||||
|
}
|
||||||
|
|
||||||
|
subsetLabels(labelArray) {
|
||||||
|
return this.__promote(labelArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
withLabel(label) {
|
||||||
|
return this.__promote([...this.keys(), label]);
|
||||||
|
}
|
||||||
|
|
||||||
|
dropLabel(label) {
|
||||||
|
const labelArray = [...this.keys()];
|
||||||
|
labelArray.splice(labelArray.indexOf(label), 1);
|
||||||
|
return this.__promote(labelArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable class-methods-use-this */
|
||||||
|
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
class KeyIndex {
|
||||||
|
/*
|
||||||
|
KeyIndex indexes arbitrary JS primitive types, and uses a Map()
|
||||||
|
as its core data structure.
|
||||||
|
*/
|
||||||
|
constructor(labels) {
|
||||||
|
const index = new Map();
|
||||||
|
if (labels === undefined) {
|
||||||
|
labels = [];
|
||||||
|
}
|
||||||
|
const rindex = labels;
|
||||||
|
labels.forEach((v, i) => {
|
||||||
|
index.set(v, i);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.index = index;
|
||||||
|
this.rindex = rindex;
|
||||||
|
this.__compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
__compile() {
|
||||||
|
const { index, rindex } = this;
|
||||||
|
this.getOffset = function getOffset(k) {
|
||||||
|
return index.get(k);
|
||||||
|
};
|
||||||
|
this.getLabel = function getLabel(i) {
|
||||||
|
return rindex[i];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return this.rindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.rindex.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
subsetLabels(labelArray) {
|
||||||
|
return new KeyIndex(labelArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
withLabel(label) {
|
||||||
|
return new KeyIndex([...this.rindex, label]);
|
||||||
|
}
|
||||||
|
|
||||||
|
dropLabel(label) {
|
||||||
|
const idx = this.rindex.indexOf(label);
|
||||||
|
const labelArray = [...this.rindex];
|
||||||
|
labelArray.splice(idx, 1);
|
||||||
|
return new KeyIndex(labelArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable class-methods-use-this */
|
||||||
|
|
||||||
|
function isLabelIndex(i) {
|
||||||
|
return (
|
||||||
|
i instanceof IdentityInt32Index ||
|
||||||
|
i instanceof DenseInt32Index ||
|
||||||
|
i instanceof KeyIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DenseInt32Index, IdentityInt32Index, KeyIndex, isLabelIndex };
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
Private dataframe support functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function summarizeContinuous(col) {
|
||||||
|
let min;
|
||||||
|
let max;
|
||||||
|
let nan = 0;
|
||||||
|
let pinf = 0;
|
||||||
|
let ninf = 0;
|
||||||
|
if (col) {
|
||||||
|
for (let r = 0, l = col.length; r < l; r += 1) {
|
||||||
|
const val = Number(col[r]);
|
||||||
|
if (Number.isFinite(val)) {
|
||||||
|
if (min === undefined) {
|
||||||
|
min = val;
|
||||||
|
max = val;
|
||||||
|
} else {
|
||||||
|
min = val < min ? val : min;
|
||||||
|
max = val > max ? val : max;
|
||||||
|
}
|
||||||
|
} else if (Number.isNaN(val)) {
|
||||||
|
nan += 1;
|
||||||
|
} else if (val > 0) {
|
||||||
|
pinf += 1;
|
||||||
|
} else {
|
||||||
|
ninf += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
categorical: false,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
nan,
|
||||||
|
pinf,
|
||||||
|
ninf
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeCategorical(col) {
|
||||||
|
const categoryCounts = new Map();
|
||||||
|
if (col) {
|
||||||
|
for (let r = 0, l = col.length; r < l; r += 1) {
|
||||||
|
const val = col[r];
|
||||||
|
let curCount = categoryCounts.get(val);
|
||||||
|
if (curCount === undefined) curCount = 0;
|
||||||
|
categoryCounts.set(val, curCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
categorical: true,
|
||||||
|
categories: [...categoryCounts.keys()],
|
||||||
|
categoryCounts,
|
||||||
|
numCategories: categoryCounts.size
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Private utility code for dataframe
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isTypedArray(x) {
|
||||||
|
return (
|
||||||
|
ArrayBuffer.isView(x) &&
|
||||||
|
Object.prototype.toString.call(x) !== "[object DataView]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArrayOrTypedArray(x) {
|
||||||
|
return Array.isArray(x) || isTypedArray(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function callOnceLazy(f) {
|
||||||
|
let value;
|
||||||
|
let calledOnce = false;
|
||||||
|
const result = function result(...args) {
|
||||||
|
if (!calledOnce) {
|
||||||
|
value = f(...args);
|
||||||
|
calledOnce = true;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
Helper functions for the controls reducer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
import * as globals from "../../globals";
|
||||||
|
import { fillRange } from "../typedCrossfilter/util";
|
||||||
|
import {
|
||||||
|
userDefinedDimensionName,
|
||||||
|
diffexpDimensionName
|
||||||
|
} from "../nameCreators";
|
||||||
|
import * as World from "./world";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Selection state for categoricals are tracked in an Object that
|
||||||
|
has two main components for each category:
|
||||||
|
1. mapping of option value to an index
|
||||||
|
2. array of bool selection state by index
|
||||||
|
Remember that option values can be ANY js type, except undefined/null.
|
||||||
|
|
||||||
|
{
|
||||||
|
_category_name_1: {
|
||||||
|
// map of option value to index
|
||||||
|
categoryIndices: Map([
|
||||||
|
catval1: index,
|
||||||
|
...
|
||||||
|
])
|
||||||
|
|
||||||
|
// index->selection true/false state
|
||||||
|
categorySelected: [ true/false, true/false, ... ]
|
||||||
|
|
||||||
|
// number of options
|
||||||
|
numCategories: number,
|
||||||
|
|
||||||
|
// isTruncated - true if the options for selection has
|
||||||
|
// been truncated (ie, was too large to implement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function topNCategories(summary) {
|
||||||
|
const counts = _.map(summary.categories, cat =>
|
||||||
|
summary.categoryCounts.get(cat)
|
||||||
|
);
|
||||||
|
const sortIndex = fillRange(new Array(summary.numCategories)).sort(
|
||||||
|
(a, b) => counts[b] - counts[a]
|
||||||
|
);
|
||||||
|
const sortedCategories = _.map(sortIndex, i => summary.categories[i]);
|
||||||
|
const sortedCounts = _.map(sortIndex, i => counts[i]);
|
||||||
|
const N = globals.maxCategoricalOptionsToDisplay;
|
||||||
|
|
||||||
|
if (sortedCategories.length < N) {
|
||||||
|
return [sortedCategories, sortedCounts];
|
||||||
|
}
|
||||||
|
return [sortedCategories.slice(0, N), sortedCounts.slice(0, N)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCategoricalSelectionState(state, world) {
|
||||||
|
const res = {};
|
||||||
|
_.forEach(world.obsAnnotations.colIndex.keys(), key => {
|
||||||
|
const summary = world.obsAnnotations.col(key).summarize();
|
||||||
|
if (summary.categories) {
|
||||||
|
const isColorField = key.includes("color") || key.includes("Color");
|
||||||
|
const isSelectableCategory =
|
||||||
|
!isColorField &&
|
||||||
|
key !== "name" &&
|
||||||
|
summary.categories.length < state.maxCategoryItems;
|
||||||
|
if (isSelectableCategory) {
|
||||||
|
const [categoryValues, categoryCounts] = topNCategories(summary);
|
||||||
|
const categoryIndices = new Map(categoryValues.map((v, i) => [v, i]));
|
||||||
|
const numCategories = categoryIndices.size;
|
||||||
|
const categorySelected = new Array(numCategories).fill(true);
|
||||||
|
const isTruncated = categoryValues.length < summary.numCategories;
|
||||||
|
res[key] = {
|
||||||
|
categoryValues, // array: of natively typed category values
|
||||||
|
categoryIndices, // map: category value (native type) -> category index
|
||||||
|
categorySelected, // array: t/f selection state
|
||||||
|
numCategories, // number: of categories
|
||||||
|
isTruncated, // bool: true if list was truncated
|
||||||
|
categoryCounts // array: cardinality of each category
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
given a categoricalSelectionState, return the list of all category values
|
||||||
|
where selection state is true (ie, they are selected).
|
||||||
|
*/
|
||||||
|
export function selectedValuesForCategory(categorySelectionState) {
|
||||||
|
const selectedValues = _([...categorySelectionState.categoryIndices])
|
||||||
|
.filter(tuple => categorySelectionState.categorySelected[tuple[1]])
|
||||||
|
.map(tuple => tuple[0])
|
||||||
|
.value();
|
||||||
|
return selectedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
build a crossfilter dimension map for all gene expression related dimensions.
|
||||||
|
*/
|
||||||
|
export function createGenesDimMap(
|
||||||
|
userDefinedGenes,
|
||||||
|
diffexpGenes,
|
||||||
|
world,
|
||||||
|
crossfilter
|
||||||
|
) {
|
||||||
|
function _createGenesDimMap(genes, nameCreator) {
|
||||||
|
return genes.reduce((acc, gene) => {
|
||||||
|
acc[nameCreator(gene)] = World.createVarDataDimension(
|
||||||
|
world,
|
||||||
|
crossfilter,
|
||||||
|
gene
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
..._createGenesDimMap(userDefinedGenes, userDefinedDimensionName),
|
||||||
|
..._createGenesDimMap(diffexpGenes, diffexpDimensionName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pruneVarDataCache(varData, needed) {
|
||||||
|
/*
|
||||||
|
Remove any unneeded columns from the varData dataframe. Will only
|
||||||
|
prune / remove if the total column count exceeds VarDataCacheLowWatermark
|
||||||
|
|
||||||
|
Note: this code leverages the fact that dataframe offsets indicate
|
||||||
|
the order in which the columns were added. This crudely provides
|
||||||
|
LRU semantics, so we can delete "older" columns first.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
VarDataCacheLowWatermark - this cofig value sets the minimum cache size,
|
||||||
|
in columns, below which we don't throw away data.
|
||||||
|
|
||||||
|
The value should be high enough so we are caching the maximum which will
|
||||||
|
"typically" be used in the UI (currently: 10 for diffexp, and N for user-
|
||||||
|
specified genes), and low enough to account for memory use (any single
|
||||||
|
column size is 4 bytes * numObs, so a column can be multi-megabyte in common
|
||||||
|
use cases).
|
||||||
|
*/
|
||||||
|
const VarDataCacheLowWatermark = 32;
|
||||||
|
|
||||||
|
const numOverWatermark = varData.dims[1] - VarDataCacheLowWatermark;
|
||||||
|
if (numOverWatermark <= 0) return varData;
|
||||||
|
|
||||||
|
const { colIndex } = varData;
|
||||||
|
const all = colIndex.keys();
|
||||||
|
const unused = _.difference(all, needed);
|
||||||
|
if (unused.length > 0) {
|
||||||
|
// sort by offset in the dataframe - ie, psuedo-LRU
|
||||||
|
unused.sort((a, b) => colIndex.getOffset(a) - colIndex.getOffset(b));
|
||||||
|
const numToDrop =
|
||||||
|
unused.length < numOverWatermark ? unused.length : numOverWatermark;
|
||||||
|
for (let i = 0; i < numToDrop; i += 1) {
|
||||||
|
varData = varData.dropCol(unused[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return varData;
|
||||||
|
}
|
||||||
@@ -16,5 +16,5 @@ exists to support those concepts.
|
|||||||
|
|
||||||
export * as Universe from "./universe";
|
export * as Universe from "./universe";
|
||||||
export * as World from "./world";
|
export * as World from "./world";
|
||||||
export * as kvCache from "./keyvalcache";
|
|
||||||
export * as WorldUtil from "./worldUtil";
|
export * as WorldUtil from "./worldUtil";
|
||||||
|
export * as ControlsHelper from "./controlsHelpers";
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
// jshint esversion: 6
|
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Very simple key/value cache for use by World & Universe. Cache keys must
|
|
||||||
be a string, and values are any JS non-primitive value.
|
|
||||||
|
|
||||||
* constructor(lowWatermark, minTTL):
|
|
||||||
- lowWatermark defines the number of cache elements below which
|
|
||||||
flushing will not occur.
|
|
||||||
- minTTL defines minimum time in milliseconds that cache entries will live.
|
|
||||||
A value of -1 disables automatic flushing (flush() can still
|
|
||||||
be called by external user).
|
|
||||||
* set() - add a key/val pair.
|
|
||||||
* get() - get a value or undefined if not present.
|
|
||||||
* flush(minAgeMs) - flush cache entries in excess of lowWatermark if those
|
|
||||||
entries are older than minAgeMs.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const cachePrivateKey = "__kvcachekey__";
|
|
||||||
const defaultLowWatermark = 32;
|
|
||||||
const defaultMinTTL = 1000;
|
|
||||||
|
|
||||||
function create(lowWatermark = defaultLowWatermark, minTTL = defaultMinTTL) {
|
|
||||||
if (typeof minTTL !== "number" || typeof lowWatermark !== "number") {
|
|
||||||
throw new TypeError(
|
|
||||||
"minTTL and lowWatermark parameters must be a primitive number"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (lowWatermark < 0 || minTTL < 0) {
|
|
||||||
throw new RangeError(
|
|
||||||
"minTTL and lowWatermark parameters must be number greater than zero"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
[cachePrivateKey]: {
|
|
||||||
lowWatermark,
|
|
||||||
minTTL
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(kvcache, key) {
|
|
||||||
if (key === cachePrivateKey) {
|
|
||||||
throw new RangeError(`key parameter may not have value ${cachePrivateKey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const val = kvcache[key];
|
|
||||||
if (val) {
|
|
||||||
val[cachePrivateKey] = Date.now();
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
function set(kvcache, key, val) {
|
|
||||||
if (key === cachePrivateKey) {
|
|
||||||
throw new RangeError(`key parameter may not have value ${cachePrivateKey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newKvCache = { ...kvcache };
|
|
||||||
newKvCache[key] = val;
|
|
||||||
val[cachePrivateKey] = Date.now();
|
|
||||||
flushInPlace(newKvCache);
|
|
||||||
return newKvCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flush(kvcache) {
|
|
||||||
const newKvCache = { ...kvcache };
|
|
||||||
flushInPlace(newKvCache);
|
|
||||||
return newKvCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Flush elements from cache IF cache size is greater than lowWatermark, and
|
|
||||||
those elements are older than minAgeMS
|
|
||||||
*/
|
|
||||||
function flushInPlace(kvCache) {
|
|
||||||
const { lowWatermark, minTTL } = kvCache[cachePrivateKey];
|
|
||||||
const eol = Date.now() - minTTL;
|
|
||||||
const allKeys = _(kvCache)
|
|
||||||
.keys()
|
|
||||||
.filter(k => k !== cachePrivateKey)
|
|
||||||
.sortBy([k => kvCache[k][cachePrivateKey]])
|
|
||||||
.value();
|
|
||||||
|
|
||||||
if (allKeys.length > lowWatermark) {
|
|
||||||
const keysToDelete = _(allKeys)
|
|
||||||
.slice(0, allKeys.length - lowWatermark)
|
|
||||||
.filter(k => kvCache[k][cachePrivateKey] <= eol)
|
|
||||||
.value();
|
|
||||||
_.forEach(keysToDelete, k => delete kvCache[k]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return kvCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
use to create a cache that is a transformation of another cache.
|
|
||||||
*/
|
|
||||||
function map(srcKvCache, cb, createOptions) {
|
|
||||||
const keysInSrcKvCache = _(srcKvCache)
|
|
||||||
.keys()
|
|
||||||
.filter(k => k !== cachePrivateKey)
|
|
||||||
.value();
|
|
||||||
const lowWatermark = _.get(
|
|
||||||
createOptions,
|
|
||||||
"lowWatermark",
|
|
||||||
defaultLowWatermark
|
|
||||||
);
|
|
||||||
const minTTL = _.get(createOptions, "minTTL", defaultMinTTL);
|
|
||||||
const newKvCache = create(lowWatermark, minTTL);
|
|
||||||
_.forEach(keysInSrcKvCache, key => {
|
|
||||||
const val = cb(get(srcKvCache, key), key);
|
|
||||||
newKvCache[key] = val;
|
|
||||||
val[cachePrivateKey] = Date.now();
|
|
||||||
});
|
|
||||||
return newKvCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { create, get, set, flush, map };
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import finiteExtent from "../finiteExtent";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Build and return obs/var summary using any annotation in the schema
|
|
||||||
|
|
||||||
Summary information for each annotation, keyed by annotation name.
|
|
||||||
Value will be an object, containing summary information.
|
|
||||||
|
|
||||||
For continuous annotations (int, float, etc):
|
|
||||||
<annotation_name>: {
|
|
||||||
categorical: false,
|
|
||||||
range {
|
|
||||||
min: <number>,
|
|
||||||
max: <number>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
For categorical annotations (boolean, string, category):
|
|
||||||
<annotation_name>: {
|
|
||||||
categorical: true,
|
|
||||||
categories: [ <category1>, <category2>, ... ]
|
|
||||||
categoryCounts: Map {
|
|
||||||
<category1>: <number>,
|
|
||||||
...
|
|
||||||
},
|
|
||||||
numCategories: <number>
|
|
||||||
}
|
|
||||||
|
|
||||||
Summarize will be returned for BOTH obs and var annotations.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
{
|
|
||||||
"Splice_sites_Annotated": {
|
|
||||||
categorical: false,
|
|
||||||
range: {
|
|
||||||
"min": 26,
|
|
||||||
"max": 1075869
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Selection": {
|
|
||||||
categorical: true,
|
|
||||||
numCategories, 3,
|
|
||||||
categories: [ "Astrocytes(HEPACAM)", "Endothelial(BSC)", "Unpanned" ],
|
|
||||||
categoryCounts: Map {
|
|
||||||
"Astrocytes(HEPACAM)": 714,
|
|
||||||
"Endothelial(BSC)": 123,
|
|
||||||
"Unpanned": 665
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NOTE: will not summarize the required 'name' annotation, as that is
|
|
||||||
specified as unique per element.
|
|
||||||
*/
|
|
||||||
function _summarizeAnnotations(_schema, annotations) {
|
|
||||||
const summary = _(_schema) // lodash wrapping: https://lodash.com/docs/4.17.11#lodash
|
|
||||||
.filter(v => v.name !== "name")
|
|
||||||
.keyBy("name")
|
|
||||||
.mapValues(anno => {
|
|
||||||
const { name, type } = anno;
|
|
||||||
const continuous = type === "int32" || type === "float32";
|
|
||||||
|
|
||||||
if (continuous) {
|
|
||||||
let min;
|
|
||||||
let max;
|
|
||||||
let nan = 0;
|
|
||||||
let pinf = 0;
|
|
||||||
let ninf = 0;
|
|
||||||
for (let r = 0; r < annotations.length; r += 1) {
|
|
||||||
const val = Number(annotations[r][name]);
|
|
||||||
if (Number.isFinite(val)) {
|
|
||||||
if (min === undefined) {
|
|
||||||
min = val;
|
|
||||||
max = val;
|
|
||||||
} else {
|
|
||||||
min = val < min ? val : min;
|
|
||||||
max = val > max ? val : max;
|
|
||||||
}
|
|
||||||
} else if (Number.isNaN(val)) {
|
|
||||||
nan += 1;
|
|
||||||
} else if (val > 0) {
|
|
||||||
pinf += 1;
|
|
||||||
} else {
|
|
||||||
ninf += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
categorical: false,
|
|
||||||
range: { min, max, nan, pinf, ninf }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* else categorical */
|
|
||||||
const categoryCounts = new Map();
|
|
||||||
for (let r = 0; r < annotations.length; r += 1) {
|
|
||||||
const val = annotations[r][name];
|
|
||||||
let curCount = categoryCounts.get(val);
|
|
||||||
if (curCount === undefined) curCount = 0;
|
|
||||||
categoryCounts.set(val, curCount + 1);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
categorical: true,
|
|
||||||
categories: [...categoryCounts.keys()],
|
|
||||||
categoryCounts,
|
|
||||||
numCategories: categoryCounts.size
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.value();
|
|
||||||
return summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function summarizeAnnotations(
|
|
||||||
schema,
|
|
||||||
obsAnnotations,
|
|
||||||
varAnnotations
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
obs: _summarizeAnnotations(schema.annotations.obs, obsAnnotations),
|
|
||||||
var: _summarizeAnnotations(schema.annotations.var, varAnnotations)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,24 +2,15 @@
|
|||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
import * as kvCache from "./keyvalcache";
|
|
||||||
import summarizeAnnotations from "./summarizeAnnotations";
|
|
||||||
import decodeMatrixFBS from "./matrix";
|
import decodeMatrixFBS from "./matrix";
|
||||||
|
import * as Dataframe from "../dataframe";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Private helper function - create and return a template Universe
|
Private helper function - create and return a template Universe
|
||||||
*/
|
*/
|
||||||
function templateUniverse() {
|
function templateUniverse() {
|
||||||
/* default universe template */
|
/* default universe template */
|
||||||
|
|
||||||
/* varDataCache config - see kvCache for semantics */
|
|
||||||
const VarDataCacheLowWatermark = 32; // cache element count
|
|
||||||
const VarDataCacheTTLMs = 1000; // min cache time in MS
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
api: null,
|
|
||||||
finalized: false, // XXX: may not be needed
|
|
||||||
|
|
||||||
nObs: 0,
|
nObs: 0,
|
||||||
nVar: 0,
|
nVar: 0,
|
||||||
schema: {},
|
schema: {},
|
||||||
@@ -27,21 +18,14 @@ function templateUniverse() {
|
|||||||
/*
|
/*
|
||||||
Annotations
|
Annotations
|
||||||
*/
|
*/
|
||||||
obsAnnotations: [] /* all obs annotations, by obs index */,
|
obsAnnotations: Dataframe.Dataframe.empty(),
|
||||||
varAnnotations: [] /* all var annotations, by var index */,
|
varAnnotations: Dataframe.Dataframe.empty(),
|
||||||
obsNameToIndexMap: {} /* reverse map 'name' to index */,
|
obsLayout: Dataframe.Dataframe.empty(),
|
||||||
varNameToIndexMap: {} /* reverse map 'name' to index */,
|
|
||||||
summary: null /* derived data summaries XXX: consider exploding in place */,
|
|
||||||
|
|
||||||
obsLayout: { X: [], Y: [] } /* xy layout */,
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Cache of var data (expression), by var annotation name. Data can be
|
Var data columns - subset of all
|
||||||
accesses as a POJO, but if you want caching semantics, use the kvCache
|
|
||||||
API (eg., kvCache.get(), kvCache.set(), ...), which will maintain the
|
|
||||||
LRU semantics.
|
|
||||||
*/
|
*/
|
||||||
varDataCache: kvCache.create(VarDataCacheLowWatermark, VarDataCacheTTLMs)
|
varData: Dataframe.Dataframe.empty(null, new Dataframe.KeyIndex())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,81 +37,29 @@ These functions are used exclusively by the actions and reducers to
|
|||||||
build an internal POJO for use by the rendering components.
|
build an internal POJO for use by the rendering components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
function AnnotationsFBSToDataframe(arrayBuffer) {
|
||||||
generate any client-side transformations or summarization that
|
|
||||||
is independent of REST API response formats.
|
|
||||||
*/
|
|
||||||
function finalize(universe) {
|
|
||||||
/* A bit of sanity checking! */
|
|
||||||
const { nObs, nVar } = universe;
|
|
||||||
if (
|
|
||||||
nObs !== universe.obsAnnotations.length ||
|
|
||||||
nObs !== universe.obsLayout.X.length ||
|
|
||||||
nObs !== universe.obsLayout.Y.length ||
|
|
||||||
nVar !== universe.varAnnotations.length
|
|
||||||
) {
|
|
||||||
throw new Error("Universe dimensionality mismatch - failed to load");
|
|
||||||
}
|
|
||||||
// TODO: add more sanity checks, such as:
|
|
||||||
// - all annotations in the schema
|
|
||||||
// - layout has supported number of dimensions
|
|
||||||
// - ...
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Create all derived (convenience) data structures.
|
Convert a Matrix FBS to a Dataframe.
|
||||||
*/
|
|
||||||
universe.obsNameToIndexMap = _.transform(
|
|
||||||
universe.obsAnnotations,
|
|
||||||
(acc, value, idx) => {
|
|
||||||
acc[value.name] = idx;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
universe.varNameToIndexMap = _.transform(
|
|
||||||
universe.varAnnotations,
|
|
||||||
(acc, value, idx) => {
|
|
||||||
acc[value.name] = idx;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
universe.finalized = true;
|
|
||||||
return universe;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RESTv02AnotationsFBSResponseToInternal(arrayBuffer) {
|
|
||||||
/*
|
|
||||||
Convert a Matrix FBS to our internal format -- row-major array of
|
|
||||||
observations/cells, stored as an object. Each obs has a key for each
|
|
||||||
annotation, plus __index__ containing its obsIndex.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
[
|
|
||||||
{ __index__: 0, tissue_type: "lung", sex: "F", ... },
|
|
||||||
...
|
|
||||||
]
|
|
||||||
|
|
||||||
XXX TODO: we could make use of the columns in building crossfilter
|
|
||||||
dimensions (they have to be recreated). Future optimization.
|
|
||||||
*/
|
*/
|
||||||
const fbs = decodeMatrixFBS(arrayBuffer);
|
const fbs = decodeMatrixFBS(arrayBuffer);
|
||||||
const keys = fbs.colIdx;
|
const df = new Dataframe.Dataframe(
|
||||||
const result = Array(fbs.nRows);
|
[fbs.nRows, fbs.nCols],
|
||||||
for (let row = 0; row < fbs.nRows; row += 1) {
|
fbs.columns,
|
||||||
const rec = { __index__: row };
|
null,
|
||||||
for (let col = 0; col < fbs.nCols; col += 1) {
|
new Dataframe.KeyIndex(fbs.colIdx)
|
||||||
rec[keys[col]] = fbs.columns[col][row];
|
);
|
||||||
}
|
return df;
|
||||||
result[row] = rec;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RESTv02LayoutFBSResponseToInternal(arrayBuffer) {
|
function LayoutFBSToDataframe(arrayBuffer) {
|
||||||
const fbs = decodeMatrixFBS(arrayBuffer, true);
|
const fbs = decodeMatrixFBS(arrayBuffer, true);
|
||||||
return {
|
const df = new Dataframe.Dataframe(
|
||||||
X: fbs.columns[0],
|
[fbs.nRows, fbs.nCols],
|
||||||
Y: fbs.columns[1]
|
fbs.columns,
|
||||||
};
|
null,
|
||||||
|
new Dataframe.KeyIndex(["X", "Y"])
|
||||||
|
);
|
||||||
|
return df;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reconcileSchemaCategoriesWithSummary(universe) {
|
function reconcileSchemaCategoriesWithSummary(universe) {
|
||||||
@@ -149,14 +81,14 @@ function reconcileSchemaCategoriesWithSummary(universe) {
|
|||||||
) {
|
) {
|
||||||
const categories = _.union(
|
const categories = _.union(
|
||||||
_.get(s, "categories", []),
|
_.get(s, "categories", []),
|
||||||
_.get(universe.summary.obs[s.name], "categories", [])
|
_.get(universe.obsAnnotations.col(s.name).summarize(), "categories", [])
|
||||||
);
|
);
|
||||||
s.categories = categories;
|
s.categories = categories;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUniverseFromRestV02Response(
|
export function createUniverseFromResponse(
|
||||||
configResponse,
|
configResponse,
|
||||||
schemaResponse,
|
schemaResponse,
|
||||||
annotationsObsResponse,
|
annotationsObsResponse,
|
||||||
@@ -169,33 +101,28 @@ export function createUniverseFromRestV02Response(
|
|||||||
const { schema } = schemaResponse;
|
const { schema } = schemaResponse;
|
||||||
const universe = templateUniverse();
|
const universe = templateUniverse();
|
||||||
|
|
||||||
/* constants */
|
|
||||||
universe.api = "0.2";
|
|
||||||
|
|
||||||
/* schema related */
|
/* schema related */
|
||||||
universe.schema = schema;
|
universe.schema = schema;
|
||||||
universe.nObs = schema.dataframe.nObs;
|
universe.nObs = schema.dataframe.nObs;
|
||||||
universe.nVar = schema.dataframe.nVar;
|
universe.nVar = schema.dataframe.nVar;
|
||||||
|
|
||||||
/* annotations */
|
/* annotations */
|
||||||
universe.obsAnnotations = RESTv02AnotationsFBSResponseToInternal(
|
universe.obsAnnotations = AnnotationsFBSToDataframe(annotationsObsResponse);
|
||||||
annotationsObsResponse
|
universe.varAnnotations = AnnotationsFBSToDataframe(annotationsVarResponse);
|
||||||
);
|
|
||||||
universe.varAnnotations = RESTv02AnotationsFBSResponseToInternal(
|
|
||||||
annotationsVarResponse
|
|
||||||
);
|
|
||||||
|
|
||||||
/* layout */
|
/* layout */
|
||||||
universe.obsLayout = RESTv02LayoutFBSResponseToInternal(layoutFBSResponse);
|
universe.obsLayout = LayoutFBSToDataframe(layoutFBSResponse);
|
||||||
|
|
||||||
universe.summary = summarizeAnnotations(
|
/* sanity check */
|
||||||
universe.schema,
|
if (
|
||||||
universe.obsAnnotations,
|
universe.nObs !== universe.obsLayout.length ||
|
||||||
universe.varAnnotations
|
universe.nObs !== universe.obsAnnotations.length ||
|
||||||
);
|
universe.nVar !== universe.varAnnotations.length
|
||||||
|
) {
|
||||||
|
throw new Error("Universe dimensionality mismatch - failed to load");
|
||||||
|
}
|
||||||
|
|
||||||
reconcileSchemaCategoriesWithSummary(universe);
|
reconcileSchemaCategoriesWithSummary(universe);
|
||||||
return finalize(universe);
|
return universe;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertDataFBStoObject(universe, arrayBuffer) {
|
export function convertDataFBStoObject(universe, arrayBuffer) {
|
||||||
@@ -214,8 +141,8 @@ export function convertDataFBStoObject(universe, arrayBuffer) {
|
|||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
for (let c = 0; c < colIdx.length; c += 1) {
|
for (let c = 0; c < colIdx.length; c += 1) {
|
||||||
const gene = universe.varAnnotations[colIdx[c]].name;
|
const varName = universe.varAnnotations.at(colIdx[c], "name");
|
||||||
result[gene] = columns[c];
|
result[varName] = columns[c];
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import * as kvCache from "./keyvalcache";
|
|
||||||
import summarizeAnnotations from "./summarizeAnnotations";
|
|
||||||
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
|
import { layoutDimensionName, obsAnnoDimensionName } from "../nameCreators";
|
||||||
import Crossfilter from "../typedCrossfilter";
|
import Crossfilter from "../typedCrossfilter";
|
||||||
import { sliceByIndex } from "../typedCrossfilter/util";
|
import * as Dataframe from "../dataframe";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
World is a subset of universe. Most code should use world, and should
|
World is a subset of universe. Most code should use world, and should
|
||||||
(generally) not use Universe. World contains any per-obs or per-var data
|
(generally) not use Universe. World contains any per-obs or per-var data
|
||||||
that must be consistent acorss the app when we view/manipulate subsets
|
that must be consistent acorss the app when we view/manipulate subsets
|
||||||
@@ -16,120 +15,76 @@ of Universe.
|
|||||||
Private API indicated by leading underscore in key name (eg, _foo). Anything else
|
Private API indicated by leading underscore in key name (eg, _foo). Anything else
|
||||||
is public.
|
is public.
|
||||||
|
|
||||||
World contains several public keys, obsAnnotations, and obsLayout, which are
|
Notable keys in the world object:
|
||||||
arrays contianing information about an OBS in the same order/offset. In
|
|
||||||
other words, world.obsAnnotations[0] and world.obsLayout.X[0] refer to the same
|
* nObs, nVar: dimensions
|
||||||
obs/cell.
|
|
||||||
|
* schema: data schema from the server
|
||||||
|
|
||||||
* obsAnnotations:
|
* obsAnnotations:
|
||||||
|
|
||||||
obsAnnotations will return an array of objects. Each object contains all annotation
|
Dataframe containing obs annotations. Columns are indexed by annotation
|
||||||
values for a given observation/cell, keyed by annotation name, PLUS a key
|
name (eg, 'tissue type'), and rows are indexed by the REST API obsIndex
|
||||||
'__cellId__', containing a REST API ID for this obs/cell (referred to as the
|
(ie, the offset into the underlying server-side dataframe).
|
||||||
obsIndex in the REST 0.2 spec or cellIndex in the 0.1 spec.
|
|
||||||
|
|
||||||
Example: [ { __cellId__: 99, cluster: 'blue', numReads: 93933 } ]
|
This indexing means that you can access data by _either_ the server's
|
||||||
|
obxIndex, or the offset into the client-side column array . Be careful
|
||||||
NOTE: world.obsAnnotation should be identical to the old state.cells value,
|
to know which you want and are using.
|
||||||
EXCEPT that
|
|
||||||
* __cellIndex__ renamed to __index__
|
|
||||||
* __x__ and __y__ are now in world.obsLayout
|
|
||||||
* __color__ and __colorRBG__ should be moved to controls reducer
|
|
||||||
|
|
||||||
* obsLayout:
|
* obsLayout:
|
||||||
|
|
||||||
obsLayout will return an object containing two arrays, containing X and Y
|
A dataframe containing the X/Y layout for all obs. Columns are named
|
||||||
coordinates respectively.
|
'X' and 'Y', and rows are indexed in the same way as obsAnnotation.
|
||||||
|
|
||||||
Example: { X: [ 0.33, 0.23, ... ], Y: [ 0.8, 0.777, ... ]}
|
* varData: a cache of expression columns, stored in a Dataframe. Cache
|
||||||
|
managed by controls reducer.
|
||||||
* crossfilter - a crossfilter object across world.obsAnnotations
|
|
||||||
|
|
||||||
* dimensionMap - an object mapping annotation names to dimensions on
|
|
||||||
the crossfilter
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* varDataCache config - see kvCache for semantics */
|
|
||||||
const VarDataCacheLowWatermark = 32; // cache element count
|
|
||||||
const VarDataCacheTTLMs = 1000; // min cache time in MS
|
|
||||||
|
|
||||||
function templateWorld() {
|
function templateWorld() {
|
||||||
return {
|
return {
|
||||||
// map from universe obsIndex to world offset.
|
|
||||||
// Undefined / null indicates identity mapping.
|
|
||||||
obsIndex: null,
|
|
||||||
obsBackIndex: null,
|
|
||||||
|
|
||||||
/* schema/version related */
|
/* schema/version related */
|
||||||
api: null,
|
|
||||||
schema: null,
|
schema: null,
|
||||||
nObs: 0,
|
nObs: 0,
|
||||||
nVar: 0,
|
nVar: 0,
|
||||||
|
|
||||||
/* annotations */
|
/* annotations */
|
||||||
obsAnnotations: null,
|
obsAnnotations: Dataframe.Dataframe.empty(),
|
||||||
varAnnotations: null,
|
varAnnotations: Dataframe.Dataframe.empty(),
|
||||||
|
|
||||||
/* layout of graph */
|
/* layout of graph. Dataframe. */
|
||||||
obsLayout: null,
|
obsLayout: Dataframe.Dataframe.empty(),
|
||||||
|
|
||||||
/* derived data summaries XXX: consider exploding in place */
|
/*
|
||||||
summary: null,
|
Var data columns - subset of all data (may be empty)
|
||||||
|
*/
|
||||||
varDataCache: kvCache.create(
|
varData: Dataframe.Dataframe.empty(null, new Dataframe.KeyIndex())
|
||||||
VarDataCacheLowWatermark,
|
|
||||||
VarDataCacheTTLMs
|
|
||||||
) /* cache of var data (expression) */
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createWorldFromEntireUniverse(universe) {
|
export function createWorldFromEntireUniverse(universe) {
|
||||||
if (!universe.finalized) {
|
|
||||||
throw new Error("World can't be created from an partial Universe");
|
|
||||||
}
|
|
||||||
|
|
||||||
const world = templateWorld();
|
const world = templateWorld();
|
||||||
|
|
||||||
// map from the universe obsIndex to our world offset.
|
|
||||||
// undefined/null indicates identity map.
|
|
||||||
// In other words obsBackIndex[universeIdx] -> worldIdx
|
|
||||||
world.obsBackIndex = null;
|
|
||||||
// Map to the universe index for each element in world.
|
|
||||||
// Null indicates identity map (aka world === universe)
|
|
||||||
// In other wrods obsIndex[worldIdx] -> universeIdx
|
|
||||||
world.obsIndex = null;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
public interface follows
|
public interface follows
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Schema related */
|
/* Schema related */
|
||||||
world.api = universe.api;
|
|
||||||
world.schema = universe.schema;
|
world.schema = universe.schema;
|
||||||
world.nObs = universe.nObs;
|
world.nObs = universe.nObs;
|
||||||
world.nVar = universe.nVar;
|
world.nVar = universe.nVar;
|
||||||
|
|
||||||
/* annotations */
|
/* annotation dataframes */
|
||||||
world.obsAnnotations = universe.obsAnnotations;
|
world.obsAnnotations = universe.obsAnnotations;
|
||||||
world.varAnnotations = universe.varAnnotations;
|
world.varAnnotations = universe.varAnnotations;
|
||||||
|
|
||||||
/* layout and display characteristics */
|
/* layout and display characteristics dataframe */
|
||||||
world.obsLayout = universe.obsLayout;
|
world.obsLayout = universe.obsLayout;
|
||||||
|
|
||||||
/* derived data & summaries */
|
/*
|
||||||
world.summary = summarizeAnnotations(
|
Var data columns - subset of all
|
||||||
world.schema,
|
*/
|
||||||
world.obsAnnotations,
|
world.varData = universe.varData.clone();
|
||||||
world.varAnnotations
|
|
||||||
);
|
|
||||||
|
|
||||||
/* build the varDataCache */
|
|
||||||
world.varDataCache = kvCache.map(
|
|
||||||
universe.varDataCache,
|
|
||||||
val => subsetVarData(world, universe, val),
|
|
||||||
{ lowWatermark: VarDataCacheLowWatermark, minTTL: VarDataCacheTTLMs }
|
|
||||||
);
|
|
||||||
|
|
||||||
return world;
|
return world;
|
||||||
}
|
}
|
||||||
@@ -138,54 +93,24 @@ export function createWorldFromCurrentSelection(universe, world, crossfilter) {
|
|||||||
const newWorld = templateWorld();
|
const newWorld = templateWorld();
|
||||||
|
|
||||||
/* these don't change as only OBS are selected in our current implementation */
|
/* these don't change as only OBS are selected in our current implementation */
|
||||||
newWorld.api = universe.api;
|
|
||||||
newWorld.nVar = universe.nVar;
|
newWorld.nVar = universe.nVar;
|
||||||
newWorld.schema = universe.schema;
|
newWorld.schema = universe.schema;
|
||||||
newWorld.varAnnotations = universe.varAnnotations;
|
newWorld.varAnnotations = universe.varAnnotations;
|
||||||
|
|
||||||
/* build index maps and back maps based upon current selection state */
|
/* now subset/cut obs */
|
||||||
const obsBackIndex = new Uint32Array(universe.nObs);
|
const mask = crossfilter.allFilteredMask();
|
||||||
obsBackIndex.fill(-1); // default - aka unused
|
newWorld.obsAnnotations = world.obsAnnotations.isubsetMask(mask);
|
||||||
const notSelected = obsBackIndex[0];
|
newWorld.obsLayout = world.obsLayout.isubsetMask(mask);
|
||||||
let nObs = 0;
|
newWorld.nObs = newWorld.obsAnnotations.dims[0];
|
||||||
for (let i = 0; i < universe.nObs; i += 1) {
|
|
||||||
if (crossfilter.isElementFiltered(i)) {
|
/*
|
||||||
obsBackIndex[i] = nObs;
|
Var data columns - subset of all
|
||||||
nObs += 1;
|
*/
|
||||||
}
|
if (world.varData.isEmpty()) {
|
||||||
|
newWorld.varData = world.varData.clone();
|
||||||
|
} else {
|
||||||
|
newWorld.varData = world.varData.isubsetMask(mask);
|
||||||
}
|
}
|
||||||
const obsIndex = new Uint32Array(nObs);
|
|
||||||
for (let i = 0; i < universe.nObs; i += 1) {
|
|
||||||
const worldIdx = obsBackIndex[i];
|
|
||||||
if (worldIdx !== notSelected) {
|
|
||||||
obsIndex[worldIdx] = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newWorld.nObs = nObs;
|
|
||||||
newWorld.obsIndex = obsIndex;
|
|
||||||
newWorld.obsBackIndex = obsBackIndex;
|
|
||||||
|
|
||||||
/* now slice */
|
|
||||||
newWorld.obsAnnotations = sliceByIndex(universe.obsAnnotations, obsIndex);
|
|
||||||
newWorld.obsLayout = {
|
|
||||||
X: sliceByIndex(universe.obsLayout.X, obsIndex),
|
|
||||||
Y: sliceByIndex(universe.obsLayout.Y, obsIndex)
|
|
||||||
};
|
|
||||||
|
|
||||||
/* derived data & summaries */
|
|
||||||
newWorld.summary = summarizeAnnotations(
|
|
||||||
newWorld.schema,
|
|
||||||
newWorld.obsAnnotations,
|
|
||||||
newWorld.varAnnotations
|
|
||||||
);
|
|
||||||
|
|
||||||
/* build the varDataCache */
|
|
||||||
newWorld.varDataCache = kvCache.map(
|
|
||||||
universe.varDataCache,
|
|
||||||
val => subsetVarData(newWorld, universe, val),
|
|
||||||
{ lowWatermark: VarDataCacheLowWatermark, minTTL: VarDataCacheTTLMs }
|
|
||||||
);
|
|
||||||
return newWorld;
|
return newWorld;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,17 +150,10 @@ function deduceDimensionType(attributes, fieldName) {
|
|||||||
when it is no longer needed
|
when it is no longer needed
|
||||||
(it will not be garbage collected without this call)
|
(it will not be garbage collected without this call)
|
||||||
*/
|
*/
|
||||||
|
export function createVarDataDimension(world, crossfilter, name) {
|
||||||
export function createVarDimension(
|
|
||||||
world,
|
|
||||||
_worldVarDataCache,
|
|
||||||
crossfilter,
|
|
||||||
geneName
|
|
||||||
) {
|
|
||||||
// return crossfilter.dimension(_worldVarDataCache[geneName], Float32Array);
|
|
||||||
return crossfilter.dimension(
|
return crossfilter.dimension(
|
||||||
Crossfilter.ScalarDimension,
|
Crossfilter.ScalarDimension,
|
||||||
_worldVarDataCache[geneName],
|
world.varData.col(name).asArray(),
|
||||||
Float32Array
|
Float32Array
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -245,22 +163,23 @@ export function createObsDimensionMap(crossfilter, world) {
|
|||||||
create and return a crossfilter dimension for every obs annotation
|
create and return a crossfilter dimension for every obs annotation
|
||||||
for which we have a supported type.
|
for which we have a supported type.
|
||||||
*/
|
*/
|
||||||
const { schema, obsLayout } = world;
|
const { schema, obsLayout, obsAnnotations } = world;
|
||||||
|
|
||||||
// Create a crossfilter dimension for all obs annotations *except* 'name'
|
// Create a crossfilter dimension for all obs annotations *except* 'name'
|
||||||
const dimensionMap = _(schema.annotations.obs)
|
const dimensionMap = _(schema.annotations.obs)
|
||||||
.filter(anno => anno.name !== "name")
|
.filter(anno => anno.name !== "name")
|
||||||
.transform((result, anno) => {
|
.transform((result, anno) => {
|
||||||
const dimType = deduceDimensionType(anno, anno.name);
|
const dimType = deduceDimensionType(anno, anno.name);
|
||||||
|
const colData = obsAnnotations.col(anno.name).asArray();
|
||||||
if (dimType === "enum") {
|
if (dimType === "enum") {
|
||||||
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
||||||
Crossfilter.EnumDimension,
|
Crossfilter.EnumDimension,
|
||||||
r => r[anno.name]
|
colData
|
||||||
);
|
);
|
||||||
} else {
|
} else if (dimType) {
|
||||||
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
result[obsAnnoDimensionName(anno.name)] = crossfilter.dimension(
|
||||||
Crossfilter.ScalarDimension,
|
Crossfilter.ScalarDimension,
|
||||||
r => r[anno.name],
|
colData,
|
||||||
dimType
|
dimType
|
||||||
);
|
);
|
||||||
} // else ignore the annotation
|
} // else ignore the annotation
|
||||||
@@ -272,8 +191,8 @@ export function createObsDimensionMap(crossfilter, world) {
|
|||||||
*/
|
*/
|
||||||
dimensionMap[layoutDimensionName("XY")] = crossfilter.dimension(
|
dimensionMap[layoutDimensionName("XY")] = crossfilter.dimension(
|
||||||
Crossfilter.SpatialDimension,
|
Crossfilter.SpatialDimension,
|
||||||
obsLayout.X,
|
obsLayout.col("X").asArray(),
|
||||||
obsLayout.Y
|
obsLayout.col("Y").asArray()
|
||||||
);
|
);
|
||||||
|
|
||||||
return dimensionMap;
|
return dimensionMap;
|
||||||
@@ -283,10 +202,20 @@ export function worldEqUniverse(world, universe) {
|
|||||||
return world.obsAnnotations === universe.obsAnnotations;
|
return world.obsAnnotations === universe.obsAnnotations;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subsetVarData(world, universe, varData) {
|
export function getSelectedByIndex(crossfilter) {
|
||||||
// If world === universe, just return the entire varData array
|
/*
|
||||||
if (worldEqUniverse(world, universe)) {
|
return array of obsIndex, containing all selected obs/cells.
|
||||||
return varData;
|
*/
|
||||||
|
const selected = crossfilter.allFilteredMask(); // array of bool-ish
|
||||||
|
const keys = crossfilter.data.rowIndex.keys(); // row keys, aka universe rowIndex
|
||||||
|
|
||||||
|
const set = new Int32Array(selected.length);
|
||||||
|
let numElems = 0;
|
||||||
|
for (let i = 0, l = selected.length; i < l; i += 1) {
|
||||||
|
if (selected[i]) {
|
||||||
|
set[numElems] = keys[i];
|
||||||
|
numElems += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sliceByIndex(varData, world.obsIndex);
|
return new Int32Array(set.buffer, 0, numElems);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,23 @@ Map {
|
|||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Parameters are:
|
||||||
|
- dim1: dimension 1 name/label
|
||||||
|
- dim2: dimension 2 name/label
|
||||||
|
- df: dataframe containing dim1 and dim2 on the column axis
|
||||||
|
|
||||||
*/
|
*/
|
||||||
function _countCategoryValues2D(dim1, dim2, rows) {
|
function _countCategoryValues2D(dim1, dim2, df) {
|
||||||
const dimMap = new Map();
|
const dimMap = new Map();
|
||||||
for (let r = 0; r < rows.length; r += 1) {
|
const col1 = df.col(dim1) ? df.col(dim1).asArray() : null;
|
||||||
const row = rows[r];
|
const col2 = df.col(dim2) ? df.col(dim2).asArray() : null;
|
||||||
const val1 = row[dim1];
|
if (!col1 || !col2) {
|
||||||
const val2 = row[dim2];
|
return dimMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let r = 0, l = df.length; r < l; r += 1) {
|
||||||
|
const val1 = col1[r];
|
||||||
|
const val2 = col2[r];
|
||||||
let d2Map = dimMap.get(val1);
|
let d2Map = dimMap.get(val1);
|
||||||
if (d2Map === undefined) {
|
if (d2Map === undefined) {
|
||||||
d2Map = new Map();
|
d2Map = new Map();
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BitArray {
|
|||||||
// Return the number of records that are selected, ie, have a one bit in
|
// Return the number of records that are selected, ie, have a one bit in
|
||||||
// all allocated dimensions.
|
// all allocated dimensions.
|
||||||
//
|
//
|
||||||
get selectionCount() {
|
selectionCount() {
|
||||||
return this.countAllOnes();
|
return this.countAllOnes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,16 +48,27 @@ class BitArray {
|
|||||||
//
|
//
|
||||||
countAllOnes() {
|
countAllOnes() {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const { bitarray, bitmask, length, width } = this;
|
const { bitarray, length, width } = this;
|
||||||
for (let l = 0; l < length; l += 1) {
|
if (width === 1) {
|
||||||
let dimensionsSet = 0;
|
// special case, width === 1, for performance
|
||||||
for (let w = 0; w < width; w += 1) {
|
const bitmask = this.bitmask[0];
|
||||||
if (bitarray[w * length + l] === bitmask[w]) {
|
for (let l = 0; l < length; l += 1) {
|
||||||
dimensionsSet += 1;
|
if (bitarray[l] === bitmask) {
|
||||||
|
count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dimensionsSet === width) {
|
} else {
|
||||||
count += 1;
|
const { bitmask } = this;
|
||||||
|
for (let l = 0; l < length; l += 1) {
|
||||||
|
let dimensionsSet = 0;
|
||||||
|
for (let w = 0; w < width; w += 1) {
|
||||||
|
if (bitarray[w * length + l] === bitmask[w]) {
|
||||||
|
dimensionsSet += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dimensionsSet === width) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
@@ -233,12 +244,14 @@ class BitArray {
|
|||||||
fillBySelection(result, selectedValue, deselectedValue) {
|
fillBySelection(result, selectedValue, deselectedValue) {
|
||||||
// special case (width === 1) for performance
|
// special case (width === 1) for performance
|
||||||
if (this.width === 1) {
|
if (this.width === 1) {
|
||||||
const bitmask = this.bitmask[0];
|
const { bitmask, bitarray } = this;
|
||||||
for (let i = 0, len = this.length; i < len; i += 1) {
|
const mask = bitmask[0];
|
||||||
result[i] =
|
if (!mask) {
|
||||||
bitmask && this.bitarray[i] === bitmask
|
result.fill(deselectedValue);
|
||||||
? selectedValue
|
} else {
|
||||||
: deselectedValue;
|
for (let i = 0, len = this.length; i < len; i += 1) {
|
||||||
|
result[i] = bitarray[i] === mask ? selectedValue : deselectedValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0, len = this.length; i < len; i += 1) {
|
for (let i = 0, len = this.length; i < len; i += 1) {
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ import {
|
|||||||
upperBoundIndirect
|
upperBoundIndirect
|
||||||
} from "./util";
|
} from "./util";
|
||||||
|
|
||||||
|
function isArrayOrTypedArray(x) {
|
||||||
|
return (
|
||||||
|
Array.isArray(x) ||
|
||||||
|
(ArrayBuffer.isView(x) &&
|
||||||
|
Object.prototype.toString.call(x) !== "[object DataView]")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class NotImplementedError extends Error {
|
class NotImplementedError extends Error {
|
||||||
constructor(...params) {
|
constructor(...params) {
|
||||||
super(...params);
|
super(...params);
|
||||||
@@ -52,6 +60,11 @@ class NotImplementedError extends Error {
|
|||||||
|
|
||||||
class TypedCrossfilter {
|
class TypedCrossfilter {
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
|
/*
|
||||||
|
Typically, data is one of:
|
||||||
|
- Array of objects/records
|
||||||
|
- Dataframe (util/dataframe)
|
||||||
|
*/
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
|
||||||
// filters: array of { id, dimension }
|
// filters: array of { id, dimension }
|
||||||
@@ -97,18 +110,32 @@ class TypedCrossfilter {
|
|||||||
// return array of all records that are selected/filtered
|
// return array of all records that are selected/filtered
|
||||||
// by all dimensions.
|
// by all dimensions.
|
||||||
allFiltered() {
|
allFiltered() {
|
||||||
const { selection } = this;
|
const { data, selection } = this;
|
||||||
const res = [];
|
if (Array.isArray(data)) {
|
||||||
for (let i = 0, len = this.data.length; i < len; i += 1) {
|
const res = [];
|
||||||
if (selection.isSelected(i)) {
|
for (let i = 0, len = data.length; i < len; i += 1) {
|
||||||
res.push(this.data[i]);
|
if (selection.isSelected(i)) {
|
||||||
|
res.push(data[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
return res;
|
/* else, Dataframe-like */
|
||||||
|
return data.isubsetMask(this.allFilteredMask());
|
||||||
|
}
|
||||||
|
|
||||||
|
// return Uint8array containing selection state (truthy/falsey) for each record.
|
||||||
|
//
|
||||||
|
allFilteredMask() {
|
||||||
|
return this.selection.fillBySelection(
|
||||||
|
new Uint8Array(this.data.length),
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
countFiltered() {
|
countFiltered() {
|
||||||
return this.selection.selectionCount;
|
return this.selection.selectionCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
isElementFiltered(i) {
|
isElementFiltered(i) {
|
||||||
@@ -161,6 +188,7 @@ class ScalarDimension extends _Dimension {
|
|||||||
// or a map function which will create it.
|
// or a map function which will create it.
|
||||||
let array;
|
let array;
|
||||||
if (value instanceof ValueArrayType) {
|
if (value instanceof ValueArrayType) {
|
||||||
|
// user has provided the final typed array - just use it
|
||||||
if (value.length !== this.crossfilter.data.length) {
|
if (value.length !== this.crossfilter.data.length) {
|
||||||
throw new RangeError(
|
throw new RangeError(
|
||||||
"ScalarDimension values length must equal crossfilter data record count"
|
"ScalarDimension values length must equal crossfilter data record count"
|
||||||
@@ -168,11 +196,18 @@ class ScalarDimension extends _Dimension {
|
|||||||
}
|
}
|
||||||
array = value;
|
array = value;
|
||||||
} else if (value instanceof Function) {
|
} else if (value instanceof Function) {
|
||||||
// Create value array
|
// Create value array from user-provided map function.
|
||||||
array = this._createValueArray(
|
array = this._createValueArray(
|
||||||
value,
|
value,
|
||||||
new ValueArrayType(this.crossfilter.data.length)
|
new ValueArrayType(this.crossfilter.data.length)
|
||||||
);
|
);
|
||||||
|
} else if (isArrayOrTypedArray(value)) {
|
||||||
|
// Create value array from user-provided array. Typically used
|
||||||
|
// only by enumerated dimensions
|
||||||
|
array = this._createValueArray(
|
||||||
|
i => value[i],
|
||||||
|
new ValueArrayType(this.crossfilter.data.length)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new NotImplementedError(
|
throw new NotImplementedError(
|
||||||
"dimension value must be function or value array type"
|
"dimension value must be function or value array type"
|
||||||
@@ -190,7 +225,7 @@ class ScalarDimension extends _Dimension {
|
|||||||
const len = data.length;
|
const len = data.length;
|
||||||
const larray = array;
|
const larray = array;
|
||||||
for (let i = 0; i < len; i += 1) {
|
for (let i = 0; i < len; i += 1) {
|
||||||
larray[i] = value(data[i]);
|
larray[i] = value(i, data);
|
||||||
}
|
}
|
||||||
return larray;
|
return larray;
|
||||||
}
|
}
|
||||||
@@ -387,7 +422,7 @@ class EnumDimension extends ScalarDimension {
|
|||||||
// and the enum.
|
// and the enum.
|
||||||
const s = new Set();
|
const s = new Set();
|
||||||
for (let i = 0; i < len; i += 1) {
|
for (let i = 0; i < len; i += 1) {
|
||||||
s.add(value(data[i]));
|
s.add(value(i, data));
|
||||||
}
|
}
|
||||||
this.enumIndex = Array.from(s);
|
this.enumIndex = Array.from(s);
|
||||||
this.enumIndex.sort();
|
this.enumIndex.sort();
|
||||||
@@ -395,7 +430,7 @@ class EnumDimension extends ScalarDimension {
|
|||||||
// create dimension value array
|
// create dimension value array
|
||||||
const enumLen = this.enumIndex.length;
|
const enumLen = this.enumIndex.length;
|
||||||
for (let i = 0; i < len; i += 1) {
|
for (let i = 0; i < len; i += 1) {
|
||||||
const v = value(data[i]);
|
const v = value(i, data);
|
||||||
const e = lowerBound(this.enumIndex, v, 0, enumLen);
|
const e = lowerBound(this.enumIndex, v, 0, enumLen);
|
||||||
larray[i] = e;
|
larray[i] = e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ build-server : build-client
|
|||||||
cp -r server/* $(SERVERBUILD)
|
cp -r server/* $(SERVERBUILD)
|
||||||
cp -r client/build/ $(CLIENTBUILD)
|
cp -r client/build/ $(CLIENTBUILD)
|
||||||
mkdir -p $(SERVERBUILD)/app/web/static/img
|
mkdir -p $(SERVERBUILD)/app/web/static/img
|
||||||
|
mkdir -p $(SERVERBUILD)/app/web/templates/
|
||||||
cp $(CLIENTBUILD)/index.html $(SERVERBUILD)/app/web/templates/
|
cp $(CLIENTBUILD)/index.html $(SERVERBUILD)/app/web/templates/
|
||||||
cp -r $(CLIENTBUILD)/static $(SERVERBUILD)/app/web/
|
cp -r $(CLIENTBUILD)/static $(SERVERBUILD)/app/web/
|
||||||
cp $(CLIENTBUILD)/favicon.png $(SERVERBUILD)/app/web/static/img
|
cp $(CLIENTBUILD)/favicon.png $(SERVERBUILD)/app/web/static/img
|
||||||
@@ -28,6 +29,8 @@ build-client :
|
|||||||
# If you are actively developing in the server folder use this, dirties the source tree
|
# If you are actively developing in the server folder use this, dirties the source tree
|
||||||
build-for-server-dev : clean-server build-client
|
build-for-server-dev : clean-server build-client
|
||||||
mkdir -p server/app/web/static/img
|
mkdir -p server/app/web/static/img
|
||||||
|
mkdir -p server/app/web/static/js
|
||||||
|
mkdir -p server/app/web/templates/
|
||||||
cp client/build/index.html server/app/web/templates/
|
cp client/build/index.html server/app/web/templates/
|
||||||
cp -r client/build/static server/app/web/
|
cp -r client/build/static server/app/web/
|
||||||
cp client/build/favicon.png server/app/web/static/img
|
cp client/build/favicon.png server/app/web/static/img
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from flask import Flask
|
|||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from flask_compress import Compress
|
from flask_compress import Compress
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_restful_swagger_2 import get_swagger_blueprint
|
|
||||||
|
|
||||||
from .rest_api.rest import get_api_resources
|
from .rest_api.rest import get_api_resources
|
||||||
from .util.utils import Float32JSONEncoder
|
from .util.utils import Float32JSONEncoder
|
||||||
@@ -26,21 +25,7 @@ app.config.update(SECRET_KEY=SECRET_KEY)
|
|||||||
# Application Data
|
# Application Data
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
# A list of swagger document objects
|
|
||||||
docs = []
|
|
||||||
resources = get_api_resources()
|
resources = get_api_resources()
|
||||||
docs.append(resources.get_swagger_doc())
|
|
||||||
|
|
||||||
app.register_blueprint(webapp.bp)
|
app.register_blueprint(webapp.bp)
|
||||||
app.register_blueprint(resources.blueprint)
|
app.register_blueprint(resources.blueprint)
|
||||||
app.register_blueprint(
|
|
||||||
get_swagger_blueprint(
|
|
||||||
docs,
|
|
||||||
"/api/swagger",
|
|
||||||
produces=["application/json"],
|
|
||||||
title="cellxgene rest api",
|
|
||||||
description="An API connecting ExpressionMatrix2 clustering algorithm to cellxgene",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_url_rule("/", endpoint="index")
|
app.add_url_rule("/", endpoint="index")
|
||||||
|
|||||||
@@ -42,45 +42,12 @@ class CXGDriver(metaclass=ABCMeta):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def filter_dataframe(self, filter):
|
def annotation_to_fbs_matrix(self, axis, field=None):
|
||||||
"""
|
|
||||||
Filter cells from data and return a subset of the data. They can operate on both obs and var dimension with
|
|
||||||
indexing and filtering by annotation value. Filters are combined with the and operator.
|
|
||||||
See REST specs for info on filter format:
|
|
||||||
https://github.com/chanzuckerberg/cellxgene/blob/master/docs/REST_API.md
|
|
||||||
|
|
||||||
:param filter: dictionary with filter params
|
|
||||||
:return: View into scanpy object with cells/genes filtered
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def annotation(self, filter, axis, fields=None):
|
|
||||||
"""
|
"""
|
||||||
Gets annotation value for each observation
|
Gets annotation value for each observation
|
||||||
:param filter: filter: dictionary with filter params
|
|
||||||
:param axis: string obs or var
|
:param axis: string obs or var
|
||||||
:param fields: list of keys for annotation to return, returns all annotation values if not set.
|
:param fields: list of keys for annotation to return, returns all annotation values if not set.
|
||||||
:return: dict: names - list of fields in order, data - list of lists or metadata
|
:return: flatbuffer: in fbs/matrix.fbs encoding
|
||||||
[observation ids, val1, val2...]
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def annotation_to_fbs_matrix(self, axis, field=None):
|
|
||||||
""" Same as annotation(), except returns a flatbuffer, and does not support filtering. """
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def data_frame(self, filter, axis):
|
|
||||||
"""
|
|
||||||
Retrieves data for each variable for observations in data frame
|
|
||||||
:param filter: filter: dictionary with filter params
|
|
||||||
:param axis: string obs or var
|
|
||||||
:return: {
|
|
||||||
"var": list of variable ids,
|
|
||||||
"obs": [cellid, var1 expression, var2 expression, ...],
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -104,16 +71,6 @@ class CXGDriver(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def layout(self, filter, interactive_limit=None):
|
|
||||||
"""
|
|
||||||
Computes a n-d layout for cells through dimensionality reduction.
|
|
||||||
:param filter: filter: dictionary with filter params
|
|
||||||
:param interactive_limit: -- don't compute if total # genes in dataframes are larger than this
|
|
||||||
:return: [cellid, x, y, ...]
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def layout_to_fbs_matrix(self, filter):
|
def layout_to_fbs_matrix(self, filter):
|
||||||
""" same as layout, except returns a flatbuffer """
|
""" same as layout, except returns a flatbuffer """
|
||||||
|
|||||||
+13
-674
@@ -3,22 +3,17 @@ import pkg_resources
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from flask import Blueprint, current_app, jsonify, make_response, request
|
from flask import Blueprint, current_app, jsonify, make_response, request
|
||||||
from flask_restful_swagger_2 import Api, swagger, Resource
|
from flask_restful import Api, Resource
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
|
||||||
|
|
||||||
from server.app.util.constants import (
|
from server.app.util.constants import (
|
||||||
Axis,
|
Axis,
|
||||||
DiffExpMode,
|
DiffExpMode,
|
||||||
JSON_NaN_to_num_warning_msg,
|
JSON_NaN_to_num_warning_msg,
|
||||||
)
|
)
|
||||||
from server.app.util.filter import parse_filter, QueryStringError
|
|
||||||
from server.app.util.models import FilterModel
|
|
||||||
from server.app.util.utils import get_mime_type
|
|
||||||
from server.app.util.errors import (
|
from server.app.util.errors import (
|
||||||
FilterError,
|
FilterError,
|
||||||
InteractiveError,
|
InteractiveError,
|
||||||
JSONEncodingValueError,
|
JSONEncodingValueError,
|
||||||
MimeTypeError,
|
|
||||||
PrepareError,
|
PrepareError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,47 +26,6 @@ Sort order for routes
|
|||||||
|
|
||||||
|
|
||||||
class SchemaAPI(Resource):
|
class SchemaAPI(Resource):
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "get schema for dataframe and annotations",
|
|
||||||
"tags": ["initialize"],
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "schema",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"dataframe": {
|
|
||||||
"nObs": 383,
|
|
||||||
"nVar": 19944,
|
|
||||||
"type": "float32",
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"obs": [
|
|
||||||
{"name": "name", "type": "string"},
|
|
||||||
{"name": "tissue_type", "type": "string"},
|
|
||||||
{"name": "num_reads", "type": "int32"},
|
|
||||||
{"name": "sample_name", "type": "string"},
|
|
||||||
{
|
|
||||||
"name": "clusters",
|
|
||||||
"type": "categorical",
|
|
||||||
"categories": [99, 1, "unknown cluster"],
|
|
||||||
},
|
|
||||||
{"name": "QScore", "type": "float32"},
|
|
||||||
],
|
|
||||||
"var": [
|
|
||||||
{"name": "name", "type": "string"},
|
|
||||||
{"name": "gene", "type": "string"},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"schema": current_app.data.schema}), HTTPStatus.OK
|
jsonify({"schema": current_app.data.schema}), HTTPStatus.OK
|
||||||
@@ -79,47 +33,6 @@ class SchemaAPI(Resource):
|
|||||||
|
|
||||||
|
|
||||||
class ConfigAPI(Resource):
|
class ConfigAPI(Resource):
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Configuration information to assist in front-end adaptation"
|
|
||||||
" to underlying engine, available functionality, interactive time limits, etc",
|
|
||||||
"tags": ["initialize"],
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "schema",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"config": {
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/cluster/",
|
|
||||||
"available": False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/layout/obs",
|
|
||||||
"available": True,
|
|
||||||
"interactiveLimit": 10000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/layout/var",
|
|
||||||
"available": False,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"displayNames": {
|
|
||||||
"engine": "ScanPy version 1.33",
|
|
||||||
"dataset": "/home/joe/mouse/blorth.csv",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self):
|
def get(self):
|
||||||
config = {
|
config = {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -158,51 +71,13 @@ class ConfigAPI(Resource):
|
|||||||
|
|
||||||
|
|
||||||
class AnnotationsObsAPI(Resource):
|
class AnnotationsObsAPI(Resource):
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Fetch annotations (metadata) for all observations.",
|
|
||||||
"tags": ["annotations"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "annotation-name",
|
|
||||||
"type": "string",
|
|
||||||
"description": "list of 1 or more annotation names",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": ["tissue_type", "sex", "num_reads", "clusters"],
|
|
||||||
"data": [
|
|
||||||
[0, "lung", "F", 39844, 99],
|
|
||||||
[1, "heart", "M", 83, 1],
|
|
||||||
[49, "spleen", None, 2, "unknown cluster"],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "one or more of the annotation-name identifiers were not associated with an "
|
|
||||||
"annotation name"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self):
|
def get(self):
|
||||||
fields = request.args.getlist("annotation-name", None)
|
fields = request.args.getlist("annotation-name", None)
|
||||||
preferred_mimetype = request.accept_mimetypes.best_match(
|
preferred_mimetype = request.accept_mimetypes.best_match(
|
||||||
["application/json", "application/octet-stream"],
|
["application/octet-stream"]
|
||||||
"application/json"
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if preferred_mimetype == "application/json":
|
if preferred_mimetype == "application/octet-stream":
|
||||||
return make_response(
|
|
||||||
current_app.data.annotation({}, "obs", fields), HTTPStatus.OK, {"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
elif preferred_mimetype == "application/octet-stream":
|
|
||||||
return make_response(current_app.data.annotation_to_fbs_matrix("obs", fields),
|
return make_response(current_app.data.annotation_to_fbs_matrix("obs", fields),
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
{"Content-Type": "application/octet-stream"})
|
{"Content-Type": "application/octet-stream"})
|
||||||
@@ -210,119 +85,18 @@ class AnnotationsObsAPI(Resource):
|
|||||||
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Fetch annotations (metadata) for filtered subset of observations.",
|
|
||||||
"tags": ["annotations"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "annotation-name",
|
|
||||||
"type": "string",
|
|
||||||
"description": "list of 1 or more annotation names",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "filter",
|
|
||||||
"description": "Complex Filter",
|
|
||||||
"in": "body",
|
|
||||||
"schema": FilterModel,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": ["tissue_type", "sex", "num_reads", "clusters"],
|
|
||||||
"data": [
|
|
||||||
[0, "lung", "F", 39844, 99],
|
|
||||||
[1, "heart", "M", 83, 1],
|
|
||||||
[49, "spleen", None, 2, "unknown cluster"],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "malformed filter or one or more of the annotation-name identifiers were"
|
|
||||||
"not associated with an annotation name"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def put(self):
|
|
||||||
fields = request.args.getlist("annotation-name", None)
|
|
||||||
try:
|
|
||||||
annotation_response = current_app.data.annotation(
|
|
||||||
request.get_json()["filter"], "obs", fields
|
|
||||||
)
|
|
||||||
return make_response(
|
|
||||||
annotation_response, HTTPStatus.OK, {"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
|
||||||
except FilterError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class AnnotationsVarAPI(Resource):
|
class AnnotationsVarAPI(Resource):
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Fetch annotations (metadata) for all variables.",
|
|
||||||
"tags": ["annotations"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "annotation-name",
|
|
||||||
"type": "string",
|
|
||||||
"description": "list of 1 or more annotation names",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": ["name", "category"],
|
|
||||||
"data": [
|
|
||||||
[0, "ATAD3C", 1],
|
|
||||||
[1, "RER1", None],
|
|
||||||
[49, "S100B", 6],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "one or more of the annotation-name identifiers were not associated with an"
|
|
||||||
" annotation name"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self):
|
def get(self):
|
||||||
fields = request.args.getlist("annotation-name", None)
|
fields = request.args.getlist("annotation-name", None)
|
||||||
preferred_mimetype = request.accept_mimetypes.best_match(
|
preferred_mimetype = request.accept_mimetypes.best_match(
|
||||||
["application/json", "application/octet-stream"],
|
["application/octet-stream"]
|
||||||
"application/json"
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if preferred_mimetype == "application/json":
|
if preferred_mimetype == "application/octet-stream":
|
||||||
return make_response(current_app.data.annotation({}, "var", fields),
|
|
||||||
HTTPStatus.OK,
|
|
||||||
{"Content-Type": "application/json"})
|
|
||||||
elif preferred_mimetype == "application/octet-stream":
|
|
||||||
return make_response(current_app.data.annotation_to_fbs_matrix("var", fields),
|
return make_response(current_app.data.annotation_to_fbs_matrix("var", fields),
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
{"Content-Type": "application/octet-stream"})
|
{"Content-Type": "application/octet-stream"})
|
||||||
@@ -330,317 +104,22 @@ class AnnotationsVarAPI(Resource):
|
|||||||
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Fetch annotations (metadata) for filtered subset of variables.",
|
|
||||||
"tags": ["annotations"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "annotation-name",
|
|
||||||
"type": "string",
|
|
||||||
"description": "list of 1 or more annotation names",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "filter",
|
|
||||||
"description": "Complex Filter",
|
|
||||||
"in": "body",
|
|
||||||
"schema": FilterModel,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": ["name", "category"],
|
|
||||||
"data": [
|
|
||||||
[0, "ATAD3C", 1],
|
|
||||||
[1, "RER1", None],
|
|
||||||
[49, "S100B", 6],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "malformed filter or one or more of the annotation-name identifiers were"
|
|
||||||
"not associated with an annotation name"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def put(self):
|
|
||||||
fields = request.args.getlist("annotation-name", None)
|
|
||||||
try:
|
|
||||||
annotation_response = current_app.data.annotation(
|
|
||||||
request.get_json()["filter"], "var", fields
|
|
||||||
)
|
|
||||||
return make_response(
|
|
||||||
annotation_response, HTTPStatus.OK, {"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
|
||||||
except FilterError:
|
|
||||||
return make_response("Malformed filter", HTTPStatus.BAD_REQUEST)
|
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
class DataObsAPI(Resource):
|
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
|
||||||
"tags": ["data"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "filter",
|
|
||||||
"type": "string",
|
|
||||||
"description": "axis:key:value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "accept-type",
|
|
||||||
"type": "string",
|
|
||||||
"description": "MIME type",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"var": [0, 20000],
|
|
||||||
"obs": [[1, 39483, 3902, 203, 0, 0, 28]],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {"description": "Malformed filter"},
|
|
||||||
"406": {"description": "Unacceptable MIME type"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self):
|
|
||||||
accept_type = request.args.get("accept-type", None)
|
|
||||||
# request.args is immutable
|
|
||||||
args = request.args.copy()
|
|
||||||
args.pop("accept-type", None)
|
|
||||||
try:
|
|
||||||
filter_ = parse_filter(
|
|
||||||
ImmutableMultiDict(args), current_app.data.schema["annotations"]
|
|
||||||
)
|
|
||||||
except QueryStringError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
|
||||||
# TODO support CSV
|
|
||||||
try:
|
|
||||||
# TODO store mime_type when more than one is supported
|
|
||||||
get_mime_type(
|
|
||||||
acceptable_types=["application/json"],
|
|
||||||
query_param=accept_type,
|
|
||||||
header=request.accept_mimetypes,
|
|
||||||
)
|
|
||||||
except MimeTypeError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
|
||||||
try:
|
|
||||||
return make_response(
|
|
||||||
current_app.data.data_frame(filter_, axis=Axis.OBS),
|
|
||||||
HTTPStatus.OK,
|
|
||||||
{"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
except FilterError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
|
||||||
"tags": ["data"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "filter",
|
|
||||||
"description": "Complex Filter",
|
|
||||||
"in": "body",
|
|
||||||
"schema": FilterModel,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"var": [0, 20000],
|
|
||||||
"obs": [[1, 39483, 3902, 203, 0, 0, 28]],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {"description": "Malformed filter"},
|
|
||||||
"406": {"description": "Unacceptable MIME type"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def put(self):
|
|
||||||
if not request.accept_mimetypes.best_match(["application/json", "text/csv"]):
|
|
||||||
return make_response(
|
|
||||||
f"Unsupported MIME type '{request.accept_mimetypes}'",
|
|
||||||
HTTPStatus.NOT_ACCEPTABLE,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
get_mime_type(
|
|
||||||
acceptable_types=["application/json"], header=request.accept_mimetypes
|
|
||||||
)
|
|
||||||
except MimeTypeError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
|
||||||
try:
|
|
||||||
return make_response(
|
|
||||||
(
|
|
||||||
current_app.data.data_frame(
|
|
||||||
request.get_json()["filter"], axis=Axis.OBS
|
|
||||||
)
|
|
||||||
),
|
|
||||||
HTTPStatus.OK,
|
|
||||||
{"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
except FilterError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class DataVarAPI(Resource):
|
class DataVarAPI(Resource):
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
|
||||||
"tags": ["data"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "filter",
|
|
||||||
"type": "string",
|
|
||||||
"description": "axis:key:value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "accept-type",
|
|
||||||
"type": "string",
|
|
||||||
"description": "MIME type",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"obs": [0, 20000],
|
|
||||||
"var": [[1, 39483, 3902, 203, 0, 0, 28]],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {"description": "Malformed filter"},
|
|
||||||
"406": {"description": "Unacceptable MIME type"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self):
|
|
||||||
accept_type = request.args.get("accept-type", None)
|
|
||||||
# request.args is immutable
|
|
||||||
args = request.args.copy()
|
|
||||||
args.pop("accept-type", None)
|
|
||||||
try:
|
|
||||||
filter_ = parse_filter(
|
|
||||||
ImmutableMultiDict(args), current_app.data.schema["annotations"]
|
|
||||||
)
|
|
||||||
except QueryStringError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
|
||||||
try:
|
|
||||||
get_mime_type(
|
|
||||||
acceptable_types=["application/json"],
|
|
||||||
query_param=accept_type,
|
|
||||||
header=request.accept_mimetypes,
|
|
||||||
)
|
|
||||||
except MimeTypeError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
|
||||||
try:
|
|
||||||
return make_response(
|
|
||||||
current_app.data.data_frame(filter_, axis=Axis.VAR),
|
|
||||||
HTTPStatus.OK,
|
|
||||||
{"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
except FilterError as e:
|
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
|
||||||
"tags": ["data"],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "filter",
|
|
||||||
"description": "Complex Filter",
|
|
||||||
"in": "body",
|
|
||||||
"schema": FilterModel,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"obs": [0, 20000],
|
|
||||||
"var": [[1, 39483, 3902, 203, 0, 0, 28]],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {"description": "Malformed filter"},
|
|
||||||
"406": {"description": "Unacceptable MIME type"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def put(self):
|
def put(self):
|
||||||
preferred_mimetype = request.accept_mimetypes.best_match(
|
preferred_mimetype = request.accept_mimetypes.best_match(
|
||||||
["application/json", "application/octet-stream"],
|
["application/octet-stream"]
|
||||||
"application/json"
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if preferred_mimetype == "application/json":
|
if preferred_mimetype == "application/octet-stream":
|
||||||
return make_response(
|
filter_json = request.get_json()
|
||||||
(
|
filter = filter_json["filter"] if filter_json else None
|
||||||
current_app.data.data_frame(
|
|
||||||
request.get_json()["filter"], axis=Axis.VAR
|
|
||||||
)
|
|
||||||
),
|
|
||||||
HTTPStatus.OK,
|
|
||||||
{"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
elif preferred_mimetype == "application/octet-stream":
|
|
||||||
return make_response(
|
return make_response(
|
||||||
current_app.data.data_frame_to_fbs_matrix(
|
current_app.data.data_frame_to_fbs_matrix(
|
||||||
request.get_json()["filter"], axis=Axis.VAR
|
filter, axis=Axis.VAR
|
||||||
),
|
),
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
{"Content-Type": "application/octet-stream"})
|
{"Content-Type": "application/octet-stream"})
|
||||||
@@ -648,73 +127,11 @@ class DataVarAPI(Resource):
|
|||||||
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
||||||
except FilterError as e:
|
except FilterError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class DiffExpObsAPI(Resource):
|
class DiffExpObsAPI(Resource):
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Generate differential expression (DE) statistics for two specified subsets of data, "
|
|
||||||
"as indicated by the two provided observation complex filters",
|
|
||||||
"tags": ["diffexp"],
|
|
||||||
# TODO sort out params
|
|
||||||
# "parameters": [
|
|
||||||
# # {
|
|
||||||
# # "in": "body",
|
|
||||||
# # "name": "mode",
|
|
||||||
# # "type": "string",
|
|
||||||
# # "required": True,
|
|
||||||
# # "description": "topN or varFilter"
|
|
||||||
# # },
|
|
||||||
# {
|
|
||||||
# "in": "query",
|
|
||||||
# "name": "count",
|
|
||||||
# "type": "int32",
|
|
||||||
# "description": "TopN mode: how many vars to return"
|
|
||||||
# },
|
|
||||||
# {
|
|
||||||
# "in": "body",
|
|
||||||
# "name": "varFilter",
|
|
||||||
# "schema": FilterModel,
|
|
||||||
# "description": "varFilter: Complex filter, only var for which vars to return"
|
|
||||||
# },
|
|
||||||
# {
|
|
||||||
# "in": "body",
|
|
||||||
# "name": "set1",
|
|
||||||
# "schema": FilterModel,
|
|
||||||
# "required": True,
|
|
||||||
# "description": "Complex filter, only obs - observations in set1"
|
|
||||||
# },
|
|
||||||
# {
|
|
||||||
# "in": "body",
|
|
||||||
# "name": "set2",
|
|
||||||
# "schema": FilterModel,
|
|
||||||
# "description": "Complex filter, only obs - observations in set2.
|
|
||||||
# If not included, inverse of set1."
|
|
||||||
# },
|
|
||||||
# ],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Statistics are encoded as an array of arrays, with fields ordered as: "
|
|
||||||
"varIndex, logfoldchange, pVal, pValAdj",
|
|
||||||
"examples": {
|
|
||||||
"application/json": [
|
|
||||||
[328, -2.569_489, 2.655_706e-63, 3.642_036e-57],
|
|
||||||
[1250, -2.569_489, 2.655_706e-63, 3.642_036e-57],
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {"description": "malformed filter"},
|
|
||||||
"403": {"description": "non-interactive request"},
|
|
||||||
"501": {"description": "diffexp is not implemented"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def post(self):
|
def post(self):
|
||||||
args = request.get_json()
|
args = request.get_json()
|
||||||
# confirm mode is present and legal
|
# confirm mode is present and legal
|
||||||
@@ -783,40 +200,12 @@ class DiffExpObsAPI(Resource):
|
|||||||
|
|
||||||
|
|
||||||
class LayoutObsAPI(Resource):
|
class LayoutObsAPI(Resource):
|
||||||
@swagger.doc(
|
|
||||||
{
|
|
||||||
"summary": "Get the default layout for all observations.",
|
|
||||||
"tags": ["layout"],
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "layout",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"layout": {
|
|
||||||
"ndims": 2,
|
|
||||||
"coordinates": [
|
|
||||||
[0, 0.284_483, 0.983_744],
|
|
||||||
[1, 0.038_844, 0.739_444],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"400": {"description": "Data preparation error"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self):
|
def get(self):
|
||||||
preferred_mimetype = request.accept_mimetypes.best_match(
|
preferred_mimetype = request.accept_mimetypes.best_match(
|
||||||
["application/json", "application/octet-stream"],
|
["application/octet-stream"]
|
||||||
"application/json"
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if preferred_mimetype == "application/json":
|
if preferred_mimetype == "application/octet-stream":
|
||||||
return make_response(current_app.data.layout({}), HTTPStatus.OK, {"Content-Type": "application/json"})
|
|
||||||
|
|
||||||
elif preferred_mimetype == "application/octet-stream":
|
|
||||||
return make_response(current_app.data.layout_to_fbs_matrix(),
|
return make_response(current_app.data.layout_to_fbs_matrix(),
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
{"Content-Type": "application/octet-stream"})
|
{"Content-Type": "application/octet-stream"})
|
||||||
@@ -824,69 +213,19 @@ class LayoutObsAPI(Resource):
|
|||||||
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
||||||
except PrepareError as e:
|
except PrepareError as e:
|
||||||
return make_response(e.message, HTTPStatus.INTERNAL_SERVER_ERROR)
|
return make_response(e.message, HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
except JSONEncodingValueError as e:
|
|
||||||
# JSON encoding failure, usually due to bad data
|
|
||||||
warnings.warn(JSON_NaN_to_num_warning_msg)
|
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
# @swagger.doc({
|
|
||||||
# "summary": "Observation layout for filtered subset.",
|
|
||||||
# "tags": ["layout"],
|
|
||||||
# "parameters": [
|
|
||||||
# {
|
|
||||||
# "name": "filter",
|
|
||||||
# "description": "Complex Filter",
|
|
||||||
# "in": "body",
|
|
||||||
# "schema": FilterModel
|
|
||||||
# }
|
|
||||||
# ],
|
|
||||||
# "responses": {
|
|
||||||
# "200": {
|
|
||||||
# "description": "layout",
|
|
||||||
# "examples": {
|
|
||||||
# "application/json": {
|
|
||||||
# "layout": {
|
|
||||||
# "ndims": 2,
|
|
||||||
# "coordinates": [
|
|
||||||
# [0, 0.284483, 0.983744],
|
|
||||||
# [1, 0.038844, 0.739444]
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# },
|
|
||||||
# "400": {
|
|
||||||
# "description": "Malformed filter"
|
|
||||||
# },
|
|
||||||
# "403": {
|
|
||||||
# "description": "Non-interactive request"
|
|
||||||
# },
|
|
||||||
# }
|
|
||||||
# })
|
|
||||||
# def put(self):
|
|
||||||
# try:
|
|
||||||
# filter = request.get_json()["filter"]
|
|
||||||
# interactive_limit = current_app.data.features["layout"]["obs"]["interactiveLimit"]
|
|
||||||
# layout = current_app.data.layout(filter, interactive_limit=interactive_limit)
|
|
||||||
# return make_response(layout, HTTPStatus.OK, {"Content-Type": content_type})
|
|
||||||
# except FilterError as e:
|
|
||||||
# return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
|
||||||
# except InteractiveError:
|
|
||||||
# return make_response("Non-interactive request", HTTPStatus.FORBIDDEN)
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_resources():
|
def get_api_resources():
|
||||||
bp = Blueprint("api", __name__, url_prefix="/api/v0.2")
|
bp = Blueprint("api", __name__, url_prefix="/api/v0.2")
|
||||||
api = Api(bp, add_api_spec_resource=False)
|
api = Api(bp)
|
||||||
# Initialization routes
|
# Initialization routes
|
||||||
api.add_resource(SchemaAPI, "/schema")
|
api.add_resource(SchemaAPI, "/schema")
|
||||||
api.add_resource(ConfigAPI, "/config")
|
api.add_resource(ConfigAPI, "/config")
|
||||||
# Data routes
|
# Data routes
|
||||||
api.add_resource(AnnotationsObsAPI, "/annotations/obs")
|
api.add_resource(AnnotationsObsAPI, "/annotations/obs")
|
||||||
api.add_resource(AnnotationsVarAPI, "/annotations/var")
|
api.add_resource(AnnotationsVarAPI, "/annotations/var")
|
||||||
api.add_resource(DataObsAPI, "/data/obs")
|
|
||||||
api.add_resource(DataVarAPI, "/data/var")
|
api.add_resource(DataVarAPI, "/data/var")
|
||||||
# Computation routes
|
# Computation routes
|
||||||
api.add_resource(DiffExpObsAPI, "/diffexp/obs")
|
api.add_resource(DiffExpObsAPI, "/diffexp/obs")
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pandas import DataFrame
|
|
||||||
from pandas.core.dtypes.dtypes import CategoricalDtype
|
from pandas.core.dtypes.dtypes import CategoricalDtype
|
||||||
import scanpy.api as sc
|
import scanpy.api as sc
|
||||||
from scipy import sparse
|
|
||||||
|
|
||||||
from server.app.driver.driver import CXGDriver
|
from server.app.driver.driver import CXGDriver
|
||||||
from server.app.util.constants import Axis, DEFAULT_TOP_N
|
from server.app.util.constants import Axis, DEFAULT_TOP_N
|
||||||
from server.app.util.errors import (
|
from server.app.util.errors import (
|
||||||
FilterError,
|
FilterError,
|
||||||
InteractiveError,
|
|
||||||
JSONEncodingValueError,
|
JSONEncodingValueError,
|
||||||
PrepareError,
|
PrepareError,
|
||||||
ScanpyFileError,
|
ScanpyFileError,
|
||||||
@@ -197,23 +194,6 @@ class ScanpyEngine(CXGDriver):
|
|||||||
f"to solve this problem. "
|
f"to solve this problem. "
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_dataframe(self, filter):
|
|
||||||
"""
|
|
||||||
Filter cells from data and return a subset of the data. They can operate on both obs and var dimension with
|
|
||||||
indexing and filtering by annotation value. Filters are combined with the and operator.
|
|
||||||
See REST specs for info on filter format:
|
|
||||||
# TODO update this link to swagger when it's done
|
|
||||||
https://docs.google.com/document/d/1Fxjp1SKtCk7l8QP9-7KAjGXL0eldi_qEnNT0NmlGzXI/edit#heading=h.8qc9q57amldx
|
|
||||||
|
|
||||||
:param filter: dictionary with filter params
|
|
||||||
:return: View into scanpy object with cells/genes filtered
|
|
||||||
"""
|
|
||||||
if not filter:
|
|
||||||
return self.data
|
|
||||||
obs_selector, var_selector = self._filter_to_mask(filter, use_slices=False)
|
|
||||||
data = self._slice(self.data, obs_selector, var_selector)
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _annotation_filter_to_mask(filter, d_axis, count):
|
def _annotation_filter_to_mask(filter, d_axis, count):
|
||||||
mask = np.ones((count,), dtype=bool)
|
mask = np.ones((count,), dtype=bool)
|
||||||
@@ -277,72 +257,6 @@ class ScanpyEngine(CXGDriver):
|
|||||||
)
|
)
|
||||||
return obs_selector, var_selector
|
return obs_selector, var_selector
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _slice(data, obs_selector=None, vars_selector=None):
|
|
||||||
"""
|
|
||||||
Slice date using any selector that the AnnData object
|
|
||||||
supprots for slicing. If selector is None, will not slice
|
|
||||||
on that axis.
|
|
||||||
|
|
||||||
This method exists to optimize filtering/slicing sparse data that has
|
|
||||||
access patterns which impact slicing performance.
|
|
||||||
|
|
||||||
https://docs.scipy.org/doc/scipy/reference/sparse.html
|
|
||||||
"""
|
|
||||||
prefer_row_access = (
|
|
||||||
sparse.isspmatrix_csr(data._X)
|
|
||||||
or sparse.isspmatrix_lil(data._X)
|
|
||||||
or sparse.isspmatrix_bsr(data._X)
|
|
||||||
)
|
|
||||||
if prefer_row_access:
|
|
||||||
# Row-major slicing
|
|
||||||
if obs_selector is not None:
|
|
||||||
data = data[obs_selector, :]
|
|
||||||
if vars_selector is not None:
|
|
||||||
data = data[:, vars_selector]
|
|
||||||
else:
|
|
||||||
# Col-major slicing
|
|
||||||
if vars_selector is not None:
|
|
||||||
data = data[:, vars_selector]
|
|
||||||
if obs_selector is not None:
|
|
||||||
data = data[obs_selector, :]
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def annotation(self, filter, axis, fields=None):
|
|
||||||
"""
|
|
||||||
Gets annotation value for each observation
|
|
||||||
:param filter: filter: dictionary with filter params
|
|
||||||
:param axis: string obs or var
|
|
||||||
:param fields: list of keys for annotation to return, returns all annotation values if not set.
|
|
||||||
:return: dict: names - list of fields in order, data - list of lists or metadata
|
|
||||||
[observation ids, val1, val2...]
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
obs_selector, var_selector = self._filter_to_mask(filter)
|
|
||||||
except (KeyError, IndexError) as e:
|
|
||||||
raise FilterError(f"Error parsing filter: {e}") from e
|
|
||||||
if axis == Axis.OBS:
|
|
||||||
obs = self.data.obs[obs_selector]
|
|
||||||
if not fields:
|
|
||||||
fields = obs.columns.tolist()
|
|
||||||
result = {
|
|
||||||
"names": fields,
|
|
||||||
"data": DataFrame(obs[fields]).to_records(index=True).tolist(),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
var = self.data.var[var_selector]
|
|
||||||
if not fields:
|
|
||||||
fields = var.columns.tolist()
|
|
||||||
result = {
|
|
||||||
"names": fields,
|
|
||||||
"data": DataFrame(var[fields]).to_records(index=True).tolist(),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
return jsonify_scanpy(result)
|
|
||||||
except ValueError:
|
|
||||||
raise JSONEncodingValueError("Error encoding annotations to JSON")
|
|
||||||
|
|
||||||
def annotation_to_fbs_matrix(self, axis, fields=None):
|
def annotation_to_fbs_matrix(self, axis, fields=None):
|
||||||
if axis == Axis.OBS:
|
if axis == Axis.OBS:
|
||||||
df = self.data.obs
|
df = self.data.obs
|
||||||
@@ -352,44 +266,6 @@ class ScanpyEngine(CXGDriver):
|
|||||||
df = df[fields]
|
df = df[fields]
|
||||||
return encode_matrix_fbs(df, col_idx=df.columns)
|
return encode_matrix_fbs(df, col_idx=df.columns)
|
||||||
|
|
||||||
def data_frame(self, filter, axis):
|
|
||||||
"""
|
|
||||||
Retrieves data for each variable for observations in data frame
|
|
||||||
:param filter: filter: dictionary with filter params
|
|
||||||
:param axis: string obs or var
|
|
||||||
:return: {
|
|
||||||
"var": list of variable ids,
|
|
||||||
"obs": [cellid, var1 expression, var2 expression, ...],
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
obs_selector, var_selector = self._filter_to_mask(filter)
|
|
||||||
except (KeyError, IndexError) as e:
|
|
||||||
raise FilterError(f"Error parsing filter: {e}") from e
|
|
||||||
_X = self.data._X[obs_selector, var_selector]
|
|
||||||
if sparse.issparse(_X):
|
|
||||||
_X = _X.toarray()
|
|
||||||
var_index_sliced = self.data.var.index[var_selector]
|
|
||||||
obs_index_sliced = self.data.obs.index[obs_selector]
|
|
||||||
if axis == Axis.OBS:
|
|
||||||
result = {
|
|
||||||
"var": var_index_sliced.tolist(),
|
|
||||||
"obs": DataFrame(_X, index=obs_index_sliced)
|
|
||||||
.to_records(index=True)
|
|
||||||
.tolist(),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
result = {
|
|
||||||
"obs": obs_index_sliced.tolist(),
|
|
||||||
"var": DataFrame(_X.T, index=var_index_sliced)
|
|
||||||
.to_records(index=True)
|
|
||||||
.tolist(),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
return jsonify_scanpy(result)
|
|
||||||
except ValueError:
|
|
||||||
raise JSONEncodingValueError("Error encoding dataframe to JSON")
|
|
||||||
|
|
||||||
def data_frame_to_fbs_matrix(self, filter, axis):
|
def data_frame_to_fbs_matrix(self, filter, axis):
|
||||||
"""
|
"""
|
||||||
Retrieves data 'X' and returns in a flatbuffer Matrix.
|
Retrieves data 'X' and returns in a flatbuffer Matrix.
|
||||||
@@ -405,7 +281,7 @@ class ScanpyEngine(CXGDriver):
|
|||||||
raise ValueError("Only VAR dimension access is supported")
|
raise ValueError("Only VAR dimension access is supported")
|
||||||
try:
|
try:
|
||||||
obs_selector, var_selector = self._filter_to_mask(filter, use_slices=False)
|
obs_selector, var_selector = self._filter_to_mask(filter, use_slices=False)
|
||||||
except (KeyError, IndexError) as e:
|
except (KeyError, IndexError, TypeError) as e:
|
||||||
raise FilterError(f"Error parsing filter: {e}") from e
|
raise FilterError(f"Error parsing filter: {e}") from e
|
||||||
if obs_selector is not None:
|
if obs_selector is not None:
|
||||||
raise FilterError("filtering on obs unsupported")
|
raise FilterError("filtering on obs unsupported")
|
||||||
@@ -440,50 +316,6 @@ class ScanpyEngine(CXGDriver):
|
|||||||
"Error encoding differential expression to JSON"
|
"Error encoding differential expression to JSON"
|
||||||
)
|
)
|
||||||
|
|
||||||
def layout(self, filter, interactive_limit=None):
|
|
||||||
"""
|
|
||||||
Computes a n-d layout for cells through dimensionality reduction.
|
|
||||||
:param filter: filter: dictionary with filter params
|
|
||||||
:param interactive_limit: -- don't compute if total # genes in dataframes are larger than this
|
|
||||||
:return: [cellid, x, y, ...]
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
df = self.filter_dataframe(filter)
|
|
||||||
except (KeyError, IndexError) as e:
|
|
||||||
raise FilterError(f"Error parsing filter: {e}") from e
|
|
||||||
if interactive_limit and len(df.obs.index) > interactive_limit:
|
|
||||||
raise InteractiveError("Size data is too large for interactive computation")
|
|
||||||
# TODO Filtering cells is fine, but filtering genes does nothing because the neighbors are
|
|
||||||
# calculated using the original vars (geneset) and this doesn’t get updated when you use less.
|
|
||||||
# Need to recalculate neighbors (long) if user requests new layout filtered by var
|
|
||||||
# TODO for MVP we are pushing computation of layout to preprocessing and not allowing re-layout
|
|
||||||
# this will probably change after user feedback
|
|
||||||
# getattr(sc.tl, self.layout_method)(df, random_state=123)
|
|
||||||
try:
|
|
||||||
df_layout = df.obsm[f"X_{self.layout_method}"]
|
|
||||||
except ValueError as e:
|
|
||||||
raise PrepareError(
|
|
||||||
f"Layout has not been calculated using {self.layout_method}, "
|
|
||||||
f"please prepare your datafile and relaunch cellxgene"
|
|
||||||
) from e
|
|
||||||
normalized_layout = DataFrame(
|
|
||||||
(df_layout - df_layout.min()) / (df_layout.max() - df_layout.min()),
|
|
||||||
index=df.obs.index,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return jsonify_scanpy(
|
|
||||||
{
|
|
||||||
"layout": {
|
|
||||||
"ndims": normalized_layout.shape[1],
|
|
||||||
"coordinates": normalized_layout.to_records(
|
|
||||||
index=True
|
|
||||||
).tolist(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
raise JSONEncodingValueError("Error encoding layout to JSON")
|
|
||||||
|
|
||||||
def layout_to_fbs_matrix(self):
|
def layout_to_fbs_matrix(self):
|
||||||
"""
|
"""
|
||||||
Return the default 2-D layout for cells as a FBS Matrix.
|
Return the default 2-D layout for cells as a FBS Matrix.
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import json
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from numpy import float32, int32
|
|
||||||
|
|
||||||
from server.app.util.constants import Axis
|
|
||||||
|
|
||||||
|
|
||||||
class QueryStringError(Exception):
|
|
||||||
def __init__(self, key, message):
|
|
||||||
self.key = key
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_variable(datatype, variable):
|
|
||||||
"""
|
|
||||||
Convert variable to number (float/int)
|
|
||||||
Used for dataset metadata and for query string
|
|
||||||
:param datatype: type to convert to
|
|
||||||
:param variable (string or None): value of variable
|
|
||||||
:return: converted variable
|
|
||||||
:raises: AssertionError
|
|
||||||
"""
|
|
||||||
assert datatype in ["boolean", "categorical", "float32", "int32", "string"]
|
|
||||||
if variable is None:
|
|
||||||
return variable
|
|
||||||
if datatype == "int32":
|
|
||||||
variable = int32(variable)
|
|
||||||
elif datatype == "float32":
|
|
||||||
variable = float32(variable)
|
|
||||||
elif datatype == "boolean":
|
|
||||||
variable = json.loads(variable)
|
|
||||||
assert isinstance(variable, bool)
|
|
||||||
return variable
|
|
||||||
|
|
||||||
|
|
||||||
def parse_filter(query_filter, schema):
|
|
||||||
"""
|
|
||||||
The filter comes in as arguments from a GET request
|
|
||||||
For categorical metadata keys filter based on axis:key=value
|
|
||||||
For continuous metadata keys filter by axis:key=min,max
|
|
||||||
Either value can be replaced by a * To have only a minimum
|
|
||||||
value axis:key=min,* To have only a maximum value axis:key=*,max
|
|
||||||
|
|
||||||
They combine via AND so a cell's metadata would have to match every filter
|
|
||||||
|
|
||||||
The results is a matrix with the cells the pass the filter and at this point all the genes
|
|
||||||
:param query_filter: flask's request.args
|
|
||||||
:param schema: dictionary schema
|
|
||||||
:raises QueryStringError
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
query = defaultdict(lambda: defaultdict(list))
|
|
||||||
|
|
||||||
for key in query_filter:
|
|
||||||
axis, annotation = key.split(":", 1)
|
|
||||||
try:
|
|
||||||
Axis(axis)
|
|
||||||
except ValueError:
|
|
||||||
raise QueryStringError(key, f"Error: key {key} not in metadata schema")
|
|
||||||
ann_filter = {"name": annotation}
|
|
||||||
for ann in schema[axis]:
|
|
||||||
if ann["name"] == annotation:
|
|
||||||
dtype = ann["type"]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise QueryStringError(key, f"Error: {annotation} not a valid annotation name")
|
|
||||||
if dtype in ["string", "categorical", "boolean"]:
|
|
||||||
ann_filter["values"] = [_convert_variable(dtype, i) for i in query_filter.getlist(key)]
|
|
||||||
else:
|
|
||||||
value = query_filter.get(key)
|
|
||||||
try:
|
|
||||||
min_, max_ = value.split(",")
|
|
||||||
except ValueError:
|
|
||||||
raise QueryStringError(key, f"Error: min,max format required for range for {annotation}, got {value}")
|
|
||||||
if min_ == "*":
|
|
||||||
min_ = None
|
|
||||||
if max_ == "*":
|
|
||||||
max_ = None
|
|
||||||
try:
|
|
||||||
ann_filter["min"] = _convert_variable(dtype, min_)
|
|
||||||
ann_filter["max"] = _convert_variable(dtype, max_)
|
|
||||||
except ValueError:
|
|
||||||
raise QueryStringError(key, f"Error: expected type {query[key]['type']} for key {key}, got {value}")
|
|
||||||
query[axis]["annotation_value"].append(ann_filter)
|
|
||||||
return query
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
from flask_restful_swagger_2 import Schema
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotationModel(Schema):
|
|
||||||
type = "object"
|
|
||||||
description = "Filter by annotation key: value"
|
|
||||||
properties = {
|
|
||||||
"name": {"type": "string"},
|
|
||||||
# TODO update to OpenAPI v3.0 when a library is available that supports it
|
|
||||||
# Unfortunately 2.0 doesn't have a way to have a schema that accepts multiple types
|
|
||||||
# Overloading the type key with a list seems to work ok and makes it to the page
|
|
||||||
"values": {"type": "array", "items": {"type": ["float32", "string", "int32", "bool"]}},
|
|
||||||
"min": {"type": ["int32", "float32"]},
|
|
||||||
"max": {"type": ["int32", "float32"]},
|
|
||||||
}
|
|
||||||
required = ["name"]
|
|
||||||
|
|
||||||
|
|
||||||
class IndexModel(Schema):
|
|
||||||
type = "object"
|
|
||||||
description = "Filter by index of observation/variable ex. [0, 5, 15]"
|
|
||||||
properties = {"index": {"type": "array", "items": {"format": "int32", "type": "integer"}}}
|
|
||||||
|
|
||||||
|
|
||||||
class AxisModel(Schema):
|
|
||||||
type = "object"
|
|
||||||
description = "Axis of data -- obs or var"
|
|
||||||
properties = {"index": IndexModel, "annotation_value": AnnotationModel.array()}
|
|
||||||
|
|
||||||
|
|
||||||
class FilterModel(Schema):
|
|
||||||
type = "object"
|
|
||||||
description = "Complex filter"
|
|
||||||
properties = {"filter": {"type": "object", "properties": {"obs": AxisModel, "var": AxisModel}}}
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from argparse import ArgumentTypeError
|
|
||||||
|
|
||||||
from numpy import float32, integer
|
from numpy import float32, integer
|
||||||
|
|
||||||
from server.app.util.errors import MimeTypeError
|
|
||||||
|
|
||||||
|
|
||||||
class Float32JSONEncoder(json.JSONEncoder):
|
class Float32JSONEncoder(json.JSONEncoder):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -30,31 +26,5 @@ def custom_format_warning(msg, *args, **kwargs):
|
|||||||
return f"[cellxgene] Warning: {msg} \n"
|
return f"[cellxgene] Warning: {msg} \n"
|
||||||
|
|
||||||
|
|
||||||
def get_mime_type(
|
|
||||||
default="application/json", acceptable_types=["application/json", "text/csv"], query_param=None, header=None
|
|
||||||
):
|
|
||||||
mime_type = default
|
|
||||||
if query_param:
|
|
||||||
if query_param in acceptable_types:
|
|
||||||
mime_type = query_param
|
|
||||||
else:
|
|
||||||
raise MimeTypeError(f"Unsupported mime type {query_param} specified in query parameter 'accept-type'")
|
|
||||||
elif len(header):
|
|
||||||
mime_type = header.best_match(acceptable_types)
|
|
||||||
if not mime_type:
|
|
||||||
raise MimeTypeError(f"Unsupported mime type(s) {header} in HTTP Accept header")
|
|
||||||
return mime_type
|
|
||||||
|
|
||||||
|
|
||||||
def whole_number(value):
|
|
||||||
try:
|
|
||||||
value = int(value)
|
|
||||||
except ValueError as e:
|
|
||||||
raise ArgumentTypeError(f"{value} is not type int") from e
|
|
||||||
if value < 0:
|
|
||||||
raise ArgumentTypeError(f"{value} is not >= 0")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def jsonify_scanpy(data):
|
def jsonify_scanpy(data):
|
||||||
return json.dumps(data, cls=Float32JSONEncoder, allow_nan=False)
|
return json.dumps(data, cls=Float32JSONEncoder, allow_nan=False)
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>cellxgene REST API - Swagger definition</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700"
|
|
||||||
rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.2.1/swagger-ui.css"
|
|
||||||
crossorigin="anonymous"/>
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow: -moz-scrollbars-vertical;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
box-sizing: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
style="position:absolute;width:0;height:0">
|
|
||||||
<defs>
|
|
||||||
<symbol viewBox="0 0 20 20" id="unlocked">
|
|
||||||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol viewBox="0 0 20 20" id="locked">
|
|
||||||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol viewBox="0 0 20 20" id="close">
|
|
||||||
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol viewBox="0 0 20 20" id="large-arrow">
|
|
||||||
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol viewBox="0 0 20 20" id="large-arrow-down">
|
|
||||||
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
|
|
||||||
<symbol viewBox="0 0 24 24" id="jump-to">
|
|
||||||
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
<symbol viewBox="0 0 24 24" id="expand">
|
|
||||||
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div id="swagger-ui"></div>
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.2.1/swagger-ui-bundle.js"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.2.1/swagger-ui-standalone-preset.js"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script>
|
|
||||||
window.onload = function () {
|
|
||||||
const ui = SwaggerUIBundle({
|
|
||||||
url: window.location.href.replace(/\/swagger$/, "") + "/api/swagger.json",
|
|
||||||
dom_id: '#swagger-ui',
|
|
||||||
deepLinking: true,
|
|
||||||
presets: [
|
|
||||||
SwaggerUIBundle.presets.apis,
|
|
||||||
SwaggerUIStandalonePreset
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
SwaggerUIBundle.plugins.DownloadUrl
|
|
||||||
],
|
|
||||||
layout: "StandaloneLayout"
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ui = ui
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -11,13 +11,6 @@ def index():
|
|||||||
return render_template("index.html", datasetTitle=dataset_title)
|
return render_template("index.html", datasetTitle=dataset_title)
|
||||||
|
|
||||||
|
|
||||||
# renders swagger documentation
|
|
||||||
@bp.route("/swagger")
|
|
||||||
def swag():
|
|
||||||
return render_template("swagger.html")
|
|
||||||
|
|
||||||
|
|
||||||
# renders swagger documentation
|
|
||||||
@bp.route("/favicon.png")
|
@bp.route("/favicon.png")
|
||||||
def favicon():
|
def favicon():
|
||||||
return send_from_directory(os.path.join(bp.root_path, "static/img/"), "favicon.png")
|
return send_from_directory(os.path.join(bp.root_path, "static/img/"), "favicon.png")
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ from .prepare import prepare
|
|||||||
|
|
||||||
|
|
||||||
@click.group(name="cellxgene", context_settings=dict(max_content_width=85))
|
@click.group(name="cellxgene", context_settings=dict(max_content_width=85))
|
||||||
@click.version_option(version="0.6.0", prog_name="cellxgene", message="[%(prog)s] Version %(version)s")
|
@click.version_option(version="0.7.0", prog_name="cellxgene", message="[%(prog)s] Version %(version)s")
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ Flask-Caching>=1.4.0
|
|||||||
Flask-Compress>=1.4.0
|
Flask-Compress>=1.4.0
|
||||||
Flask-Cors>=3.0.6
|
Flask-Cors>=3.0.6
|
||||||
Flask-RESTful>=0.3.6
|
Flask-RESTful>=0.3.6
|
||||||
flask-restful-swagger-2>=0.35
|
|
||||||
flatbuffers>=1.10.0
|
flatbuffers>=1.10.0
|
||||||
matplotlib>=2.2
|
matplotlib>=2.2
|
||||||
numpy>=1.14.5
|
numpy>=1.14.5
|
||||||
|
|||||||
+71
-292
@@ -57,16 +57,6 @@ class EndPoints(unittest.TestCase):
|
|||||||
self.assertEqual(result_data["config"]["displayNames"]["dataset"], "pbmc3k")
|
self.assertEqual(result_data["config"]["displayNames"]["dataset"], "pbmc3k")
|
||||||
self.assertEqual(len(result_data["config"]["features"]), 4)
|
self.assertEqual(len(result_data["config"]["features"]), 4)
|
||||||
|
|
||||||
def test_get_layout(self):
|
|
||||||
endpoint = "layout/obs"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
result = self.session.get(url)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["layout"]["ndims"], 2)
|
|
||||||
self.assertEqual(len(result_data["layout"]["coordinates"]), 2638)
|
|
||||||
|
|
||||||
def test_get_layout_fbs(self):
|
def test_get_layout_fbs(self):
|
||||||
endpoint = "layout/obs"
|
endpoint = "layout/obs"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
@@ -82,53 +72,11 @@ class EndPoints(unittest.TestCase):
|
|||||||
self.assertIsNone(df['row_idx'])
|
self.assertIsNone(df['row_idx'])
|
||||||
self.assertEqual(len(df['columns']), df['n_cols'])
|
self.assertEqual(len(df['columns']), df['n_cols'])
|
||||||
|
|
||||||
# def test_put_layout(self):
|
|
||||||
# endpoint = "layout/obs"
|
|
||||||
# url = f"{URL_BASE}{endpoint}"
|
|
||||||
# obs_filter = {
|
|
||||||
# "filter": {
|
|
||||||
# "obs": {
|
|
||||||
# "annotation_value": [
|
|
||||||
# {"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
|
||||||
# {"name": "n_counts", "min": 3000},
|
|
||||||
# ],
|
|
||||||
# "index": [1, 99, [1000, 2000]]
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# result = self.session.put(url, json=obs_filter)
|
|
||||||
# self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
# result_data = result.json()
|
|
||||||
# self.assertEqual(len(result_data["layout"]["coordinates"]), 15)
|
|
||||||
|
|
||||||
def test_bad_filter(self):
|
def test_bad_filter(self):
|
||||||
endpoints = ["annotations/obs", "annotations/var", "data/obs", "data/var"]
|
endpoint = "data/var"
|
||||||
for endpoint in endpoints:
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
result = self.session.put(url, json=BAD_FILTER)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
def test_get_annotations_obs(self):
|
|
||||||
endpoint = "annotations/obs"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
result = self.session.get(url)
|
result = self.session.put(url, json=BAD_FILTER)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["name", "n_genes", "percent_mito", "n_counts", "louvain"])
|
|
||||||
self.assertEqual(len(result_data["data"]), 2638)
|
|
||||||
self.assertEqual(len(result_data["data"][0]), 6)
|
|
||||||
|
|
||||||
def test_get_annotations_obs_keys(self):
|
|
||||||
endpoint = "annotations/obs"
|
|
||||||
query = "annotation-name=n_genes&annotation-name=percent_mito"
|
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
|
||||||
result = self.session.get(url)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["n_genes", "percent_mito"])
|
|
||||||
self.assertEqual(len(result_data["data"][0]), 3)
|
|
||||||
|
|
||||||
def test_get_annotations_obs_fbs(self):
|
def test_get_annotations_obs_fbs(self):
|
||||||
endpoint = "annotations/obs"
|
endpoint = "annotations/obs"
|
||||||
@@ -146,6 +94,23 @@ class EndPoints(unittest.TestCase):
|
|||||||
self.assertEqual(len(df['columns']), df['n_cols'])
|
self.assertEqual(len(df['columns']), df['n_cols'])
|
||||||
self.assertListEqual(df['col_idx'], ['name', 'n_genes', 'percent_mito', 'n_counts', 'louvain'])
|
self.assertListEqual(df['col_idx'], ['name', 'n_genes', 'percent_mito', 'n_counts', 'louvain'])
|
||||||
|
|
||||||
|
def test_get_annotations_obs_keys_fbs(self):
|
||||||
|
endpoint = "annotations/obs"
|
||||||
|
query = "annotation-name=n_genes&annotation-name=percent_mito"
|
||||||
|
url = f"{URL_BASE}{endpoint}?{query}"
|
||||||
|
header = {"Accept": "application/octet-stream"}
|
||||||
|
result = self.session.get(url, headers=header)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertEqual(df['n_rows'], 2638)
|
||||||
|
self.assertEqual(df['n_cols'], 2)
|
||||||
|
self.assertIsNotNone(df['columns'])
|
||||||
|
self.assertIsNotNone(df['col_idx'])
|
||||||
|
self.assertIsNone(df['row_idx'])
|
||||||
|
self.assertEqual(len(df['columns']), df['n_cols'])
|
||||||
|
self.assertListEqual(df['col_idx'], ['n_genes', 'percent_mito'])
|
||||||
|
|
||||||
def test_get_annotations_obs_error(self):
|
def test_get_annotations_obs_error(self):
|
||||||
endpoint = "annotations/obs"
|
endpoint = "annotations/obs"
|
||||||
query = "annotation-name=notakey"
|
query = "annotation-name=notakey"
|
||||||
@@ -153,50 +118,6 @@ class EndPoints(unittest.TestCase):
|
|||||||
result = self.session.get(url)
|
result = self.session.get(url)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
def test_put_annotations_obs(self):
|
|
||||||
endpoint = "annotations/obs"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
obs_filter = {
|
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
],
|
|
||||||
"index": [1, 99, [1000, 2000]],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, json=obs_filter)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["name", "n_genes", "percent_mito", "n_counts", "louvain"])
|
|
||||||
self.assertEqual(len(result_data["data"]), 15)
|
|
||||||
|
|
||||||
def test_filter_put_annotations_obs(self):
|
|
||||||
endpoint = "annotations/obs"
|
|
||||||
query = "annotation-name=n_genes&annotation-name=percent_mito"
|
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
|
||||||
obs_filter = {
|
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
],
|
|
||||||
"index": [1, 99, [1000, 2000]],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, json=obs_filter)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["n_genes", "percent_mito"])
|
|
||||||
self.assertEqual(len(result_data["data"][0]), 3)
|
|
||||||
self.assertEqual(len(result_data["data"]), 15)
|
|
||||||
|
|
||||||
def test_diff_exp(self):
|
def test_diff_exp(self):
|
||||||
endpoint = "diffexp/obs"
|
endpoint = "diffexp/obs"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
@@ -227,28 +148,6 @@ class EndPoints(unittest.TestCase):
|
|||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
self.assertEqual(len(result_data), 10)
|
self.assertEqual(len(result_data), 10)
|
||||||
|
|
||||||
def test_get_annotations_var(self):
|
|
||||||
endpoint = "annotations/var"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
result = self.session.get(url)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["name", "n_cells"])
|
|
||||||
self.assertEqual(len(result_data["data"]), 1838)
|
|
||||||
self.assertEqual(len(result_data["data"][0]), 3)
|
|
||||||
|
|
||||||
def test_get_annotations_var_keys(self):
|
|
||||||
endpoint = "annotations/var"
|
|
||||||
query = "annotation-name=n_cells"
|
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
|
||||||
result = self.session.get(url)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["n_cells"])
|
|
||||||
self.assertEqual(len(result_data["data"][0]), 2)
|
|
||||||
|
|
||||||
def test_get_annotations_var_fbs(self):
|
def test_get_annotations_var_fbs(self):
|
||||||
endpoint = "annotations/var"
|
endpoint = "annotations/var"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
@@ -265,6 +164,23 @@ class EndPoints(unittest.TestCase):
|
|||||||
self.assertEqual(len(df['columns']), df['n_cols'])
|
self.assertEqual(len(df['columns']), df['n_cols'])
|
||||||
self.assertListEqual(df['col_idx'], ['name', 'n_cells'])
|
self.assertListEqual(df['col_idx'], ['name', 'n_cells'])
|
||||||
|
|
||||||
|
def test_get_annotations_var_keys_fbs(self):
|
||||||
|
endpoint = "annotations/var"
|
||||||
|
query = "annotation-name=n_cells"
|
||||||
|
url = f"{URL_BASE}{endpoint}?{query}"
|
||||||
|
header = {"Accept": "application/octet-stream"}
|
||||||
|
result = self.session.get(url, headers=header)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertEqual(df['n_rows'], 1838)
|
||||||
|
self.assertEqual(df['n_cols'], 1)
|
||||||
|
self.assertIsNotNone(df['columns'])
|
||||||
|
self.assertIsNotNone(df['col_idx'])
|
||||||
|
self.assertIsNone(df['row_idx'])
|
||||||
|
self.assertEqual(len(df['columns']), df['n_cols'])
|
||||||
|
self.assertListEqual(df['col_idx'], ['n_cells'])
|
||||||
|
|
||||||
def test_get_annotations_var_error(self):
|
def test_get_annotations_var_error(self):
|
||||||
endpoint = "annotations/var"
|
endpoint = "annotations/var"
|
||||||
query = "annotation-name=notakey"
|
query = "annotation-name=notakey"
|
||||||
@@ -272,95 +188,36 @@ class EndPoints(unittest.TestCase):
|
|||||||
result = self.session.get(url)
|
result = self.session.get(url)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
def test_put_annotations_var(self):
|
|
||||||
endpoint = "annotations/var"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
var_filter = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["ATAD3C", "RER1"]}]}}}
|
|
||||||
result = self.session.put(url, json=var_filter)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["name", "n_cells"])
|
|
||||||
self.assertEqual(len(result_data["data"]), 2)
|
|
||||||
|
|
||||||
def test_filter_put_annotations_var(self):
|
|
||||||
endpoint = "annotations/var"
|
|
||||||
query = "annotation-name=n_cells"
|
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
|
||||||
var_filter = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["ATAD3C", "RER1"]}]}}}
|
|
||||||
result = self.session.put(url, json=var_filter)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(result_data["names"], ["n_cells"])
|
|
||||||
self.assertEqual(len(result_data["data"][0]), 2)
|
|
||||||
self.assertEqual(len(result_data["data"]), 2)
|
|
||||||
|
|
||||||
def test_get_data(self):
|
|
||||||
for axis in ["obs", "var"]:
|
|
||||||
endpoint = f"data/{axis}"
|
|
||||||
query = "accept-type=application/json"
|
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
|
||||||
result = self.session.get(url)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(len(result_data["obs"]), 2638)
|
|
||||||
|
|
||||||
def test_data_mimetype_error(self):
|
def test_data_mimetype_error(self):
|
||||||
for axis in ["obs", "var"]:
|
endpoint = f"data/var"
|
||||||
endpoint = f"data/{axis}"
|
header = {"Accept": "xxx"}
|
||||||
query = "accept-type=xxx"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
result = self.session.put(url, headers=header)
|
||||||
result = self.session.get(url)
|
self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE)
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
header = {"Accept": "sdkljfa;dsjalkj"}
|
|
||||||
result = self.session.get(url, headers=header)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.NOT_ACCEPTABLE)
|
|
||||||
|
|
||||||
def test_json_default(self):
|
def test_fbs_default(self):
|
||||||
for axis in ["obs", "var"]:
|
endpoint = f"data/var"
|
||||||
endpoint = f"data/{axis}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
result = self.session.put(url)
|
||||||
result = self.session.get(url)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
|
|
||||||
def test_data_filter(self):
|
|
||||||
for axis in ["obs", "var"]:
|
|
||||||
endpoint = f"data/{axis}"
|
|
||||||
query = "accept-type=application/json&obs:louvain=NK cells&obs:louvain=CD8 T cells&obs:n_counts=3000,*"
|
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
|
||||||
result = self.session.get(url)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(len(result_data["obs"]), 38)
|
|
||||||
|
|
||||||
def test_data_json_put(self):
|
|
||||||
for axis in ["obs", "var"]:
|
|
||||||
endpoint = f"data/{axis}"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
header = {"Accept": "application/json"}
|
|
||||||
obs_filter = {
|
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
],
|
|
||||||
"index": [1, 99, [1000, 2000]],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, headers=header, json=obs_filter)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
self.assertEqual(len(result_data["obs"]), 15)
|
|
||||||
|
|
||||||
def test_data_put_fbs(self):
|
def test_data_put_fbs(self):
|
||||||
|
endpoint = f"data/var"
|
||||||
|
url = f"{URL_BASE}{endpoint}"
|
||||||
|
header = {"Accept": "application/octet-stream"}
|
||||||
|
result = self.session.put(url, headers=header)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertEqual(df['n_rows'], 2638)
|
||||||
|
self.assertEqual(df['n_cols'], 1838)
|
||||||
|
self.assertIsNotNone(df['columns'])
|
||||||
|
self.assertListEqual(df['col_idx'].tolist(), [])
|
||||||
|
self.assertIsNone(df['row_idx'])
|
||||||
|
self.assertEqual(len(df['columns']), df['n_cols'])
|
||||||
|
|
||||||
|
def test_data_put_filter_fbs(self):
|
||||||
endpoint = f"data/var"
|
endpoint = f"data/var"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
header = {"Accept": "application/octet-stream"}
|
header = {"Accept": "application/octet-stream"}
|
||||||
@@ -384,94 +241,16 @@ class EndPoints(unittest.TestCase):
|
|||||||
self.assertListEqual(df['col_idx'].tolist(), [0, 1, 4])
|
self.assertListEqual(df['col_idx'].tolist(), [0, 1, 4])
|
||||||
|
|
||||||
def test_data_put_single_var(self):
|
def test_data_put_single_var(self):
|
||||||
for axis in ["obs", "var"]:
|
endpoint = f"data/var"
|
||||||
endpoint = f"data/{axis}"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
header = {"Accept": "application/json"}
|
|
||||||
var_filter = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}}}
|
|
||||||
result = self.session.put(url, headers=header, json=var_filter)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data = result.json()
|
|
||||||
if axis == "obs":
|
|
||||||
self.assertEqual(len(result_data["obs"][0]), 2)
|
|
||||||
self.assertEqual(len(result_data["var"]), 1)
|
|
||||||
elif axis == "var":
|
|
||||||
self.assertEqual(len(result_data["obs"]), 2638)
|
|
||||||
self.assertEqual(len(result_data["var"][0]), 2639)
|
|
||||||
|
|
||||||
def test_cache(self):
|
|
||||||
endpoint = "annotations/var"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
f1 = {
|
header = {"Accept": "application/octet-stream"}
|
||||||
"filter": {
|
var_filter = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}}}
|
||||||
"var": {
|
result = self.session.put(url, headers=header, json=var_filter)
|
||||||
"annotation_value": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"values": [
|
|
||||||
"HLA-DRB1",
|
|
||||||
"HLA-DQA1",
|
|
||||||
"HLA-DQB1",
|
|
||||||
"HLA-DPA1",
|
|
||||||
"HLA-DPB1",
|
|
||||||
"MS4A1",
|
|
||||||
"IL32",
|
|
||||||
"CCL5",
|
|
||||||
"CD79B",
|
|
||||||
"CD79A",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, json=f1)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
result_data1 = result.json()
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
f2 = {
|
self.assertEqual(df["n_rows"], 2638)
|
||||||
"filter": {
|
self.assertEqual(df["n_cols"], 1)
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH", "CCL5", "CCL4", "CST7", "NKG7"],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, json=f2)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data2 = result.json()
|
|
||||||
self.assertNotEqual(result_data1, result_data2)
|
|
||||||
|
|
||||||
def test_cache_nofilter(self):
|
|
||||||
endpoint = "annotations/var"
|
|
||||||
url = f"{URL_BASE}{endpoint}"
|
|
||||||
f1 = {"filter": {}}
|
|
||||||
result = self.session.put(url, json=f1)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
result_data1 = result.json()
|
|
||||||
f2 = {
|
|
||||||
"filter": {
|
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH", "CCL5", "CCL4", "CST7", "NKG7"],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, json=f2)
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
|
||||||
result_data2 = result.json()
|
|
||||||
self.assertNotEqual(result_data1, result_data2)
|
|
||||||
|
|
||||||
def test_static(self):
|
def test_static(self):
|
||||||
endpoint = "static"
|
endpoint = "static"
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import json
|
|
||||||
from os import path
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from numpy import float32, int32
|
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
|
||||||
|
|
||||||
from server.app.util.filter import _convert_variable, parse_filter, QueryStringError
|
|
||||||
|
|
||||||
|
|
||||||
class UtilTest(unittest.TestCase):
|
|
||||||
"""Test Case for endpoints"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
with open(path.join(path.dirname(__file__), "schema.json")) as fh:
|
|
||||||
schema = json.load(fh)
|
|
||||||
self.schema = schema["annotations"]
|
|
||||||
|
|
||||||
def test_convert(self):
|
|
||||||
five = _convert_variable("int32", "5")
|
|
||||||
self.assertEqual(five, int32(5))
|
|
||||||
|
|
||||||
def test_convert_zero(self):
|
|
||||||
zero = _convert_variable("int32", "0")
|
|
||||||
self.assertEqual(zero, 0)
|
|
||||||
|
|
||||||
def test_convert_float(self):
|
|
||||||
str_to_convert = "4.38719237129"
|
|
||||||
val = _convert_variable("float32", str_to_convert)
|
|
||||||
self.assertAlmostEqual(val, float32(str_to_convert))
|
|
||||||
|
|
||||||
def test_convert_bool(self):
|
|
||||||
str_to_convert = "false"
|
|
||||||
val = _convert_variable("boolean", str_to_convert)
|
|
||||||
self.assertFalse(val)
|
|
||||||
str_to_convert = "true"
|
|
||||||
val = _convert_variable("boolean", str_to_convert)
|
|
||||||
self.assertTrue(val)
|
|
||||||
str_to_convert = "0"
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
val = _convert_variable("boolean", str_to_convert)
|
|
||||||
|
|
||||||
def test_empty_convert(self):
|
|
||||||
empty = _convert_variable("int32", None)
|
|
||||||
self.assertIsNone(empty)
|
|
||||||
|
|
||||||
def test_bad_convert(self):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
_convert_variable("int32", "5.5")
|
|
||||||
|
|
||||||
def test_bad_datatype(self):
|
|
||||||
with self.assertRaises(AssertionError):
|
|
||||||
_convert_variable("jkasdslkja", 1)
|
|
||||||
|
|
||||||
def test_complex_filter(self):
|
|
||||||
filter_dict = ImmutableMultiDict(
|
|
||||||
[("obs:louvain", "NK cells"), ("obs:louvain", "CD8 T cells"), ("obs:n_counts", "3000,*")]
|
|
||||||
)
|
|
||||||
filter_ = parse_filter(filter_dict, self.schema)
|
|
||||||
self.assertIn("obs", filter_)
|
|
||||||
self.assertEqual(
|
|
||||||
filter_["obs"]["annotation_value"],
|
|
||||||
[
|
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
|
||||||
{"name": "n_counts", "max": None, "min": 3000.0},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_bad_filter(self):
|
|
||||||
bad_annotation_type = ImmutableMultiDict([("obs:tissue", "lung")])
|
|
||||||
with self.assertRaises(QueryStringError):
|
|
||||||
parse_filter(bad_annotation_type, self.schema)
|
|
||||||
bad_axis = ImmutableMultiDict([("xyz:n_genes", "100,1000")])
|
|
||||||
with self.assertRaises(QueryStringError):
|
|
||||||
parse_filter(bad_axis, self.schema)
|
|
||||||
|
|
||||||
def test_boolean_filter(self):
|
|
||||||
schema = {"obs": [{"name": "bool_filter", "type": "boolean"}]}
|
|
||||||
filter_dict = ImmutableMultiDict([("obs:bool_filter", "false")])
|
|
||||||
filter_ = parse_filter(filter_dict, schema)
|
|
||||||
self.assertIn("obs", filter_)
|
|
||||||
self.assertEqual(filter_["obs"]["annotation_value"], [{"name": "bool_filter", "values": [False]}])
|
|
||||||
@@ -2,6 +2,9 @@ from http import HTTPStatus
|
|||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
import unittest
|
import unittest
|
||||||
import time
|
import time
|
||||||
|
import math
|
||||||
|
|
||||||
|
import decode_fbs
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -43,9 +46,29 @@ class WithNaNs(unittest.TestCase):
|
|||||||
result = self.session.get(url)
|
result = self.session.get(url)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
|
||||||
def test_errors(self):
|
def test_data(self):
|
||||||
endpoints = ["annotations/obs", "annotations/var", "data/obs", "data/var"]
|
endpoint = "data/var"
|
||||||
for endpoint in endpoints:
|
url = f"{URL_BASE}{endpoint}"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
result = self.session.put(url)
|
||||||
result = self.session.get(url)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertTrue(math.isnan(df["columns"][3][3]))
|
||||||
|
|
||||||
|
def test_annotation_obs(self):
|
||||||
|
endpoint = "annotations/obs"
|
||||||
|
url = f"{URL_BASE}{endpoint}"
|
||||||
|
result = self.session.get(url)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertTrue(math.isnan(df["columns"][2][0]))
|
||||||
|
|
||||||
|
def test_annotation_var(self):
|
||||||
|
endpoint = "annotations/var"
|
||||||
|
url = f"{URL_BASE}{endpoint}"
|
||||||
|
result = self.session.get(url)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertTrue(math.isnan(df["columns"][2][0]))
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import pytest
|
import pytest
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
@@ -7,7 +6,7 @@ import math
|
|||||||
import decode_fbs
|
import decode_fbs
|
||||||
|
|
||||||
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
||||||
from server.app.util.errors import JSONEncodingValueError
|
from server.app.util.errors import FilterError
|
||||||
|
|
||||||
|
|
||||||
class NaNTest(unittest.TestCase):
|
class NaNTest(unittest.TestCase):
|
||||||
@@ -42,10 +41,15 @@ class NaNTest(unittest.TestCase):
|
|||||||
self.assertEqual(data_frame_var["n_cols"], 100)
|
self.assertEqual(data_frame_var["n_cols"], 100)
|
||||||
self.assertTrue(math.isnan(data_frame_var["columns"][3][3]))
|
self.assertTrue(math.isnan(data_frame_var["columns"][3][3]))
|
||||||
|
|
||||||
with pytest.raises(JSONEncodingValueError):
|
with pytest.raises(FilterError):
|
||||||
json.loads(self.data.data_frame(None, "obs"))
|
self.data.data_frame_to_fbs_matrix("an erroneous filter", "var")
|
||||||
with pytest.raises(JSONEncodingValueError):
|
with pytest.raises(FilterError):
|
||||||
json.loads(self.data.data_frame(None, "var"))
|
filter_ = {
|
||||||
|
"filter": {
|
||||||
|
"obs": {"index": [1, 99, [200, 300]]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.data.data_frame_to_fbs_matrix(filter_["filter"], "var")
|
||||||
|
|
||||||
def test_dataframe_obs_not_implemented(self):
|
def test_dataframe_obs_not_implemented(self):
|
||||||
with self.assertRaises(ValueError) as cm:
|
with self.assertRaises(ValueError) as cm:
|
||||||
@@ -65,8 +69,3 @@ class NaNTest(unittest.TestCase):
|
|||||||
self.assertEqual(annotations["col_idx"], ["name", "n_cells", "var_with_nans"])
|
self.assertEqual(annotations["col_idx"], ["name", "n_cells", "var_with_nans"])
|
||||||
self.assertEqual(annotations["n_rows"], 100)
|
self.assertEqual(annotations["n_rows"], 100)
|
||||||
self.assertTrue(math.isnan(annotations["columns"][2][0]))
|
self.assertTrue(math.isnan(annotations["columns"][2][0]))
|
||||||
|
|
||||||
with pytest.raises(JSONEncodingValueError):
|
|
||||||
json.loads(self.data.annotation(None, "obs"))
|
|
||||||
with pytest.raises(JSONEncodingValueError):
|
|
||||||
json.loads(self.data.annotation(None, "var"))
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ from os import path
|
|||||||
import pytest
|
import pytest
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
|
import decode_fbs
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pandas import Series
|
from pandas import Series
|
||||||
|
|
||||||
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
||||||
|
from server.app.util.errors import FilterError
|
||||||
|
|
||||||
|
|
||||||
class UtilTest(unittest.TestCase):
|
class UtilTest(unittest.TestCase):
|
||||||
@@ -45,55 +47,29 @@ class UtilTest(unittest.TestCase):
|
|||||||
def test_filter_idx(self):
|
def test_filter_idx(self):
|
||||||
filter_ = {
|
filter_ = {
|
||||||
"filter": {
|
"filter": {
|
||||||
"var": {"index": [1, 99, [200, 300]]},
|
"var": {"index": [1, 99, [200, 300]]}
|
||||||
"obs": {"index": [1, 99, [1000, 2000]]},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
fbs = self.data.data_frame_to_fbs_matrix(filter_["filter"], "var")
|
||||||
self.assertEqual(data.shape, (1002, 102))
|
data = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
|
self.assertEqual(data["n_rows"], 2638)
|
||||||
def test_filter_annotation(self):
|
self.assertEqual(data["n_cols"], 102)
|
||||||
filter_ = {
|
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
|
||||||
self.assertEqual(data.shape, (470, 1838))
|
|
||||||
filter_ = {
|
|
||||||
"filter": {"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]}}
|
|
||||||
}
|
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
|
||||||
self.assertEqual(data.shape, (497, 1838))
|
|
||||||
|
|
||||||
def test_filter_annotation_no_uns(self):
|
|
||||||
filter_ = {
|
|
||||||
"filter": {
|
|
||||||
"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
|
||||||
self.assertEqual(data.shape[1], 1)
|
|
||||||
|
|
||||||
def test_filter_complex(self):
|
def test_filter_complex(self):
|
||||||
filter_ = {
|
filter_ = {
|
||||||
"filter": {
|
"filter": {
|
||||||
"var": {"index": [1, 99, [200, 300]]},
|
"var": {
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
"annotation_value": [
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
{"name": "n_cells", "min": 10}
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
],
|
],
|
||||||
"index": [1, 99, [1000, 2000]],
|
"index": [1, 99, [200, 300]]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
fbs = self.data.data_frame_to_fbs_matrix(filter_["filter"], "var")
|
||||||
self.assertEqual(data.shape, (15, 102))
|
data = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
|
self.assertEqual(data["n_rows"], 2638)
|
||||||
|
self.assertEqual(data["n_cols"], 91)
|
||||||
|
|
||||||
def test_obs_and_var_names(self):
|
def test_obs_and_var_names(self):
|
||||||
self.assertEqual(np.sum(self.data.data.var["name"].isna()), 0)
|
self.assertEqual(np.sum(self.data.data.var["name"].isna()), 0)
|
||||||
@@ -119,60 +95,42 @@ class UtilTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_layout(self):
|
def test_layout(self):
|
||||||
layout = json.loads(self.data.layout(None))
|
fbs = self.data.layout_to_fbs_matrix()
|
||||||
self.assertEqual(layout["layout"]["ndims"], 2)
|
layout = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
self.assertEqual(len(layout["layout"]["coordinates"]), 2638)
|
self.assertEqual(layout["n_cols"], 2)
|
||||||
self.assertEqual(layout["layout"]["coordinates"][0][0], 0)
|
self.assertEqual(layout["n_rows"], 2638)
|
||||||
for idx, val in enumerate(layout["layout"]["coordinates"]):
|
|
||||||
self.assertLessEqual(val[1], 1)
|
X = layout["columns"][0]
|
||||||
self.assertLessEqual(val[2], 1)
|
self.assertTrue((X >= 0).all() and (X <= 1).all())
|
||||||
|
Y = layout["columns"][1]
|
||||||
|
self.assertTrue((Y >= 0).all() and (Y <= 1).all())
|
||||||
|
|
||||||
def test_annotations(self):
|
def test_annotations(self):
|
||||||
annotations = json.loads(self.data.annotation(None, "obs"))
|
fbs = self.data.annotation_to_fbs_matrix("obs")
|
||||||
|
annotations = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
|
self.assertEqual(annotations["n_rows"], 2638)
|
||||||
|
self.assertEqual(annotations["n_cols"], 5)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
annotations["names"],
|
annotations["col_idx"],
|
||||||
["name", "n_genes", "percent_mito", "n_counts", "louvain"],
|
["name", "n_genes", "percent_mito", "n_counts", "louvain"],
|
||||||
)
|
)
|
||||||
self.assertEqual(len(annotations["data"]), 2638)
|
|
||||||
annotations = json.loads(self.data.annotation(None, "var"))
|
fbs = self.data.annotation_to_fbs_matrix("var")
|
||||||
self.assertEqual(annotations["names"], ["name", "n_cells"])
|
annotations = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
self.assertEqual(len(annotations["data"]), 1838)
|
self.assertEqual(annotations['n_rows'], 1838)
|
||||||
|
self.assertEqual(annotations['n_cols'], 2)
|
||||||
|
self.assertEqual(annotations["col_idx"], ["name", "n_cells"])
|
||||||
|
|
||||||
def test_annotation_fields(self):
|
def test_annotation_fields(self):
|
||||||
annotations = json.loads(
|
fbs = self.data.annotation_to_fbs_matrix("obs", ["n_genes", "n_counts"])
|
||||||
self.data.annotation(None, "obs", ["n_genes", "n_counts"])
|
annotations = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
)
|
self.assertEqual(annotations["n_rows"], 2638)
|
||||||
self.assertEqual(annotations["names"], ["n_genes", "n_counts"])
|
self.assertEqual(annotations['n_cols'], 2)
|
||||||
self.assertEqual(len(annotations["data"]), 2638)
|
|
||||||
annotations = json.loads(self.data.annotation(None, "var", ["name"]))
|
|
||||||
self.assertEqual(annotations["names"], ["name"])
|
|
||||||
self.assertEqual(len(annotations["data"]), 1838)
|
|
||||||
|
|
||||||
def test_filtered_annotation(self):
|
fbs = self.data.annotation_to_fbs_matrix("var", ["name"])
|
||||||
filter_ = {
|
annotations = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
"filter": {
|
self.assertEqual(annotations['n_rows'], 1838)
|
||||||
"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]},
|
self.assertEqual(annotations['n_cols'], 1)
|
||||||
"var": {
|
|
||||||
"annotation_value": [{"name": "name", "values": ["ATAD3C", "RER1"]}]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
annotations = json.loads(self.data.annotation(filter_["filter"], "obs"))
|
|
||||||
self.assertEqual(
|
|
||||||
annotations["names"],
|
|
||||||
["name", "n_genes", "percent_mito", "n_counts", "louvain"],
|
|
||||||
)
|
|
||||||
self.assertEqual(len(annotations["data"]), 497)
|
|
||||||
annotations = json.loads(self.data.annotation(filter_["filter"], "var"))
|
|
||||||
self.assertEqual(annotations["names"], ["name", "n_cells"])
|
|
||||||
self.assertEqual(len(annotations["data"]), 2)
|
|
||||||
|
|
||||||
def test_filtered_layout(self):
|
|
||||||
filter_ = {
|
|
||||||
"filter": {"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]}}
|
|
||||||
}
|
|
||||||
layout = json.loads(self.data.layout(filter_["filter"]))
|
|
||||||
self.assertEqual(len(layout["layout"]["coordinates"]), 497)
|
|
||||||
|
|
||||||
def test_diffexp_topN(self):
|
def test_diffexp_topN(self):
|
||||||
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
||||||
@@ -183,42 +141,51 @@ class UtilTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(result), 20)
|
self.assertEqual(len(result), 20)
|
||||||
|
|
||||||
def test_data_frame(self):
|
def test_data_frame(self):
|
||||||
data_frame_obs = json.loads(self.data.data_frame(None, "obs"))
|
fbs = self.data.data_frame_to_fbs_matrix(None, "var")
|
||||||
self.assertEqual(len(data_frame_obs["var"]), 1838)
|
data = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
self.assertEqual(len(data_frame_obs["obs"]), 2638)
|
self.assertEqual(data["n_rows"], 2638)
|
||||||
data_frame_var = json.loads(self.data.data_frame(None, "var"))
|
self.assertEqual(data["n_cols"], 1838)
|
||||||
self.assertEqual(len(data_frame_var["var"]), 1838)
|
|
||||||
self.assertEqual(len(data_frame_var["obs"]), 2638)
|
with self.assertRaises(ValueError):
|
||||||
|
self.data.data_frame_to_fbs_matrix(None, "obs")
|
||||||
|
|
||||||
def test_filtered_data_frame(self):
|
def test_filtered_data_frame(self):
|
||||||
|
filter_ = {
|
||||||
|
"filter": {"var": {"annotation_value": [{"name": "n_cells", "min": 100}]}}
|
||||||
|
}
|
||||||
|
fbs = self.data.data_frame_to_fbs_matrix(filter_["filter"], "var")
|
||||||
|
data = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
|
self.assertEqual(data["n_rows"], 2638)
|
||||||
|
self.assertEqual(data["n_cols"], 1040)
|
||||||
|
|
||||||
filter_ = {
|
filter_ = {
|
||||||
"filter": {"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]}}
|
"filter": {"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]}}
|
||||||
}
|
}
|
||||||
data_frame_obs = json.loads(self.data.data_frame(filter_["filter"], "obs"))
|
with self.assertRaises(FilterError):
|
||||||
self.assertEqual(len(data_frame_obs["var"]), 1838)
|
self.data.data_frame_to_fbs_matrix(filter_["filter"], "var")
|
||||||
self.assertEqual(len(data_frame_obs["obs"]), 497)
|
|
||||||
self.assertIsInstance(data_frame_obs["obs"][0], (list, tuple))
|
|
||||||
self.assertEqual(type(data_frame_obs["var"][0]), int)
|
|
||||||
data_frame_var = json.loads(self.data.data_frame(filter_["filter"], "var"))
|
|
||||||
self.assertEqual(len(data_frame_var["var"]), 1838)
|
|
||||||
self.assertEqual(len(data_frame_var["obs"]), 497)
|
|
||||||
self.assertIsInstance(data_frame_var["var"][0], (list, tuple))
|
|
||||||
self.assertEqual(type(data_frame_var["obs"][0]), int)
|
|
||||||
|
|
||||||
def test_data_single_gene(self):
|
def test_data_named_gene(self):
|
||||||
for axis in ["obs", "var"]:
|
filter_ = {
|
||||||
filter_ = {
|
"filter": {
|
||||||
"filter": {
|
"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}
|
||||||
"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
data_frame_var = json.loads(self.data.data_frame(filter_["filter"], axis))
|
}
|
||||||
if axis == "obs":
|
fbs = self.data.data_frame_to_fbs_matrix(filter_["filter"], "var")
|
||||||
self.assertEqual(type(data_frame_var["var"][0]), int)
|
data = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
self.assertIsInstance(data_frame_var["obs"][0], (list, tuple))
|
self.assertEqual(data["n_rows"], 2638)
|
||||||
elif axis == "var":
|
self.assertEqual(data["n_cols"], 1)
|
||||||
self.assertEqual(type(data_frame_var["obs"][0]), int)
|
self.assertEqual(data["col_idx"], [4])
|
||||||
self.assertIsInstance(data_frame_var["var"][0], (list, tuple))
|
|
||||||
|
filter_ = {
|
||||||
|
"filter": {
|
||||||
|
"var": {"annotation_value": [{"name": "name", "values": ["SPEN", "TYMP", "PRMT2"]}]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fbs = self.data.data_frame_to_fbs_matrix(filter_["filter"], "var")
|
||||||
|
data = decode_fbs.decode_matrix_FBS(fbs)
|
||||||
|
self.assertEqual(data["n_rows"], 2638)
|
||||||
|
self.assertEqual(data["n_cols"], 3)
|
||||||
|
self.assertTrue((data["col_idx"] == [15, 1818, 1837]).all())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ with open("server/requirements.txt") as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="cellxgene",
|
name="cellxgene",
|
||||||
version="0.6.0",
|
version="0.7.0",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
url="https://github.com/chanzuckerberg/cellxgene",
|
url="https://github.com/chanzuckerberg/cellxgene",
|
||||||
license="MIT",
|
license="MIT",
|
||||||
|
|||||||
Reference in New Issue
Block a user