Merge branch 'master' into colinmegill/geneset-prototype

This commit is contained in:
Colin Megill
2020-06-08 16:24:52 -04:00
89 changed files with 2516 additions and 820 deletions
+7 -11
View File
@@ -4,6 +4,7 @@ on:
push: push:
branches: master branches: master
pull_request: pull_request:
branches: '*'
env: env:
JEST_ENV: prod JEST_ENV: prod
@@ -31,20 +32,15 @@ jobs:
run: | run: |
pip install flake8 pip install flake8
cd client cd client
npm i "eslint" "eslint-config-airbnb" "eslint-config-prettier" "eslint-loader" "eslint-plugin-filenames" "eslint-plugin-import" "eslint-plugin-jest" "eslint-plugin-jsx-a11y" "eslint-plugin-react" "eslint-plugin-react-hooks" npm i "eslint" "eslint-config-airbnb" "eslint-config-prettier" "eslint-loader" "eslint-plugin-filenames" "eslint-plugin-import" "eslint-plugin-jest" "eslint-plugin-jsx-a11y" "eslint-plugin-react" "eslint-plugin-react-hooks" "eslint-plugin-prettier"
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
make lint-server make lint-server
# - name: Lint all with eslint - name: Lint src with eslint
# working-directory: ./client working-directory: ./client
# if: github.event_name != 'pull_request' run: |
# run: | make lint
# make lint
# - name: Lint diff with eslint
# working-directory: ./client
# if: github.event_name == 'pull_request'
# run: |
# make lint-diff
unit-test: unit-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+2
View File
@@ -51,3 +51,5 @@ data
# Jekyll # Jekyll
docs/_site/ docs/_site/
docs/Gemfile.lock docs/Gemfile.lock
client/.eslintcache
-5
View File
@@ -84,11 +84,6 @@ lint-server:
lint-client: lint-client:
cd client && $(MAKE) lint cd client && $(MAKE) lint
.PHONY: lint-diff-client
lint-diff-client:
cd client && $(MAKE) lint-diff
# CREATING DISTRIBUTION RELEASE # CREATING DISTRIBUTION RELEASE
.PHONY: pydist .PHONY: pydist
+1 -10
View File
@@ -25,16 +25,7 @@ build:
.PHONY: lint .PHONY: lint
lint: lint:
npx eslint . npx eslint ./src/
.PHONY: lint-diff
lint-diff:
# Get client diff against master, find all js/jsx files, remove the client prefex, run eslinst against result
git diff --name-only --diff-filter=d origin/master...HEAD -- ../client/ \
| grep -E "(.*)\.(jsx|js)$$" \
| sed "s/client\///" \
| $(if $(IS_DARWIN),xargs ./node_modules/.bin/eslint,xargs -r ./node_modules/.bin/eslint)
# Development convenience methods # Development convenience methods
.PHONY: start-frontend .PHONY: start-frontend
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`did launch page launched 1`] = `"<span style=\\"width: 185px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"color: transparent; position: relative; overflow: hidden; white-space: nowrap;\\">c3k<span style=\\"position: absolute; right: 0px; color: initial;\\">c3k</span></span></span>"`;
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"color: transparent; position: relative; overflow: hidden; white-space: nowrap;\\">vain<span style=\\"position: absolute; right: 0px; color: initial;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"><div></div></div><div></div>"`;
+4 -3
View File
@@ -44,9 +44,10 @@ export const cellxgeneActions = (page, utils) => ({
(rows) => (rows) =>
Object.fromEntries( Object.fromEntries(
rows.map((row) => { rows.map((row) => {
const cat = row.querySelector( const cat = row
"[data-testclass='categorical-value']" .querySelector("[data-testclass='categorical-value']")
).innerText; .getAttribute("aria-label");
const count = row.querySelector( const count = row.querySelector(
"[data-testclass='categorical-value-count']" "[data-testclass='categorical-value-count']"
).innerText; ).innerText;
+10 -7
View File
@@ -30,12 +30,12 @@ describe("did launch", () => {
const element = await utils.getOneElementInnerHTML( const element = await utils.getOneElementInnerHTML(
"[data-testid='header']" "[data-testid='header']"
); );
expect(element).toBe(data.title); expect(element).toMatchSnapshot();
}); });
test("terms of service, if they are there", async () => { test("terms of service, if they are there", async () => {
try { try {
await utils.clickOn("tos-cookies-accept", { timeout: 500 }); await utils.clickOn("tos-cookies-accept", { timeout: 3000 });
} catch { } catch {
console.warn("No terms of service footer detected."); console.warn("No terms of service footer detected.");
} }
@@ -48,10 +48,10 @@ describe("did launch", () => {
describe("metadata loads", () => { describe("metadata loads", () => {
test("categories and values from dataset appear", async () => { test("categories and values from dataset appear", async () => {
for (const label in data.categorical) { for (const label in data.categorical) {
const categoryName = await utils.getOneElementInnerText( const elem = await utils.getOneElementInnerHTML(
`[data-testid="category-${label}"]` `[data-testid="category-${label}"]`
); );
expect(categoryName).toMatch(label); expect(elem).toMatchSnapshot();
await utils.clickOn(`${label}:category-expand`); await utils.clickOn(`${label}:category-expand`);
const categories = await cxgActions.getAllCategoriesAndCounts(label); const categories = await cxgActions.getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject( expect(Object.keys(categories)).toMatchObject(
@@ -326,13 +326,16 @@ describe("ui elements don't error", () => {
describe("centroid labels", () => { describe("centroid labels", () => {
test("labels are created", async () => { test("labels are created", async () => {
await utils.clickOn("centroid-label-toggle");
const labels = Object.keys(data.categorical); const labels = Object.keys(data.categorical);
await utils.clickOn(`colorby-${labels[0]}`);
await utils.clickOn("centroid-label-toggle");
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
// Toggle colorby for each category and check to see if labels are generated // Toggle colorby for each category and check to see if labels are generated
for (let i = 0, { length } = labels; i < length; i += 1) { for (let i = 0, { length } = labels; i < length; i += 1) {
const label = labels[i]; const label = labels[i];
await utils.clickOn(`colorby-${label}`); // first label is already enabled
if (i !== 0) await utils.clickOn(`colorby-${label}`);
const generatedLabels = await utils.getAllByClass("centroid-label"); const generatedLabels = await utils.getAllByClass("centroid-label");
// Number of labels generated should be equal to size of the object // Number of labels generated should be equal to size of the object
expect(generatedLabels).toHaveLength( expect(generatedLabels).toHaveLength(
@@ -346,8 +349,8 @@ describe("centroid labels", () => {
describe("graph overlay", () => { describe("graph overlay", () => {
test("transform centroids correctly", async () => { test("transform centroids correctly", async () => {
const category = Object.keys(data.categorical)[0]; const category = Object.keys(data.categorical)[0];
await utils.clickOn("centroid-label-toggle");
await utils.clickOn(`colorby-${category}`); await utils.clickOn(`colorby-${category}`);
await utils.clickOn("centroid-label-toggle");
await utils.clickOn("mode-pan-zoom"); await utils.clickOn("mode-pan-zoom");
const panCoords = await cxgActions.calcDragCoordinates( const panCoords = await cxgActions.calcDragCoordinates(
"layout-graph", "layout-graph",
+11 -7
View File
@@ -201,16 +201,18 @@ describe.each([
}); });
async function assertCategoryExists(categoryName) { async function assertCategoryExists(categoryName) {
const handle = await utils.waitByID(`${categoryName}:category-expand`); const handle = await utils.waitByID(`${categoryName}:category-label`);
const result = await handle.evaluate((node) => node.innerText);
// slice beginning and end of category name result to account for truncation of long names const result = await handle.evaluate((node) =>
expect(result.slice(0, 10)).toBe(categoryName.slice(0, 10)); node.getAttribute("aria-label")
expect(result.slice(-10)).toBe(categoryName.slice(-10)); );
expect(result).toBe(categoryName);
} }
async function assertCategoryDoesNotExist(categoryName) { async function assertCategoryDoesNotExist(categoryName) {
const result = await page.$( const result = await page.$(
`[data-testid='${categoryName}:category-expand']` `[data-testid='${categoryName}:category-label']`
); );
expect(result).toBeNull(); expect(result).toBeNull();
} }
@@ -222,7 +224,9 @@ describe.each([
const previous = await utils.waitByID( const previous = await utils.waitByID(
`categorical-value-${categoryName}-${labelName}` `categorical-value-${categoryName}-${labelName}`
); );
expect(await previous.evaluate((node) => node.innerText)).toBe(labelName); expect(
await previous.evaluate((node) => node.getAttribute("aria-label"))
).toBe(labelName);
} }
async function assertLabelDoesNotExist(categoryName, labelName) { async function assertLabelDoesNotExist(categoryName, labelName) {
+2 -2
View File
@@ -50,8 +50,8 @@ export const puppeteerUtils = (page) => ({
return click; return click;
}, },
async getOneElementInnerHTML(selector) { async getOneElementInnerHTML(selector, options = {}) {
await page.waitForSelector(selector); await page.waitForSelector(selector, options);
return page.$eval(selector, (el) => el.innerHTML); return page.$eval(selector, (el) => el.innerHTML);
}, },
+4 -6
View File
@@ -4,6 +4,7 @@ import calcCentroid from "../../src/util/centroid";
import quantile from "../../src/util/quantile"; import quantile from "../../src/util/quantile";
import * as Universe from "../../src/util/stateManager/universe"; import * as Universe from "../../src/util/stateManager/universe";
import { matrixFBSToDataframe } from "../../src/util/stateManager/matrix";
import * as World from "../../src/util/stateManager/world"; import * as World from "../../src/util/stateManager/world";
import * as REST from "./stateManager/sampleResponses"; import * as REST from "./stateManager/sampleResponses";
import { ControlsHelpers as CH } from "../../src/util/stateManager"; import { ControlsHelpers as CH } from "../../src/util/stateManager";
@@ -22,16 +23,13 @@ describe("centroid", () => {
...universe, ...universe,
...Universe.addObsAnnotations( ...Universe.addObsAnnotations(
universe, universe,
Universe.matrixFBSToDataframe(REST.annotationsObs) matrixFBSToDataframe(REST.annotationsObs)
), ),
...Universe.addVarAnnotations( ...Universe.addVarAnnotations(
universe, universe,
Universe.matrixFBSToDataframe(REST.annotationsVar) matrixFBSToDataframe(REST.annotationsVar)
),
...Universe.addObsLayout(
universe,
Universe.matrixFBSToDataframe(REST.layoutObs)
), ),
...Universe.addObsLayout(universe, matrixFBSToDataframe(REST.layoutObs)),
}; };
world = World.createWorldFromEntireUniverse(universe); world = World.createWorldFromEntireUniverse(universe);
+199 -59
View File
@@ -38,8 +38,8 @@ describe("dataframe constructor", () => {
expect(df.rowIndex).toBeInstanceOf(Dataframe.DenseInt32Index); expect(df.rowIndex).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(df.colIndex).toBeInstanceOf(Dataframe.KeyIndex); expect(df.colIndex).toBeInstanceOf(Dataframe.KeyIndex);
expect(df.rowIndex.keys()).toEqual(new Int32Array([2, 1, 0])); expect(df.rowIndex.labels()).toEqual(new Int32Array([2, 1, 0]));
expect(df.colIndex.keys()).toEqual(["A", "B"]); expect(df.colIndex.labels()).toEqual(["A", "B"]);
expect(df.at(0, "A")).toEqual(2); expect(df.at(0, "A")).toEqual(2);
expect(df.at(2, "B")).toEqual(3); expect(df.at(2, "B")).toEqual(3);
@@ -138,7 +138,7 @@ describe("dataframe subsetting", () => {
new Float32Array([4.4, 5.5, 6.6]), new Float32Array([4.4, 5.5, 6.6]),
["red", "green", "blue"], ["red", "green", "blue"],
], ],
null, null, // identity index
new Dataframe.KeyIndex(["int32", "string", "float32", "colors"]) new Dataframe.KeyIndex(["int32", "string", "float32", "colors"])
); );
@@ -153,12 +153,12 @@ describe("dataframe subsetting", () => {
expect(dfA.col("colors").asArray()).toEqual( expect(dfA.col("colors").asArray()).toEqual(
sourceDf.col("colors").asArray() sourceDf.col("colors").asArray()
); );
expect(dfA.rowIndex.keys()).toEqual(sourceDf.rowIndex.keys()); expect(dfA.rowIndex.labels()).toEqual(sourceDf.rowIndex.labels());
expect(dfA.colIndex.keys()).toEqual(["colors"]); expect(dfA.colIndex.labels()).toEqual(["colors"]);
}); });
test("all rows, two columns", () => { test("all rows, two columns", () => {
const dfB = sourceDf.subset(null, ["colors", "float32"]); const dfB = sourceDf.subset(null, ["float32", "colors"]);
expect(dfB).toBeDefined(); expect(dfB).toBeDefined();
expect(dfB.dims).toEqual([3, 2]); expect(dfB.dims).toEqual([3, 2]);
expect(dfB.iat(0, 0)).toBeCloseTo(4.4); expect(dfB.iat(0, 0)).toBeCloseTo(4.4);
@@ -177,8 +177,8 @@ describe("dataframe subsetting", () => {
expect(dfB.col("float32").asArray()).toEqual( expect(dfB.col("float32").asArray()).toEqual(
sourceDf.col("float32").asArray() sourceDf.col("float32").asArray()
); );
expect(dfB.rowIndex.keys()).toEqual(sourceDf.rowIndex.keys()); expect(dfB.rowIndex.labels()).toEqual(sourceDf.rowIndex.labels());
expect(dfB.colIndex.keys()).toEqual(["float32", "colors"]); expect(dfB.colIndex.labels()).toEqual(["float32", "colors"]);
}); });
test("one row, all columns", () => { test("one row, all columns", () => {
@@ -189,8 +189,8 @@ describe("dataframe subsetting", () => {
expect(dfC.iat(0, 1)).toEqual("B"); expect(dfC.iat(0, 1)).toEqual("B");
expect(dfC.iat(0, 2)).toBeCloseTo(5.5); expect(dfC.iat(0, 2)).toBeCloseTo(5.5);
expect(dfC.iat(0, 3)).toEqual("green"); expect(dfC.iat(0, 3)).toEqual("green");
expect(dfC.rowIndex.keys()).toEqual(new Int32Array([1])); expect(dfC.rowIndex.labels()).toEqual(new Int32Array([1]));
expect(dfC.colIndex.keys()).toEqual(sourceDf.colIndex.keys()); expect(dfC.colIndex.labels()).toEqual(sourceDf.colIndex.labels());
}); });
test("two rows, all columns", () => { test("two rows, all columns", () => {
@@ -201,8 +201,17 @@ describe("dataframe subsetting", () => {
expect(dfD.icol(1).asArray()).toEqual(["A", "C"]); expect(dfD.icol(1).asArray()).toEqual(["A", "C"]);
expect(dfD.icol(2).asArray()).toEqual(new Float32Array([4.4, 6.6])); expect(dfD.icol(2).asArray()).toEqual(new Float32Array([4.4, 6.6]));
expect(dfD.icol(3).asArray()).toEqual(["red", "blue"]); expect(dfD.icol(3).asArray()).toEqual(["red", "blue"]);
expect(dfD.rowIndex.keys()).toEqual(new Int32Array([0, 2])); expect(dfD.rowIndex.labels()).toEqual(new Int32Array([0, 2]));
expect(dfD.colIndex.keys()).toEqual(sourceDf.colIndex.keys()); expect(dfD.colIndex.labels()).toEqual(sourceDf.colIndex.labels());
// reverse the row order
const dfDr = sourceDf.subset([2, 0], null);
expect(dfDr.icol(0).asArray()).toEqual(new Int32Array([2, 0]));
expect(dfDr.icol(1).asArray()).toEqual(["C", "A"]);
expect(dfDr.icol(2).asArray()).toEqual(new Float32Array([6.6, 4.4]));
expect(dfDr.icol(3).asArray()).toEqual(["blue", "red"]);
expect(dfDr.rowIndex.labels()).toEqual(new Int32Array([2, 0]));
expect(dfDr.colIndex.labels()).toEqual(sourceDf.colIndex.labels());
}); });
test("all rows, all columns", () => { test("all rows, all columns", () => {
@@ -213,8 +222,8 @@ describe("dataframe subsetting", () => {
expect(dfE.icol(1).asArray()).toEqual(sourceDf.icol(1).asArray()); expect(dfE.icol(1).asArray()).toEqual(sourceDf.icol(1).asArray());
expect(dfE.icol(2).asArray()).toEqual(sourceDf.icol(2).asArray()); expect(dfE.icol(2).asArray()).toEqual(sourceDf.icol(2).asArray());
expect(dfE.icol(3).asArray()).toEqual(sourceDf.icol(3).asArray()); expect(dfE.icol(3).asArray()).toEqual(sourceDf.icol(3).asArray());
expect(dfE.rowIndex.keys()).toEqual(sourceDf.rowIndex.keys()); expect(dfE.rowIndex.labels()).toEqual(sourceDf.rowIndex.labels());
expect(dfE.colIndex.keys()).toEqual(sourceDf.colIndex.keys()); expect(dfE.colIndex.labels()).toEqual(sourceDf.colIndex.labels());
}); });
test("two rows, two colums", () => { test("two rows, two colums", () => {
@@ -223,8 +232,17 @@ describe("dataframe subsetting", () => {
expect(dfF.dims).toEqual([2, 2]); expect(dfF.dims).toEqual([2, 2]);
expect(dfF.icol(0).asArray()).toEqual(new Int32Array([0, 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.icol(1).asArray()).toEqual(new Float32Array([4.4, 6.6]));
expect(dfF.rowIndex.keys()).toEqual(new Int32Array([0, 2])); expect(dfF.rowIndex.labels()).toEqual(new Int32Array([0, 2]));
expect(dfF.colIndex.keys()).toEqual(["int32", "float32"]); expect(dfF.colIndex.labels()).toEqual(["int32", "float32"]);
// reverse the row and column order
const dfFr = sourceDf.subset([2, 0], ["float32", "int32"]);
expect(dfFr).toBeDefined();
expect(dfFr.dims).toEqual([2, 2]);
expect(dfFr.icol(0).asArray()).toEqual(new Float32Array([6.6, 4.4]));
expect(dfFr.icol(1).asArray()).toEqual(new Int32Array([2, 0]));
expect(dfFr.rowIndex.labels()).toEqual(new Int32Array([2, 0]));
expect(dfFr.colIndex.labels()).toEqual(["float32", "int32"]);
}); });
test("withRowIndex", () => { test("withRowIndex", () => {
@@ -271,8 +289,49 @@ describe("dataframe subsetting", () => {
expect(dfA.dims).toEqual([2, 2]); expect(dfA.dims).toEqual([2, 2]);
expect(dfA.icol(0).asArray()).toEqual(new Int32Array([1, 2])); expect(dfA.icol(0).asArray()).toEqual(new Int32Array([1, 2]));
expect(dfA.icol(1).asArray()).toEqual(["green", "blue"]); expect(dfA.icol(1).asArray()).toEqual(["green", "blue"]);
expect(dfA.rowIndex.keys()).toEqual(new Int32Array([4, 6])); expect(dfA.rowIndex.labels()).toEqual(new Int32Array([4, 6]));
expect(dfA.colIndex.keys()).toEqual(["int32", "colors"]); expect(dfA.colIndex.labels()).toEqual(["int32", "colors"]);
});
describe("isubset", () => {
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, // identity index
new Dataframe.KeyIndex(["int32", "string", "float32", "colors"])
);
test("one row, all cols", () => {
const dfA = sourceDf.isubset([1], null);
expect(dfA.dims).toEqual([1, 4]);
expect(dfA.icol(0).asArray()).toEqual(new Int32Array([1]));
expect(dfA.icol(1).asArray()).toEqual(["B"]);
expect(dfA.icol(2).asArray()).toEqual(new Float32Array([5.5]));
expect(dfA.icol(3).asArray()).toEqual(["green"]);
});
test("all rows, two cols", () => {
const dfA = sourceDf.isubset(null, [1, 2]);
expect(dfA.dims).toEqual([3, 2]);
expect(dfA.icol(0).asArray()).toEqual(["A", "B", "C"]);
expect(dfA.icol(1).asArray()).toEqual(new Float32Array([4.4, 5.5, 6.6]));
expect(dfA.col("string")).toBe(dfA.icol(0));
expect(dfA.col("float32")).toBe(dfA.icol(1));
});
test("out of order rows", () => {
const dfA = sourceDf.isubset([2, 0], null);
expect(dfA.dims).toEqual([2, 4]);
expect(dfA.icol(0).asArray()).toEqual(new Int32Array([2, 0]));
expect(dfA.icol(1).asArray()).toEqual(["C", "A"]);
expect(dfA.icol(2).asArray()).toEqual(new Float32Array([6.6, 4.4]));
expect(dfA.icol(3).asArray()).toEqual(["blue", "red"]);
});
}); });
}); });
@@ -310,8 +369,8 @@ describe("dataframe factories", () => {
expect(dfB).not.toBe(dfA); expect(dfB).not.toBe(dfA);
expect(dfB.dims).toEqual(dfA.dims); expect(dfB.dims).toEqual(dfA.dims);
expect(dfB).toHaveLength(dfA.length); expect(dfB).toHaveLength(dfA.length);
expect(dfB.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(dfB.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
expect(dfB.colIndex.keys()).toEqual(dfA.colIndex.keys()); expect(dfB.colIndex.labels()).toEqual(dfA.colIndex.labels());
for (let i = 0, l = dfB.dims[1]; i < l; i += 1) { for (let i = 0, l = dfB.dims[1]; i < l; i += 1) {
expect(dfB.icol(i).asArray()).toEqual(dfA.icol(i).asArray()); expect(dfB.icol(i).asArray()).toEqual(dfA.icol(i).asArray());
} }
@@ -336,9 +395,9 @@ describe("dataframe factories", () => {
expect(dfA.icol(1).asArray()).toEqual([true, false]); expect(dfA.icol(1).asArray()).toEqual([true, false]);
expect(dfA.icol(2).asArray()).toEqual([1, 0]); expect(dfA.icol(2).asArray()).toEqual([1, 0]);
expect(dfA.col("numbers").asArray()).toEqual([1, 0]); expect(dfA.col("numbers").asArray()).toEqual([1, 0]);
expect(dfA.colIndex.keys()).toEqual(["colors", "bools", "numbers"]); expect(dfA.colIndex.labels()).toEqual(["colors", "bools", "numbers"]);
expect(df.colIndex.keys()).toEqual(["colors", "bools"]); expect(df.colIndex.labels()).toEqual(["colors", "bools"]);
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
test("DenseInt32Index", () => { test("DenseInt32Index", () => {
@@ -361,9 +420,9 @@ describe("dataframe factories", () => {
expect(dfA.col(74).asArray()).toEqual(["red", "blue"]); expect(dfA.col(74).asArray()).toEqual(["red", "blue"]);
expect(dfA.col(75).asArray()).toEqual([true, false]); expect(dfA.col(75).asArray()).toEqual([true, false]);
expect(dfA.col(72).asArray()).toEqual([1, 0]); expect(dfA.col(72).asArray()).toEqual([1, 0]);
expect(dfA.colIndex.keys()).toEqual(new Int32Array([74, 75, 72])); expect(dfA.colIndex.labels()).toEqual(new Int32Array([74, 75, 72]));
expect(df.colIndex.keys()).toEqual(new Int32Array([74, 75])); expect(df.colIndex.labels()).toEqual(new Int32Array([74, 75]));
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
test("DenseInt32Index promote", () => { test("DenseInt32Index promote", () => {
@@ -386,9 +445,9 @@ describe("dataframe factories", () => {
expect(dfA.col(74).asArray()).toEqual(["red", "blue"]); expect(dfA.col(74).asArray()).toEqual(["red", "blue"]);
expect(dfA.col(75).asArray()).toEqual([true, false]); expect(dfA.col(75).asArray()).toEqual([true, false]);
expect(dfA.col(999).asArray()).toEqual([1, 0]); expect(dfA.col(999).asArray()).toEqual([1, 0]);
expect(dfA.colIndex.keys()).toEqual(new Int32Array([74, 75, 999])); expect(dfA.colIndex.labels()).toEqual(new Int32Array([74, 75, 999]));
expect(df.colIndex.keys()).toEqual(new Int32Array([74, 75])); expect(df.colIndex.labels()).toEqual(new Int32Array([74, 75]));
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
test("IdentityInt32Index with last", () => { test("IdentityInt32Index with last", () => {
@@ -411,9 +470,9 @@ describe("dataframe factories", () => {
expect(dfA.col(0).asArray()).toEqual(["red", "blue"]); expect(dfA.col(0).asArray()).toEqual(["red", "blue"]);
expect(dfA.col(1).asArray()).toEqual([true, false]); expect(dfA.col(1).asArray()).toEqual([true, false]);
expect(dfA.col(2).asArray()).toEqual([1, 0]); expect(dfA.col(2).asArray()).toEqual([1, 0]);
expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1, 2])); expect(dfA.colIndex.labels()).toEqual(new Int32Array([0, 1, 2]));
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1])); expect(df.colIndex.labels()).toEqual(new Int32Array([0, 1]));
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
test("IdentityInt32Index promote", () => { test("IdentityInt32Index promote", () => {
@@ -436,9 +495,9 @@ describe("dataframe factories", () => {
expect(dfA.col(0).asArray()).toEqual(["red", "blue"]); expect(dfA.col(0).asArray()).toEqual(["red", "blue"]);
expect(dfA.col(1).asArray()).toEqual([true, false]); expect(dfA.col(1).asArray()).toEqual([true, false]);
expect(dfA.col(99).asArray()).toEqual([1, 0]); expect(dfA.col(99).asArray()).toEqual([1, 0]);
expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1, 99])); expect(dfA.colIndex.labels()).toEqual(new Int32Array([0, 1, 99]));
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1])); expect(df.colIndex.labels()).toEqual(new Int32Array([0, 1]));
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
describe("handle column dimensions correctly", () => { describe("handle column dimensions correctly", () => {
@@ -517,25 +576,25 @@ describe("dataframe factories", () => {
const dfLikeA = dfEmpty.withColsFrom(dfA); const dfLikeA = dfEmpty.withColsFrom(dfA);
expect(dfLikeA).toBeDefined(); expect(dfLikeA).toBeDefined();
expect(dfLikeA.dims).toEqual(dfA.dims); expect(dfLikeA.dims).toEqual(dfA.dims);
expect(dfLikeA.colIndex.keys()).toEqual(dfA.colIndex.keys()); expect(dfLikeA.colIndex.labels()).toEqual(dfA.colIndex.labels());
expect(dfLikeA.rowIndex).toEqual(dfA.rowIndex); expect(dfLikeA.rowIndex).toEqual(dfA.rowIndex);
expect(dfLikeA.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(dfLikeA.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
expect(dfLikeA.icol(0).asArray()).toEqual(dfA.icol(0).asArray()); expect(dfLikeA.icol(0).asArray()).toEqual(dfA.icol(0).asArray());
const dfAlsoLikeA = dfA.withColsFrom(dfEmpty); const dfAlsoLikeA = dfA.withColsFrom(dfEmpty);
expect(dfAlsoLikeA).toBeDefined(); expect(dfAlsoLikeA).toBeDefined();
expect(dfAlsoLikeA.dims).toEqual(dfA.dims); expect(dfAlsoLikeA.dims).toEqual(dfA.dims);
expect(dfAlsoLikeA.colIndex.keys()).toEqual(dfA.colIndex.keys()); expect(dfAlsoLikeA.colIndex.labels()).toEqual(dfA.colIndex.labels());
expect(dfAlsoLikeA.rowIndex).toEqual(dfA.rowIndex); expect(dfAlsoLikeA.rowIndex).toEqual(dfA.rowIndex);
expect(dfAlsoLikeA.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(dfAlsoLikeA.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
expect(dfAlsoLikeA.icol(0).asArray()).toEqual(dfA.icol(0).asArray()); expect(dfAlsoLikeA.icol(0).asArray()).toEqual(dfA.icol(0).asArray());
const dfC = dfA.withColsFrom(dfB); const dfC = dfA.withColsFrom(dfB);
expect(dfC).toBeDefined(); expect(dfC).toBeDefined();
expect(dfC.dims).toEqual([2, 2]); expect(dfC.dims).toEqual([2, 2]);
expect(dfC.colIndex.keys()).toEqual(["colors", "bools"]); expect(dfC.colIndex.labels()).toEqual(["colors", "bools"]);
expect(dfC.rowIndex).toEqual(dfA.rowIndex); expect(dfC.rowIndex).toEqual(dfA.rowIndex);
expect(dfC.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(dfC.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
expect(dfC.col("colors").asArray()).toEqual(["red", "blue"]); expect(dfC.col("colors").asArray()).toEqual(["red", "blue"]);
expect(dfC.col("bools").asArray()).toEqual([true, false]); expect(dfC.col("bools").asArray()).toEqual([true, false]);
}); });
@@ -562,21 +621,21 @@ describe("dataframe factories", () => {
const dfX = dfEmpty.withColsFrom(dfB, ["colors", "bools"]); const dfX = dfEmpty.withColsFrom(dfB, ["colors", "bools"]);
expect(dfX).toBeDefined(); expect(dfX).toBeDefined();
expect(dfX.dims).toEqual([2, 2]); expect(dfX.dims).toEqual([2, 2]);
expect(dfX.colIndex.keys()).toEqual(["colors", "bools"]); expect(dfX.colIndex.labels()).toEqual(["colors", "bools"]);
expect(dfX.rowIndex).toEqual(dfB.rowIndex); expect(dfX.rowIndex).toEqual(dfB.rowIndex);
expect(dfX.icol(0).asArray()).toEqual(dfB.icol(0).asArray()); expect(dfX.icol(0).asArray()).toEqual(dfB.icol(0).asArray());
const dfY = dfA.withColsFrom(dfB, ["numbers"]); const dfY = dfA.withColsFrom(dfB, ["numbers"]);
expect(dfY).toBeDefined(); expect(dfY).toBeDefined();
expect(dfY.dims).toEqual([2, 2]); expect(dfY.dims).toEqual([2, 2]);
expect(dfY.colIndex.keys()).toEqual(["colors", "numbers"]); expect(dfY.colIndex.labels()).toEqual(["colors", "numbers"]);
expect(dfY.rowIndex).toEqual(dfA.rowIndex); expect(dfY.rowIndex).toEqual(dfA.rowIndex);
expect(dfY.icol(0).asArray()).toEqual(dfA.icol(0).asArray()); expect(dfY.icol(0).asArray()).toEqual(dfA.icol(0).asArray());
const dfZ = dfA.withColsFrom(dfEmpty, []); const dfZ = dfA.withColsFrom(dfEmpty, []);
expect(dfZ).toBeDefined(); expect(dfZ).toBeDefined();
expect(dfZ.dims).toEqual(dfA.dims); expect(dfZ.dims).toEqual(dfA.dims);
expect(dfZ.colIndex.keys()).toEqual(dfA.colIndex.keys()); expect(dfZ.colIndex.labels()).toEqual(dfA.colIndex.labels());
expect(dfZ.rowIndex).toEqual(dfA.rowIndex); expect(dfZ.rowIndex).toEqual(dfA.rowIndex);
expect(dfZ.icol(0).asArray()).toEqual(dfA.icol(0).asArray()); expect(dfZ.icol(0).asArray()).toEqual(dfA.icol(0).asArray());
@@ -604,7 +663,7 @@ describe("dataframe factories", () => {
const dfX = dfA.withColsFrom(dfB, { colors: "_colors", bools: "_bools" }); const dfX = dfA.withColsFrom(dfB, { colors: "_colors", bools: "_bools" });
expect(dfX).toBeDefined(); expect(dfX).toBeDefined();
expect(dfX.dims).toEqual([2, 3]); expect(dfX.dims).toEqual([2, 3]);
expect(dfX.colIndex.keys()).toEqual(["colors", "_colors", "_bools"]); expect(dfX.colIndex.labels()).toEqual(["colors", "_colors", "_bools"]);
expect(dfX.rowIndex).toEqual(dfA.rowIndex); expect(dfX.rowIndex).toEqual(dfA.rowIndex);
expect(dfX.icol(0).asArray()).toEqual(dfA.icol(0).asArray()); expect(dfX.icol(0).asArray()).toEqual(dfA.icol(0).asArray());
expect(dfX.col("_colors").asArray()).toBe(dfB.col("colors").asArray()); expect(dfX.col("_colors").asArray()).toBe(dfB.col("colors").asArray());
@@ -630,9 +689,9 @@ describe("dataframe factories", () => {
expect(dfA.icol(0).asArray()).toEqual([true, false]); expect(dfA.icol(0).asArray()).toEqual([true, false]);
expect(dfA.icol(1).asArray()).toEqual([1, 0]); expect(dfA.icol(1).asArray()).toEqual([1, 0]);
expect(dfA.col("numbers").asArray()).toEqual([1, 0]); expect(dfA.col("numbers").asArray()).toEqual([1, 0]);
expect(dfA.colIndex.keys()).toEqual(["bools", "numbers"]); expect(dfA.colIndex.labels()).toEqual(["bools", "numbers"]);
expect(df.colIndex.keys()).toEqual(["colors", "bools", "numbers"]); expect(df.colIndex.labels()).toEqual(["colors", "bools", "numbers"]);
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
test("IdentityInt32Index drop first", () => { test("IdentityInt32Index drop first", () => {
@@ -654,9 +713,9 @@ describe("dataframe factories", () => {
expect(dfA.icol(1).asArray()).toEqual([1, 0]); expect(dfA.icol(1).asArray()).toEqual([1, 0]);
expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray()); expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray());
expect(df.col(2).asArray()).toEqual(dfA.col(2).asArray()); expect(df.col(2).asArray()).toEqual(dfA.col(2).asArray());
expect(dfA.colIndex.keys()).toEqual(new Int32Array([1, 2])); expect(dfA.colIndex.labels()).toEqual(new Int32Array([1, 2]));
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1, 2])); expect(df.colIndex.labels()).toEqual(new Int32Array([0, 1, 2]));
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
test("IdentityInt32Index drop last", () => { test("IdentityInt32Index drop last", () => {
@@ -678,9 +737,9 @@ describe("dataframe factories", () => {
expect(dfA.icol(1).asArray()).toEqual([true, false]); expect(dfA.icol(1).asArray()).toEqual([true, false]);
expect(df.col(0).asArray()).toEqual(dfA.col(0).asArray()); expect(df.col(0).asArray()).toEqual(dfA.col(0).asArray());
expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray()); expect(df.col(1).asArray()).toEqual(dfA.col(1).asArray());
expect(dfA.colIndex.keys()).toEqual(new Int32Array([0, 1])); expect(dfA.colIndex.labels()).toEqual(new Int32Array([0, 1]));
expect(df.colIndex.keys()).toEqual(new Int32Array([0, 1, 2])); expect(df.colIndex.labels()).toEqual(new Int32Array([0, 1, 2]));
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
test("DenseInt32Index", () => { test("DenseInt32Index", () => {
@@ -702,9 +761,9 @@ describe("dataframe factories", () => {
expect(dfA.icol(1).asArray()).toEqual([1, 0]); expect(dfA.icol(1).asArray()).toEqual([1, 0]);
expect(dfA.col(100).asArray()).toEqual([1, 0]); expect(dfA.col(100).asArray()).toEqual([1, 0]);
expect(dfA.col(102).asArray()).toEqual(["red", "blue"]); expect(dfA.col(102).asArray()).toEqual(["red", "blue"]);
expect(dfA.colIndex.keys()).toEqual(new Int32Array([102, 100])); expect(dfA.colIndex.labels()).toEqual(new Int32Array([102, 100]));
expect(df.colIndex.keys()).toEqual(new Int32Array([102, 101, 100])); expect(df.colIndex.labels()).toEqual(new Int32Array([102, 101, 100]));
expect(df.rowIndex.keys()).toEqual(dfA.rowIndex.keys()); expect(df.rowIndex.labels()).toEqual(dfA.rowIndex.labels());
}); });
}); });
@@ -766,8 +825,8 @@ describe("dataframe factories", () => {
new Dataframe.KeyIndex(["A", "B"]) new Dataframe.KeyIndex(["A", "B"])
); );
const dfB = dfA.renameCol("B", "C"); const dfB = dfA.renameCol("B", "C");
expect(dfA.colIndex.keys()).toEqual(["A", "B"]); expect(dfA.colIndex.labels()).toEqual(["A", "B"]);
expect(dfB.colIndex.keys()).toEqual(["A", "C"]); expect(dfB.colIndex.labels()).toEqual(["A", "C"]);
expect(dfA.dims).toMatchObject(dfB.dims); expect(dfA.dims).toMatchObject(dfB.dims);
expect(dfA.columns()).toMatchObject(dfB.columns()); expect(dfA.columns()).toMatchObject(dfB.columns());
}); });
@@ -857,3 +916,84 @@ describe("dataframe col", () => {
expect(df.col("B").indexOf(true)).toBeUndefined(); expect(df.col("B").indexOf(true)).toBeUndefined();
}); });
}); });
describe("label indexing", () => {
test("IdentityInt32Index", () => {
const idx = new Dataframe.IdentityInt32Index(12); // [0, 12)
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
expect(idx.labels()).toEqual(new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]));
expect(idx.getLabel(1)).toEqual(1);
expect(idx.getOffset(1)).toEqual(1);
expect(idx.getOffsets([1,3])).toEqual([1,3])
expect(idx.getLabels([1, 3])).toEqual([1,3])
expect(idx.size()).toEqual(12);
expect(idx.subset([2]).labels()).toEqual([2]);
expect(idx.subset([2, 3, 4]).labels()).toEqual(new Int32Array([2, 3, 4]));
expect(idx.subset([0, 1, 2, 3]).labels()).toEqual(new Int32Array([0, 1, 2, 3]));
expect(idx.isubset([2]).labels()).toEqual([2]);
expect(idx.isubset([2, 3, 4]).labels()).toEqual(new Int32Array([2, 3, 4]));
expect(idx.isubset([0, 1, 2, 3]).labels()).toEqual(new Int32Array([0, 1, 2, 3]));
expect(idx.subset([0, 1, 2, 3, 4])).toBeInstanceOf(Dataframe.IdentityInt32Index);
expect(idx.subset([2, 1, 0])).toBeInstanceOf(Dataframe.IdentityInt32Index);
expect(idx.subset([1, 2, 3, 4])).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(idx.subset([0, 1, 3, 4])).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(idx.subset([0, 1, 2, 3, 10])).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(idx.subset([4, 3, 2, 1])).toBeInstanceOf(Dataframe.DenseInt32Index);
expect(idx.subset([4])).toBeInstanceOf(Dataframe.KeyIndex);
expect(idx.withLabel(99).labels()).toEqual(new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 99]));
expect(idx.dropLabel(0).labels()).toEqual(new Int32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]));
expect(idx.dropLabel(11).labels()).toEqual(new Int32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
expect(idx.dropLabel(5).labels()).toEqual(new Int32Array([0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11]));
});
test("DenseInt32Index", () => {
const idx = new Dataframe.DenseInt32Index([99, 1002, 48, 0, 22]);
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
expect(idx.labels()).toEqual(new Int32Array([99, 1002, 48, 0, 22]));
expect(idx.size()).toEqual(5);
expect(idx.getOffset(1002)).toEqual(1);
expect(idx.getOffset(0)).toEqual(3);
expect(idx.getLabel(0)).toEqual(99);
expect(idx.getLabels(new Int32Array([2, 4]))).toEqual(new Int32Array([48, 22]));
expect(idx.getLabels([2, 4])).toEqual([48, 22]);
expect(idx.getOffsets([0, 48])).toEqual([3, 2]);
expect(idx.subset([1002, 0, 99]).labels()).toEqual(new Int32Array([1002, 0, 99]))
expect(idx.getOffsets(idx.subset([1002, 0, 99]).labels())).toEqual(new Int32Array([1, 3, 0]));
expect(idx.isubset([4, 1, 2]).labels()).toEqual(new Int32Array([22, 1002, 48]));
expect(idx.withLabel(88).labels()).toEqual(new Int32Array([99, 1002, 48, 0, 22, 88]));
expect(idx.withLabel(88).getOffset(88)).toEqual(5);
expect(idx.dropLabel(48).labels()).toEqual(new Int32Array([99, 1002, 0, 22]));
});
test("KeyIndex", () => {
const idx = new Dataframe.KeyIndex(["red", "green", "blue"]);
expect(Dataframe.isLabelIndex(idx)).toBeTruthy();
expect(idx.labels()).toEqual(["red", "green", "blue"]);
expect(idx.size()).toEqual(3);
expect(idx.getOffset("blue")).toEqual(2);
expect(idx.getLabel(1)).toEqual("green");
expect(idx.subset(["green"]).labels()).toEqual(["green"]);
expect(idx.subset(["green", "red"]).labels()).toEqual(["green", "red"]);
expect(idx.isubset([2, 1, 0]).labels()).toEqual(["blue", "green", "red"]);
expect(idx.withLabel("yo").labels()).toEqual(["red", "green", "blue", "yo"]);
expect(idx.withLabel("yo").getOffset("yo")).toEqual(3);
expect(idx.dropLabel("blue").labels()).toEqual(["red", "green"]);
});
})
@@ -4,11 +4,21 @@ test controls helpers
import { subsetAndResetGeneLists } from "../../../src/util/stateManager/controlsHelpers"; import { subsetAndResetGeneLists } from "../../../src/util/stateManager/controlsHelpers";
import * as globals from "../../../src/globals"; import * as globals from "../../../src/globals";
describe("controls helpers", () => { describe("controls helpers", () => {
test("subsetAndResetGeneLists", () => { test("subsetAndResetGeneLists", () => {
const geneList = [...Array(150).keys()].map( const geneList = [];
() => Math.random().toString(36).substring(2, 6) // random string of 4 characters const genRandGene = () => Math.random().toString(36).substring(2, 6);
);
// build a unique set of genes
for (let i = 0; i < 150; i += 1) {
let randGene = genRandGene();
while (geneList.includes(randGene)) randGene = genRandGene();
geneList.push(randGene);
}
// insert duplicates
geneList[0] = "dupl";
geneList[20] = "dupl";
const state = { const state = {
userDefinedGenes: geneList.slice(0, 20), userDefinedGenes: geneList.slice(0, 20),
diffexpGenes: geneList.slice(20), diffexpGenes: geneList.slice(20),
@@ -16,12 +26,14 @@ describe("controls helpers", () => {
const [newUserDefinedGenes, newDiffExpGenes] = subsetAndResetGeneLists( const [newUserDefinedGenes, newDiffExpGenes] = subsetAndResetGeneLists(
state state
); );
const expectedNewUserDefinedGenes = [
...geneList.slice(0, 20),
...geneList.slice(21)
].slice(0, globals.maxGenes);
expect(globals.maxUserDefinedGenes).toBeLessThan(globals.maxGenes); expect(globals.maxUserDefinedGenes).toBeLessThan(globals.maxGenes);
expect(geneList.length).toBeGreaterThan(globals.maxGenes); expect(geneList.length).toBeGreaterThan(globals.maxGenes);
expect(newUserDefinedGenes).toHaveLength(globals.maxGenes); expect(newUserDefinedGenes).toHaveLength(globals.maxGenes);
expect(newUserDefinedGenes).toStrictEqual( expect(newUserDefinedGenes).toStrictEqual(expectedNewUserDefinedGenes);
geneList.slice(0, globals.maxGenes)
);
expect(newDiffExpGenes).toStrictEqual([]); expect(newDiffExpGenes).toStrictEqual([]);
}); });
}); });
@@ -27,7 +27,7 @@ describe("encode/decode", () => {
const dfWithColIdx = new Dataframe([3, 4], columns, null, colIndex); const dfWithColIdx = new Dataframe([3, 4], columns, null, colIndex);
const dfB = decodeMatrixFBS(encodeMatrixFBS(dfWithColIdx)); const dfB = decodeMatrixFBS(encodeMatrixFBS(dfWithColIdx));
expect([dfB.nRows, dfB.nCols]).toEqual(dfWithColIdx.dims); expect([dfB.nRows, dfB.nCols]).toEqual(dfWithColIdx.dims);
expect(dfB.colIdx).toEqual(colIndex.keys()); expect(dfB.colIdx).toEqual(colIndex.labels());
expect(dfB.rowIdx).toBeNull(); expect(dfB.rowIdx).toBeNull();
expect(dfB.columns).toEqual(columns); expect(dfB.columns).toEqual(columns);
}); });
@@ -1,4 +1,5 @@
import * as Universe from "../../../src/util/stateManager/universe"; import * as Universe from "../../../src/util/stateManager/universe";
import { matrixFBSToDataframe } from "../../../src/util/stateManager/matrix";
import * as Dataframe from "../../../src/util/dataframe"; import * as Dataframe from "../../../src/util/dataframe";
import * as REST from "./sampleResponses"; import * as REST from "./sampleResponses";
@@ -51,16 +52,13 @@ describe("createUniverseFromResponse", () => {
...universe, ...universe,
...Universe.addObsAnnotations( ...Universe.addObsAnnotations(
universe, universe,
Universe.matrixFBSToDataframe(REST.annotationsObs) matrixFBSToDataframe(REST.annotationsObs)
), ),
...Universe.addVarAnnotations( ...Universe.addVarAnnotations(
universe, universe,
Universe.matrixFBSToDataframe(REST.annotationsVar) matrixFBSToDataframe(REST.annotationsVar)
),
...Universe.addObsLayout(
universe,
Universe.matrixFBSToDataframe(REST.layoutObs)
), ),
...Universe.addObsLayout(universe, matrixFBSToDataframe(REST.layoutObs)),
}; };
expect(universe).toMatchObject( expect(universe).toMatchObject(
@@ -80,7 +78,7 @@ describe("createUniverseFromResponse", () => {
REST.schema.schema.annotations.obs.columns.length, REST.schema.schema.annotations.obs.columns.length,
]); ]);
expect(universe.obsLayout.dims).toEqual([nObs, 2]); expect(universe.obsLayout.dims).toEqual([nObs, 2]);
expect(universe.obsLayout.colIndex.keys()).toEqual( expect(universe.obsLayout.colIndex.labels()).toEqual(
universe.schema.layout.obs[0].dims universe.schema.layout.obs[0].dims
); );
expect(universe.varAnnotations.dims).toEqual([ expect(universe.varAnnotations.dims).toEqual([
@@ -1,5 +1,6 @@
import _ from "lodash"; import _ from "lodash";
import * as Universe from "../../../src/util/stateManager/universe"; import * as Universe from "../../../src/util/stateManager/universe";
import { matrixFBSToDataframe } from "../../../src/util/stateManager/matrix";
import * as World from "../../../src/util/stateManager/world"; import * as World from "../../../src/util/stateManager/world";
import * as Dataframe from "../../../src/util/dataframe"; import * as Dataframe from "../../../src/util/dataframe";
import Crossfilter from "../../../src/util/typedCrossfilter"; import Crossfilter from "../../../src/util/typedCrossfilter";
@@ -26,16 +27,13 @@ const defaultBigBang = () => {
...universe, ...universe,
...Universe.addObsAnnotations( ...Universe.addObsAnnotations(
universe, universe,
Universe.matrixFBSToDataframe(REST.annotationsObs) matrixFBSToDataframe(REST.annotationsObs)
), ),
...Universe.addVarAnnotations( ...Universe.addVarAnnotations(
universe, universe,
Universe.matrixFBSToDataframe(REST.annotationsVar) matrixFBSToDataframe(REST.annotationsVar)
),
...Universe.addObsLayout(
universe,
Universe.matrixFBSToDataframe(REST.layoutObs)
), ),
...Universe.addObsLayout(universe, matrixFBSToDataframe(REST.layoutObs)),
}; };
/* create world */ /* create world */
@@ -59,9 +57,9 @@ describe("createWorldFromEntireUniverse", () => {
const universe = Universe.createUniverseFromResponse( const universe = Universe.createUniverseFromResponse(
_.cloneDeep(REST.config), _.cloneDeep(REST.config),
_.cloneDeep(REST.schema), _.cloneDeep(REST.schema),
Universe.matrixFBSToDataframe(_.cloneDeep(REST.annotationsObs)), matrixFBSToDataframe(_.cloneDeep(REST.annotationsObs)),
Universe.matrixFBSToDataframe(_.cloneDeep(REST.annotationsVar)), matrixFBSToDataframe(_.cloneDeep(REST.annotationsVar)),
Universe.matrixFBSToDataframe(_.cloneDeep(REST.layoutObs)) matrixFBSToDataframe(_.cloneDeep(REST.layoutObs))
); );
expect(universe).toBeDefined(); expect(universe).toBeDefined();
@@ -144,16 +142,16 @@ describe("createWorldFromCurrentSelection", () => {
}) })
); );
expect(world.obsAnnotations.rowIndex.keys()).toEqual( expect(world.obsAnnotations.rowIndex.labels()).toEqual(
new Int32Array(matchingIndices) new Int32Array(matchingIndices)
); );
expect(world.obsAnnotations.colIndex.keys()).toEqual( expect(world.obsAnnotations.colIndex.labels()).toEqual(
universe.obsAnnotations.colIndex.keys() universe.obsAnnotations.colIndex.labels()
); );
expect(world.obsLayout.rowIndex.keys()).toEqual( expect(world.obsLayout.rowIndex.labels()).toEqual(
new Int32Array(matchingIndices) new Int32Array(matchingIndices)
); );
expect(world.obsLayout.colIndex.keys()).toEqual( expect(world.obsLayout.colIndex.labels()).toEqual(
world.schema.layout.obs[0].dims world.schema.layout.obs[0].dims
); );
}); });
+1 -7
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
root: true, root: true,
parser: "babel-eslint", parser: "babel-eslint",
extends: ["airbnb", "prettier", "prettier/react"], extends: ["airbnb", "plugin:prettier/recommended", "prettier/react"],
env: { browser: true, commonjs: true, es6: true }, env: { browser: true, commonjs: true, es6: true },
globals: { expect: true }, globals: { expect: true },
parserOptions: { parserOptions: {
@@ -21,13 +21,7 @@ module.exports = {
"react/jsx-filename-extension": "off", "react/jsx-filename-extension": "off",
"comma-dangle": "off", "comma-dangle": "off",
"no-underscore-dangle": "off", "no-underscore-dangle": "off",
quotes: ["error", "double"],
"implicit-arrow-linebreak": "off", "implicit-arrow-linebreak": "off",
"operator-linebreak": [
"error",
"after",
{ overrides: { "?": "before", ":": "before" } },
],
"no-console": "off", "no-console": "off",
"spaced-comment": ["error", "always", { exceptions: ["*"] }], "spaced-comment": ["error", "always", { exceptions: ["*"] }],
"no-param-reassign": "off", "no-param-reassign": "off",
@@ -0,0 +1,3 @@
module.exports = {
"./src/**/*.js": "eslint --fix",
};
+599
View File
@@ -2547,6 +2547,15 @@
} }
} }
}, },
"@samverschueren/stream-to-observable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
"integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==",
"dev": true,
"requires": {
"any-observable": "^0.3.0"
}
},
"@sentry/cli": { "@sentry/cli": {
"version": "1.52.3", "version": "1.52.3",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.52.3.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.52.3.tgz",
@@ -2744,6 +2753,12 @@
"integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==", "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==",
"dev": true "dev": true
}, },
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
"@types/prettier": { "@types/prettier": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz",
@@ -3263,6 +3278,12 @@
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==",
"dev": true "dev": true
}, },
"any-observable": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
"integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
"dev": true
},
"anymatch": { "anymatch": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
@@ -5378,6 +5399,60 @@
"restore-cursor": "^3.1.0" "restore-cursor": "^3.1.0"
} }
}, },
"cli-truncate": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
"requires": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
}
}
}
},
"cli-width": { "cli-width": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
@@ -5585,6 +5660,12 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true "dev": true
}, },
"compare-versions": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
"dev": true
},
"component-emitter": { "component-emitter": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@@ -6517,6 +6598,12 @@
"mimic-response": "^2.0.0" "mimic-response": "^2.0.0"
} }
}, },
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
"dev": true
},
"deep-equal": { "deep-equal": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
@@ -6548,6 +6635,23 @@
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true "dev": true
}, },
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
"dev": true,
"requires": {
"clone": "^1.0.2"
},
"dependencies": {
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
}
}
},
"define-properties": { "define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@@ -6911,6 +7015,12 @@
"integrity": "sha512-zcUd1p/7yzTSdWkCTrqGvbnEOASy96d0RJL/lc5BDJoO23Z3G/VHd0yIPbguDU9n8QNUTCigLO7oEdtOb7fp2A==", "integrity": "sha512-zcUd1p/7yzTSdWkCTrqGvbnEOASy96d0RJL/lc5BDJoO23Z3G/VHd0yIPbguDU9n8QNUTCigLO7oEdtOb7fp2A==",
"dev": true "dev": true
}, },
"elegant-spinner": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-2.0.0.tgz",
"integrity": "sha512-5YRYHhvhYzV/FC4AiMdeSIg3jAYGq9xFvbhZMpPlJoBsfYgrw2DSCYeXfat6tYBu45PWiyRr3+flaCPPmviPaA==",
"dev": true
},
"elliptic": { "elliptic": {
"version": "6.5.2", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
@@ -6976,6 +7086,15 @@
} }
} }
}, },
"enquirer": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.5.tgz",
"integrity": "sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA==",
"dev": true,
"requires": {
"ansi-colors": "^3.2.1"
}
},
"entities": { "entities": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
@@ -7411,6 +7530,15 @@
} }
} }
}, },
"eslint-plugin-prettier": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz",
"integrity": "sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.19.0", "version": "7.19.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz",
@@ -7907,6 +8035,12 @@
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
"dev": true "dev": true
}, },
"fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"fast-json-stable-stringify": { "fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -8290,6 +8424,15 @@
"locate-path": "^3.0.0" "locate-path": "^3.0.0"
} }
}, },
"find-versions": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz",
"integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==",
"dev": true,
"requires": {
"semver-regex": "^2.0.0"
}
},
"findup-sync": { "findup-sync": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
@@ -8694,6 +8837,12 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true "dev": true
}, },
"get-own-enumerable-property-symbols": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
"integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
"dev": true
},
"get-stdin": { "get-stdin": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
@@ -9301,6 +9450,100 @@
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
"dev": true "dev": true
}, },
"husky": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz",
"integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
"ci-info": "^2.0.0",
"compare-versions": "^3.6.0",
"cosmiconfig": "^6.0.0",
"find-versions": "^3.2.0",
"opencollective-postinstall": "^2.0.2",
"pkg-dir": "^4.2.0",
"please-upgrade-node": "^3.2.0",
"slash": "^3.0.0",
"which-pm-runs": "^1.0.0"
},
"dependencies": {
"cosmiconfig": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
"integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
"dev": true,
"requires": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.1.0",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.7.2"
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"parse-json": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
"integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1",
"lines-and-columns": "^1.1.6"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"requires": {
"find-up": "^4.0.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",
@@ -9869,6 +10112,12 @@
"has": "^1.0.3" "has": "^1.0.3"
} }
}, },
"is-regexp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
"dev": true
},
"is-resolvable": { "is-resolvable": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
@@ -11739,6 +11988,208 @@
"type-check": "~0.3.2" "type-check": "~0.3.2"
} }
}, },
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
"lint-staged": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.4.tgz",
"integrity": "sha512-doTMGKXQAT34c3S3gwDrTnXmCZp/z1/92D8suPqqh755sKPT18ew1NoPNHxJdrvv1D4WrJ7CEnx79Ns3EdEFbg==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
"commander": "^5.1.0",
"cosmiconfig": "^6.0.0",
"debug": "^4.1.1",
"dedent": "^0.7.0",
"execa": "^4.0.1",
"listr2": "^2.0.2",
"log-symbols": "^4.0.0",
"micromatch": "^4.0.2",
"normalize-path": "^3.0.0",
"please-upgrade-node": "^3.2.0",
"string-argv": "0.3.1",
"stringify-object": "^3.3.0"
},
"dependencies": {
"commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"dev": true
},
"cosmiconfig": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
"integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
"dev": true,
"requires": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.1.0",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.7.2"
}
},
"cross-spawn": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz",
"integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"execa": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.0.1.tgz",
"integrity": "sha512-SCjM/zlBdOK8Q5TIjOn6iEHZaPHFsMoTxXQ2nvUvtPnuohz3H2dIozSg+etNR98dGoYUp2ENSKLL/XaMmbxVgw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.0",
"get-stream": "^5.0.0",
"human-signals": "^1.1.1",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.0",
"onetime": "^5.1.0",
"signal-exit": "^3.0.2",
"strip-final-newline": "^2.0.0"
}
},
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"dev": true,
"requires": {
"pump": "^3.0.0"
}
},
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
"dev": true
},
"npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dev": true,
"requires": {
"path-key": "^3.0.0"
}
},
"parse-json": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
"integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-better-errors": "^1.0.1",
"lines-and-columns": "^1.1.6"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"listr2": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-2.0.4.tgz",
"integrity": "sha512-oJaAcplPsa72rKW0eg4P4LbEJjhH+UO2I8uqR/I2wzHrVg16ohSfUy0SlcHS21zfYXxtsUpL8YXGHjyfWMR0cg==",
"dev": true,
"requires": {
"@samverschueren/stream-to-observable": "^0.3.0",
"chalk": "^4.0.0",
"cli-cursor": "^3.1.0",
"cli-truncate": "^2.1.0",
"elegant-spinner": "^2.0.0",
"enquirer": "^2.3.5",
"figures": "^3.2.0",
"indent-string": "^4.0.0",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"pad": "^3.2.0",
"rxjs": "^6.5.5",
"through": "^2.3.8",
"uuid": "^7.0.2"
},
"dependencies": {
"indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true
},
"p-map": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dev": true,
"requires": {
"aggregate-error": "^3.0.0"
}
},
"rxjs": {
"version": "6.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
"integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
},
"uuid": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
"dev": true
}
}
},
"load-bmfont": { "load-bmfont": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.0.tgz", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.0.tgz",
@@ -11948,6 +12399,71 @@
"integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=", "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=",
"dev": true "dev": true
}, },
"log-symbols": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
"integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
"dev": true,
"requires": {
"chalk": "^4.0.0"
}
},
"log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dev": true,
"requires": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
"slice-ansi": "^4.0.0",
"wrap-ansi": "^6.2.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
}
}
}
},
"lolex": { "lolex": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz",
@@ -12903,6 +13419,12 @@
"mimic-fn": "^2.1.0" "mimic-fn": "^2.1.0"
} }
}, },
"opencollective-postinstall": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
"integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
"dev": true
},
"optimize-css-assets-webpack-plugin": { "optimize-css-assets-webpack-plugin": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz",
@@ -13022,6 +13544,15 @@
"semver": "^5.1.0" "semver": "^5.1.0"
} }
}, },
"pad": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/pad/-/pad-3.2.0.tgz",
"integrity": "sha512-2u0TrjcGbOjBTJpyewEl4hBO3OeX5wWue7eIFPzQTg6wFSvoaHcBTTUY5m+n0hd04gmTCPuY0kCpVIVuw5etwg==",
"dev": true,
"requires": {
"wcwidth": "^1.0.1"
}
},
"pako": { "pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -13376,6 +13907,15 @@
} }
} }
}, },
"please-upgrade-node": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
"integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
"dev": true,
"requires": {
"semver-compare": "^1.0.0"
}
},
"pn": { "pn": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
@@ -14041,6 +14581,21 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true "dev": true
}, },
"prettier": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz",
"integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==",
"dev": true
},
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"requires": {
"fast-diff": "^1.1.2"
}
},
"pretty-bytes": { "pretty-bytes": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
@@ -15456,6 +16011,12 @@
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true "dev": true
}, },
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
"dev": true
},
"semver-diff": { "semver-diff": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz",
@@ -15465,6 +16026,12 @@
"semver": "^5.0.3" "semver": "^5.0.3"
} }
}, },
"semver-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz",
"integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
"dev": true
},
"send": { "send": {
"version": "0.17.1", "version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
@@ -16154,6 +16721,12 @@
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"dev": true "dev": true
}, },
"string-argv": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
"dev": true
},
"string-length": { "string-length": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz",
@@ -16227,6 +16800,17 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"stringify-object": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
"integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
"dev": true,
"requires": {
"get-own-enumerable-property-symbols": "^3.0.0",
"is-obj": "^1.0.1",
"is-regexp": "^1.0.0"
}
},
"strip-ansi": { "strip-ansi": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
@@ -17775,6 +18359,15 @@
"neo-async": "^2.5.0" "neo-async": "^2.5.0"
} }
}, },
"wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
"dev": true,
"requires": {
"defaults": "^1.0.3"
}
},
"webidl-conversions": { "webidl-conversions": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
@@ -18535,6 +19128,12 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true "dev": true
}, },
"yaml": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==",
"dev": true
},
"yargs": { "yargs": {
"version": "15.3.1", "version": "15.3.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
+9
View File
@@ -84,6 +84,7 @@
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^23.8.2", "eslint-plugin-jest": "^23.8.2",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0", "eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^2.5.1", "eslint-plugin-react-hooks": "^2.5.1",
"express": "^4.17.1", "express": "^4.17.1",
@@ -91,11 +92,14 @@
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"html-webpack-inline-source-plugin": "^1.0.0-beta.2", "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
"html-webpack-plugin": "^4.0.0", "html-webpack-plugin": "^4.0.0",
"husky": "^4.2.5",
"jest": "^25.2.7", "jest": "^25.2.7",
"jest-puppeteer": "^4.4.0", "jest-puppeteer": "^4.4.0",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"lint-staged": "^10.2.4",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"prettier": "^2.0.5",
"puppeteer": "^2.1.1", "puppeteer": "^2.1.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
@@ -146,5 +150,10 @@
] ]
} }
} }
},
"husky": {
"hooks": {
"pre-commit": "lint-staged --config \"./configuration/lint-staged/lint-staged.config.js\""
}
} }
} }
+3 -3
View File
@@ -29,7 +29,7 @@ async function obsAnnotationFetchAndLoad(dispatch, schema) {
fetchBinary( fetchBinary(
`annotations/obs?annotation-name=${encodeURIComponent(col.name)}` `annotations/obs?annotation-name=${encodeURIComponent(col.name)}`
) )
.then((buffer) => Universe.matrixFBSToDataframe(buffer)) .then((buffer) => MatrixFBS.matrixFBSToDataframe(buffer))
.then((df) => .then((df) =>
dispatch({ dispatch({
type: "universe: column load success", type: "universe: column load success",
@@ -52,7 +52,7 @@ async function varAnnotationFetchAndLoad(dispatch, schema) {
return Promise.all( return Promise.all(
names.map((name) => names.map((name) =>
fetchBinary(`annotations/var?annotation-name=${encodeURIComponent(name)}`) fetchBinary(`annotations/var?annotation-name=${encodeURIComponent(name)}`)
.then((buffer) => Universe.matrixFBSToDataframe(buffer)) .then((buffer) => MatrixFBS.matrixFBSToDataframe(buffer))
.then((df) => .then((df) =>
dispatch({ dispatch({
type: "universe: column load success", type: "universe: column load success",
@@ -77,7 +77,7 @@ function layoutFetchAndLoad(dispatch, schema) {
plimit.add(() => plimit.add(() =>
fetchBinary( fetchBinary(
`layout/obs?layout-name=${encodeURIComponent(e)}` `layout/obs?layout-name=${encodeURIComponent(e)}`
).then((buffer) => Universe.matrixFBSToDataframe(buffer)) ).then((buffer) => MatrixFBS.matrixFBSToDataframe(buffer))
) )
) )
).then((dfs) => ).then((dfs) =>
+3 -3
View File
@@ -1,5 +1,5 @@
import { API } from "../globals"; import { API } from "../globals";
import { Universe } from "../util/stateManager"; import { MatrixFBS } from "../util/stateManager";
import { import {
postNetworkErrorToast, postNetworkErrorToast,
postAsyncSuccessToast, postAsyncSuccessToast,
@@ -24,7 +24,7 @@ function abortableFetch(request, opts, timeout = 0) {
async function doReembedFetch(dispatch, getState) { async function doReembedFetch(dispatch, getState) {
const state = getState(); const state = getState();
let cells = state.world.obsAnnotations.rowIndex.keys(); let cells = state.world.obsAnnotations.rowIndex.labels();
// These lines ensure that we convert any TypedArray to an Array. // These lines ensure that we convert any TypedArray to an Array.
// This is necessary because JSON.stringify() does some very strange // This is necessary because JSON.stringify() does some very strange
@@ -80,7 +80,7 @@ export function requestReembed() {
const res = await doReembedFetch(dispatch, getState); const res = await doReembedFetch(dispatch, getState);
const schema = JSON.parse(res.headers.get("CxG-Schema")); const schema = JSON.parse(res.headers.get("CxG-Schema"));
const buffer = await res.arrayBuffer(); const buffer = await res.arrayBuffer();
const df = Universe.matrixFBSToDataframe(buffer); const df = MatrixFBS.matrixFBSToDataframe(buffer);
dispatch({ dispatch({
type: "reembed: request completed", type: "reembed: request completed",
}); });
+15 -17
View File
@@ -21,7 +21,6 @@ import actions from "../actions";
graphRenderCounter: state.controls.graphRenderCounter, graphRenderCounter: state.controls.graphRenderCounter,
})) }))
class App extends React.Component { class App extends React.Component {
componentDidMount() { componentDidMount() {
const { dispatch } = this.props; const { dispatch } = this.props;
@@ -68,22 +67,21 @@ class App extends React.Component {
error loading error loading
</div> </div>
) : null} ) : null}
{loading ? null : <Layout> {loading ? null : (
<LeftSideBar/> <Layout>
{viewportRef => <LeftSideBar />
<> {(viewportRef) => (
<MenuBar/> <>
<Autosave/> <MenuBar />
<TermsOfServicePrompt/> <Autosave />
<Legend viewportRef={viewportRef}/> <TermsOfServicePrompt />
<Graph <Legend viewportRef={viewportRef} />
key={graphRenderCounter} <Graph key={graphRenderCounter} viewportRef={viewportRef} />
viewportRef={viewportRef} </>
/> )}
</> <RightSideBar />
} </Layout>
<RightSideBar/> )}
</Layout>}
</Container> </Container>
); );
} }
@@ -6,9 +6,13 @@ import {
MenuItem, MenuItem,
Popover, Popover,
Position, Position,
Tooltip,
Icon,
PopoverInteractionKind, PopoverInteractionKind,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as globals from "../../../globals";
@connect((state) => ({ @connect((state) => ({
annotations: state.annotations, annotations: state.annotations,
})) }))
@@ -56,46 +60,57 @@ class AnnoMenuCategory extends React.PureComponent {
return ( return (
<> <>
{isUserAnno ? ( {isUserAnno ? (
<Popover <>
interactionKind={PopoverInteractionKind.HOVER} <Tooltip
boundary="window" content={createText}
position={Position.RIGHT_TOP} position="bottom"
content={ hoverOpenDelay={globals.tooltipHoverOpenDelay}
<Menu> >
<MenuItem <Button
icon="tag" style={{ marginLeft: 0, marginRight: 2 }}
data-testclass="handleAddNewLabelToCategory" data-testclass="handleAddNewLabelToCategory"
data-testid={`${metadataField}:add-new-label-to-category`} data-testid={`${metadataField}:add-new-label-to-category`}
onClick={this.activateAddNewLabelMode} icon={<Icon icon="plus" iconSize={10} />}
text={createText} onClick={this.activateAddNewLabelMode}
/> small
<MenuItem minimal
icon="edit" />
disabled={annotations.isEditingCategoryName} </Tooltip>
data-testclass="activateEditCategoryMode" <Popover
data-testid={`${metadataField}:edit-category-mode`} interactionKind={PopoverInteractionKind.HOVER}
onClick={this.activateEditCategoryMode} boundary="window"
text={editText} position={Position.RIGHT_TOP}
/> content={
<MenuItem <Menu>
icon="delete" <MenuItem
intent="danger" icon="edit"
data-testclass="handleDeleteCategory" disabled={annotations.isEditingCategoryName}
data-testid={`${metadataField}:delete-category`} data-testclass="activateEditCategoryMode"
onClick={this.handleDeleteCategory} data-testid={`${metadataField}:edit-category-mode`}
text={deleteText} onClick={this.activateEditCategoryMode}
/> text={editText}
</Menu> />
} <MenuItem
> icon="delete"
<Button intent="danger"
style={{ marginLeft: 0 }} data-testclass="handleDeleteCategory"
data-testclass="seeActions" data-testid={`${metadataField}:delete-category`}
data-testid={`${metadataField}:see-actions`} onClick={this.handleDeleteCategory}
icon="more" text={deleteText}
minimal />
/> </Menu>
</Popover> }
>
<Button
style={{ marginLeft: 0, marginRight: 5 }}
data-testclass="seeActions"
data-testid={`${metadataField}:see-actions`}
icon={<Icon icon="more" iconSize={10} />}
small
minimal
/>
</Popover>
</>
) : null} ) : null}
</> </>
); );
@@ -2,20 +2,18 @@ import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { FaChevronRight, FaChevronDown } from "react-icons/fa"; import { FaChevronRight, FaChevronDown } from "react-icons/fa";
import { import { AnchorButton, Button, Tooltip } from "@blueprintjs/core";
AnchorButton,
Button,
Tooltip,
Icon,
Position,
} from "@blueprintjs/core";
import CategoryFlipperLayout from "./categoryFlipperLayout"; import CategoryFlipperLayout from "./categoryFlipperLayout";
import AnnoMenu from "./annoMenuCategory"; import AnnoMenu from "./annoMenuCategory";
import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName"; import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName";
import AnnoDialogAddLabel from "./annoDialogAddLabel"; import AnnoDialogAddLabel from "./annoDialogAddLabel";
import Truncate from "../../util/truncate";
import * as globals from "../../../globals"; import * as globals from "../../../globals";
import maybeTruncateString from "../../../util/maybeTruncateString";
const LABEL_WIDTH = globals.leftSidebarWidth - 100;
const ANNO_BUTTON_WIDTH = 50;
const LABEL_WIDTH_ANNO = LABEL_WIDTH - ANNO_BUTTON_WIDTH;
@connect((state, ownProps) => { @connect((state, ownProps) => {
const { metadataField } = ownProps; const { metadataField } = ownProps;
@@ -120,10 +118,6 @@ class Category extends React.Component {
We are still loading this category, so render a "busy" signal. We are still loading this category, so render a "busy" signal.
*/ */
const { metadataField } = this.props; const { metadataField } = this.props;
const truncatedString = maybeTruncateString(
metadataField,
globals.categoryDisplayStringMaxLength
);
const checkboxID = `category-select-${metadataField}`; const checkboxID = `category-select-${metadataField}`;
@@ -151,26 +145,17 @@ class Category extends React.Component {
<input disabled id={checkboxID} checked type="checkbox" /> <input disabled id={checkboxID} checked type="checkbox" />
<span className="bp3-control-indicator" /> <span className="bp3-control-indicator" />
</label> </label>
<Tooltip <Truncate>
content={metadataField}
disabled={truncatedString === null}
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<span <span
style={{ style={{
cursor: "pointer", cursor: "pointer",
display: "inline-block", display: "inline-block",
width: LABEL_WIDTH,
}} }}
> >
{truncatedString || metadataField} {metadataField}
</span> </span>
</Tooltip> </Truncate>
</div> </div>
<div> <div>
<Button minimal loading intent="primary" /> <Button minimal loading intent="primary" />
@@ -205,21 +190,22 @@ class Category extends React.Component {
false false
); );
const truncatedString = maybeTruncateString(
metadataField,
globals.categoryDisplayStringMaxLength
);
if ( if (
!isUserAnno && !isUserAnno &&
schema?.annotations?.obsByName[metadataField]?.categories?.length === 1 schema?.annotations?.obsByName[metadataField]?.categories?.length === 1
) { ) {
return ( return (
<div style={{ marginBottom: 10, marginTop: 4 }}> <div style={{ marginBottom: 10, marginTop: 4 }}>
<span style={{ fontWeight: 700 }}> <Truncate>
{truncatedString || metadataField} <span style={{ maxWidth: 150, fontWeight: 700 }}>
</span> {metadataField}
: {schema.annotations.obsByName[metadataField].categories[0]} </span>
</Truncate>
<Truncate>
<span style={{ maxWidth: 150 }}>
{`: ${schema.annotations.obsByName[metadataField].categories[0]}`}
</span>
</Truncate>
</div> </div>
); );
} }
@@ -252,49 +238,42 @@ class Category extends React.Component {
/> />
<span className="bp3-control-indicator" /> <span className="bp3-control-indicator" />
</label> </label>
<Tooltip <span
content={metadataField} role="menuitem"
disabled={truncatedString === null} tabIndex="0"
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick} data-testid={`${metadataField}:category-expand`}
position={Position.LEFT} onKeyPress={(e) => {
usePortal if (e.key === "Enter") {
modifiers={{ this.handleCategoryClick();
preventOverflow: { enabled: false }, }
hide: { enabled: false },
}} }}
style={{
cursor: "pointer",
}}
onClick={this.handleCategoryClick}
> >
<span <Truncate>
role="menuitem" <span
tabIndex="0" style={{
data-testid={`${metadataField}:category-expand`} maxWidth: isUserAnno ? LABEL_WIDTH_ANNO : LABEL_WIDTH,
onKeyPress={(e) => { }}
if (e.key === "Enter") { data-testid={`${metadataField}:category-label`}
this.handleCategoryClick(); >
} {metadataField}
}} </span>
style={{ </Truncate>
cursor: "pointer", {isExpanded ? (
display: "inline-block", <FaChevronDown
}} data-testclass="category-expand-is-expanded"
onClick={this.handleCategoryClick} style={{ fontSize: 10, marginLeft: 5 }}
> />
{isUserAnno ? ( ) : (
<Icon style={{ marginRight: 5 }} icon="tag" iconSize={16} /> <FaChevronRight
) : null} data-testclass="category-expand-is-not-expanded"
{truncatedString || metadataField} style={{ fontSize: 10, marginLeft: 5 }}
{isExpanded ? ( />
<FaChevronDown )}
data-testclass="category-expand-is-expanded" </span>
style={{ fontSize: 10, marginLeft: 5 }}
/>
) : (
<FaChevronRight
data-testclass="category-expand-is-not-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
)}
</span>
</Tooltip>
</div> </div>
{<AnnoDialogEditCategoryName metadataField={metadataField} />} {<AnnoDialogEditCategoryName metadataField={metadataField} />}
{<AnnoDialogAddLabel metadataField={metadataField} />} {<AnnoDialogAddLabel metadataField={metadataField} />}
@@ -7,17 +7,17 @@ import {
MenuItem, MenuItem,
Popover, Popover,
Position, Position,
Icon,
PopoverInteractionKind, PopoverInteractionKind,
Tooltip,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import Occupancy from "./occupancy"; import Occupancy from "./occupancy";
import * as globals from "../../../globals"; import * as globals from "../../../globals";
import styles from "../categorical.css"; import styles from "../categorical.css";
import AnnoDialog from "../annoDialog"; import AnnoDialog from "../annoDialog";
import LabelInput from "../labelInput"; import LabelInput from "../labelInput";
import Truncate from "../../util/truncate";
import { AnnotationsHelpers } from "../../../util/stateManager"; import { AnnotationsHelpers } from "../../../util/stateManager";
import maybeTruncateString from "../../../util/maybeTruncateString";
import { labelPrompt, isLabelErroneous } from "../labelUtil"; import { labelPrompt, isLabelErroneous } from "../labelUtil";
/* this is defined outside of the class so we can use it in connect() */ /* this is defined outside of the class so we can use it in connect() */
@@ -316,13 +316,6 @@ class CategoryValue extends React.Component {
categories = schema.annotations.obsByName[colorAccessor]?.categories; categories = schema.annotations.obsByName[colorAccessor]?.categories;
} }
const truncatedString = maybeTruncateString(
displayString,
colorAccessor && !isColorBy
? globals.categoryLabelDisplayStringShortLength
: globals.categoryLabelDisplayStringLongLength
);
const editModeActive = const editModeActive =
isUserAnno && isUserAnno &&
annotations.labelEditable.category === metadataField && annotations.labelEditable.category === metadataField &&
@@ -331,6 +324,26 @@ class CategoryValue extends React.Component {
const valueToggleLabel = `value-toggle-checkbox-${displayString}`; const valueToggleLabel = `value-toggle-checkbox-${displayString}`;
const LEFT_MARGIN = 33;
const CHECKBOX = 26;
const CELL_NUMBER = 61;
const ANNO_MENU = 26;
const LABEL_MARGIN = 24;
const otherElementsWidth =
LEFT_MARGIN +
CHECKBOX +
CELL_NUMBER +
LABEL_MARGIN +
(isUserAnno ? ANNO_MENU : 0);
const OCCUPANCY_WIDTH = 100;
const labelWidth =
colorAccessor && !isColorBy
? globals.leftSidebarWidth - otherElementsWidth - OCCUPANCY_WIDTH
: globals.leftSidebarWidth - otherElementsWidth;
return ( return (
<div <div
key={i} key={i}
@@ -343,7 +356,7 @@ class CategoryValue extends React.Component {
} }
data-testclass="categorical-row" data-testclass="categorical-row"
style={{ style={{
padding: "4px 7px", padding: "4px 0px 4px 7px",
display: "flex", display: "flex",
alignItems: "baseline", alignItems: "baseline",
justifyContent: "space-between", justifyContent: "space-between",
@@ -383,21 +396,12 @@ class CategoryValue extends React.Component {
onMouseLeave={this.handleMouseEnter} onMouseLeave={this.handleMouseEnter}
/> />
</label> </label>
<Tooltip <Truncate>
content={displayString}
disabled={truncatedString === null}
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<span <span
data-testid={`categorical-value-${metadataField}-${displayString}`} data-testid={`categorical-value-${metadataField}-${displayString}`}
data-testclass="categorical-value" data-testclass="categorical-value"
style={{ style={{
width: labelWidth,
color: color:
displayString === globals.unassignedCategoryLabel displayString === globals.unassignedCategoryLabel
? "#ababab" ? "#ababab"
@@ -410,13 +414,13 @@ class CategoryValue extends React.Component {
overflow: "hidden", overflow: "hidden",
lineHeight: "1.1em", lineHeight: "1.1em",
height: "1.1em", height: "1.1em",
wordBreak: "break-all",
verticalAlign: "middle", verticalAlign: "middle",
marginRight: LABEL_MARGIN,
}} }}
> >
{truncatedString || displayString} {displayString}
</span> </span>
</Tooltip> </Truncate>
{editModeActive ? ( {editModeActive ? (
<div> <div>
<AnnoDialog <AnnoDialog
@@ -561,14 +565,14 @@ class CategoryValue extends React.Component {
> >
<Button <Button
style={{ style={{
marginLeft: 0, marginLeft: 2,
position: "relative", position: "relative",
top: -1, top: -1,
minHeight: 16, minHeight: 16,
}} }}
data-testclass="seeActions" data-testclass="seeActions"
data-testid={`${metadataField}:${displayString}:see-actions`} data-testid={`${metadataField}:${displayString}:see-actions`}
icon="more" icon={<Icon icon="more" iconSize={10} />}
small small
minimal minimal
/> />
@@ -168,7 +168,7 @@ class Occupancy extends React.PureComponent {
else this.createHistogram(); else this.createHistogram();
}} }}
/> />
<div key="text" style={{ fontFamily: "Roboto", fontSize: "14px" }}> <div key="text" style={{ fontSize: "14px" }}>
<p style={{ margin: "0" }}> <p style={{ margin: "0" }}>
This histograms shows the distribution of{" "} This histograms shows the distribution of{" "}
<strong>{colorAccessor}</strong> within{" "} <strong>{colorAccessor}</strong> within{" "}
@@ -105,10 +105,12 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
colorScale: state.colors.scale, colorScale: state.colors.scale,
})) }))
class ContinuousLegend extends React.Component { class ContinuousLegend extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { colorAccessor, colorScale } = this.props; const { colorAccessor, colorScale } = this.props;
if (prevProps.colorAccessor !== colorAccessor || prevProps.colorScale !== colorScale) { if (
prevProps.colorAccessor !== colorAccessor ||
prevProps.colorScale !== colorScale
) {
/* always remove it, if it's not continuous we don't put it back. */ /* always remove it, if it's not continuous we don't put it back. */
d3.select("#continuous_legend").selectAll("*").remove(); d3.select("#continuous_legend").selectAll("*").remove();
} }
@@ -130,7 +132,9 @@ class ContinuousLegend extends React.Component {
return ( return (
<div <div
id="continuous_legend" id="continuous_legend"
ref={ref => {this.ref = ref}} ref={(ref) => {
this.ref = ref;
}}
style={{ style={{
display: colorAccessor ? "inherit" : "none", display: colorAccessor ? "inherit" : "none",
position: "absolute", position: "absolute",
+15 -13
View File
@@ -2,19 +2,21 @@
import React from "react"; import React from "react";
function Container(props) { function Container(props) {
const {children} = props; const { children } = props;
return <div return (
className="container" <div
style={{ className="container"
height: "calc(100vh - (100vh - 100%))", style={{
width: "calc(100vw - (100vw - 100%))", height: "calc(100vh - (100vh - 100%))",
position: "absolute", width: "calc(100vw - (100vw - 100%))",
top: 0, position: "absolute",
left: 0, top: 0,
}} left: 0,
> }}
{children} >
</div> {children}
</div>
);
} }
export default Container; export default Container;
+54 -50
View File
@@ -2,9 +2,7 @@
import React from "react"; import React from "react";
import * as globals from "../../globals"; import * as globals from "../../globals";
class Layout extends React.Component { class Layout extends React.Component {
/* /*
Layout - this react component contains all the layout style and logic for the application once it has loaded. Layout - this react component contains all the layout style and logic for the application once it has loaded.
@@ -26,60 +24,66 @@ class Layout extends React.Component {
render() { render() {
const { children } = this.props; const { children } = this.props;
const [ leftSidebar, renderGraph, rightSidebar ] = children; const [leftSidebar, renderGraph, rightSidebar] = children;
return <div return (
style={{ <div
display: "grid", style={{
gridTemplateColumns: ` display: "grid",
[left-sidebar-start] ${globals.leftSidebarWidth+1}px gridTemplateColumns: `
[left-sidebar-start] ${globals.leftSidebarWidth + 1}px
[left-sidebar-end graph-start] auto [left-sidebar-end graph-start] auto
[graph-end right-sidebar-start] ${globals.rightSidebarWidth+1}px [right-sidebar-end] [graph-end right-sidebar-start] ${
globals.rightSidebarWidth + 1
}px [right-sidebar-end]
`, `,
gridTemplateRows: "[top] auto [bottom]", gridTemplateRows: "[top] auto [bottom]",
gridTemplateAreas: "left-sidebar | graph | right-sidebar", gridTemplateAreas: "left-sidebar | graph | right-sidebar",
columnGap: "0px", columnGap: "0px",
justifyItems: "stretch", justifyItems: "stretch",
alignItems: "stretch", alignItems: "stretch",
height: "inherit",
width: "inherit",
position: "relative",
top: 0,
left: 0,
minWidth: "1240px",
}}
>
<div
style={{
gridArea: "top / left-sidebar-start / bottom / left-sidebar-end",
position: "relative",
height: "inherit", height: "inherit",
overflowY: "auto" width: "inherit",
position: "relative",
top: 0,
left: 0,
minWidth: "1240px",
}} }}
> >
{leftSidebar} <div
style={{
gridArea: "top / left-sidebar-start / bottom / left-sidebar-end",
position: "relative",
height: "inherit",
overflowY: "auto",
}}
>
{leftSidebar}
</div>
<div
style={{
zIndex: 0,
gridArea: "top / graph-start / bottom / graph-end",
position: "relative",
height: "inherit",
}}
ref={(ref) => {
this.viewportRef = ref;
}}
>
{this.viewportRef ? renderGraph(this.viewportRef) : null}
</div>
<div
style={{
gridArea: "top / right-sidebar-start / bottom / right-sidebar-end",
position: "relative",
height: "inherit",
overflowY: "auto",
}}
>
{rightSidebar}
</div>
</div> </div>
<div );
style={{
zIndex: 0,
gridArea: "top / graph-start / bottom / graph-end",
position: "relative",
height: "inherit",
}}
ref={ref => { this.viewportRef = ref; }}
>
{this.viewportRef ? renderGraph(this.viewportRef) : null}
</div>
<div
style={{
gridArea: "top / right-sidebar-start / bottom / right-sidebar-end",
position: "relative",
height: "inherit",
overflowY: "auto"
}}
>
{rightSidebar}
</div>
</div>;
} }
} }
@@ -8,6 +8,7 @@ export default
colorAccessor: state.colors.colorAccessor, colorAccessor: state.colors.colorAccessor,
dilatedValue: state.pointDilation.categoryField, dilatedValue: state.pointDilation.categoryField,
labels: state.centroidLabels.labels, labels: state.centroidLabels.labels,
categoricalSelection: state.categoricalSelection,
})) }))
class CentroidLabels extends PureComponent { class CentroidLabels extends PureComponent {
// Check to see if centroids have either just been displayed or removed from the overlay // Check to see if centroids have either just been displayed or removed from the overlay
@@ -33,11 +34,21 @@ class CentroidLabels extends PureComponent {
dilatedValue, dilatedValue,
dispatch, dispatch,
colorAccessor, colorAccessor,
categoricalSelection,
} = this.props; } = this.props;
if (!colorAccessor || labels.size === undefined || labels.size === 0)
return null;
const {
categoryValueIndices,
categoryValueSelected,
} = categoricalSelection?.[colorAccessor];
const labelSVGS = []; const labelSVGS = [];
let fontSize = "15px"; let fontSize = "15px";
let fontWeight = null; let fontWeight = null;
const deselectOpacity = 0.375;
labels.forEach((coords, label) => { labels.forEach((coords, label) => {
fontSize = "15px"; fontSize = "15px";
fontWeight = null; fontWeight = null;
@@ -46,6 +57,8 @@ class CentroidLabels extends PureComponent {
fontWeight = "800"; fontWeight = "800";
} }
const selected = categoryValueSelected[categoryValueIndices.get(label)];
// Mirror LSB middle truncation // Mirror LSB middle truncation
let displayLabel = label; let displayLabel = label;
if (displayLabel.length > categoryLabelDisplayStringLongLength) { if (displayLabel.length > categoryLabelDisplayStringLongLength) {
@@ -72,11 +85,11 @@ class CentroidLabels extends PureComponent {
textAnchor="middle" textAnchor="middle"
data-label={label} data-label={label}
style={{ style={{
fontFamily: "Roboto Condensed",
fontSize, fontSize,
fontWeight, fontWeight,
fill: "black", fill: "black",
userSelect: "none", userSelect: "none",
opacity: selected ? 1 : deselectOpacity,
}} }}
onMouseEnter={(e) => onMouseEnter={(e) =>
dispatch({ dispatch({
@@ -81,7 +81,7 @@ export default class GraphOverlayLayer extends PureComponent {
position: "absolute", position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
zIndex: 1, zIndex: 2,
backgroundColor: displaying ? "rgba(255, 255, 255, 0.55)" : "", backgroundColor: displaying ? "rgba(255, 255, 255, 0.55)" : "",
}} }}
onMouseMove={handleCanvasEvent} onMouseMove={handleCanvasEvent}
@@ -14,7 +14,7 @@ export default (
handleDragAction, handleDragAction,
handleEndAction, handleEndAction,
handleCancelAction, handleCancelAction,
viewport, viewport
) => { ) => {
const svg = d3.select("#graph-wrapper").select("#lasso-layer"); const svg = d3.select("#graph-wrapper").select("#lasso-layer");
@@ -23,7 +23,7 @@ export default (
.brush() .brush()
.extent([ .extent([
[0, 0], [0, 0],
[viewport.width, viewport.height] [viewport.width, viewport.height],
]) ])
.on("start", handleStartAction) .on("start", handleStartAction)
.on("brush", handleDragAction) .on("brush", handleDragAction)
@@ -11,7 +11,6 @@ import TopLeftLogoAndTitle from "./topLeftLogoAndTitle";
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
})) }))
class LeftSideBar extends React.Component { class LeftSideBar extends React.Component {
render() { render() {
const { scatterplotXXaccessor, scatterplotYYaccessor } = this.props; const { scatterplotXXaccessor, scatterplotYYaccessor } = this.props;
return ( return (
@@ -3,6 +3,10 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import * as globals from "../../globals"; import * as globals from "../../globals";
import Logo from "../framework/logo"; import Logo from "../framework/logo";
import Truncate from "../util/truncate";
const DATASET_TITLE_WIDTH = 190;
const DATASET_TITLE_FONT_SIZE = 14;
@connect((state) => ({ @connect((state) => ({
datasetTitle: state.config?.displayNames?.dataset ?? "", datasetTitle: state.config?.displayNames?.dataset ?? "",
@@ -14,16 +18,6 @@ class LeftSideBar extends React.Component {
render() { render() {
const { datasetTitle, aboutURL } = this.props; const { datasetTitle, aboutURL } = this.props;
const displayTitle =
datasetTitle.length > globals.datasetTitleMaxCharacterCount
? `${datasetTitle.substring(
0,
Math.floor(globals.datasetTitleMaxCharacterCount / 2)
)}…${datasetTitle.slice(
-Math.floor(globals.datasetTitleMaxCharacterCount / 2)
)}`
: datasetTitle;
return ( return (
<div <div
style={{ style={{
@@ -60,26 +54,36 @@ class LeftSideBar extends React.Component {
gene gene
</span> </span>
<div <div
data-testid="header"
style={{ style={{
fontSize: 14, fontSize: DATASET_TITLE_FONT_SIZE,
position: "relative", position: "relative",
top: -6, top: -6,
display: "inline-block", display: "inline-block",
width: "190px", width: DATASET_TITLE_WIDTH,
marginLeft: "7px", marginLeft: "7px",
height: "1.2em", height: "1.2em",
overflow: "hidden", overflow: "hidden",
wordBreak: "break-all", wordBreak: "break-all",
}} }}
title={datasetTitle}
> >
{aboutURL ? ( {aboutURL ? (
<a href={aboutURL} target="_blank" rel="noopener noreferrer"> <Truncate>
{displayTitle} <a
</a> style={{ width: 185 }}
href={aboutURL}
data-testid="header"
target="_blank"
rel="noopener noreferrer"
>
{datasetTitle}
</a>
</Truncate>
) : ( ) : (
displayTitle <Truncate>
<span style={{ width: 185 }} data-testid="header">
{datasetTitle}
</span>
</Truncate>
)} )}
</div> </div>
</div> </div>
+1 -3
View File
@@ -35,9 +35,7 @@ function Clip(props) {
: ""; : "";
return ( return (
<div <div className={`bp3-button-group ${styles.menubarButton}`}>
className={`bp3-button-group ${styles.menubarButton}`}
>
<Popover <Popover
target={ target={
<Tooltip <Tooltip
+6
View File
@@ -37,6 +37,7 @@ import DiffexpButtons from "./diffexpButtons";
showCentroidLabels: state.centroidLabels.showLabels, showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.["about_legal_tos"], tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"], privacyURL: state.config?.parameters?.["about_legal_privacy"],
categoricalSelection: state.categoricalSelection,
})) }))
class MenuBar extends React.Component { class MenuBar extends React.Component {
static isValidDigitKeyEvent(e) { static isValidDigitKeyEvent(e) {
@@ -203,9 +204,13 @@ class MenuBar extends React.Component {
showCentroidLabels, showCentroidLabels,
privacyURL, privacyURL,
tosURL, tosURL,
categoricalSelection,
colorAccessor,
} = this.props; } = this.props;
const { pendingClipPercentiles } = this.state; const { pendingClipPercentiles } = this.state;
const isColoredByCategorical = !!categoricalSelection?.[colorAccessor];
// constants used to create selection tool button // constants used to create selection tool button
const [selectionTooltip, selectionButtonIcon] = const [selectionTooltip, selectionButtonIcon] =
selectionTool === "brush" selectionTool === "brush"
@@ -267,6 +272,7 @@ class MenuBar extends React.Component {
onClick={this.handleCentroidChange} onClick={this.handleCentroidChange}
active={showCentroidLabels} active={showCentroidLabels}
intent={showCentroidLabels ? "primary" : "none"} intent={showCentroidLabels ? "primary" : "none"}
disabled={!isColoredByCategorical}
/> />
</Tooltip> </Tooltip>
<ButtonGroup className={styles.menubarButton}> <ButtonGroup className={styles.menubarButton}>
+1 -3
View File
@@ -6,9 +6,7 @@ import styles from "./menubar.css";
function InformationMenu(props) { function InformationMenu(props) {
const { libraryVersions, aboutLink, tosURL, privacyURL } = props; const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
return ( return (
<div <div className={`bp3-button-group ${styles.menubarButton}`}>
className={`bp3-button-group ${styles.menubarButton}`}
>
<Popover <Popover
content={ content={
<Menu> <Menu>
+2 -2
View File
@@ -1,4 +1,4 @@
:local(.menubarButton) { :local(.menubarButton) {
margin-top: 8px; margin-top: 8px;
margin-left: 8px; margin-left: 8px;
} }
+2 -4
View File
@@ -12,9 +12,7 @@ function Subset(props) {
} = props; } = props;
return ( return (
<ButtonGroup <ButtonGroup className={styles.menubarButton}>
className={styles.menubarButton}
>
<Tooltip <Tooltip
content="Subset to currently selected cells and associated metadata" content="Subset to currently selected cells and associated metadata"
position="bottom" position="bottom"
@@ -26,7 +24,7 @@ function Subset(props) {
disabled={!subsetPossible} disabled={!subsetPossible}
icon="pie-chart" icon="pie-chart"
onClick={handleSubset} onClick={handleSubset}
/> />
</Tooltip> </Tooltip>
<Tooltip <Tooltip
content="Undo subset and show all cells and associated metadata" content="Undo subset and show all cells and associated metadata"
+1 -3
View File
@@ -7,9 +7,7 @@ import styles from "./menubar.css";
function InformationMenu(props) { function InformationMenu(props) {
const { undoDisabled, redoDisabled, dispatch } = props; const { undoDisabled, redoDisabled, dispatch } = props;
return ( return (
<div <div className={`bp3-button-group ${styles.menubarButton}`}>
className={`bp3-button-group ${styles.menubarButton}`}
>
<Tooltip <Tooltip
content="Undo" content="Undo"
position="bottom" position="bottom"
+2 -3
View File
@@ -5,12 +5,11 @@ import Continuous from "../continuous/continuous";
import GeneExpression from "../geneExpression"; import GeneExpression from "../geneExpression";
import * as globals from "../../globals"; import * as globals from "../../globals";
@connect(state => ({ @connect((state) => ({
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
})) }))
class RightSidebar extends React.Component { class RightSidebar extends React.Component {
render() { render() {
return ( return (
<div <div
@@ -22,7 +21,7 @@ class RightSidebar extends React.Component {
position: "relative", position: "relative",
overflowY: "inherit", overflowY: "inherit",
height: "inherit", height: "inherit",
width: "inherit" width: "inherit",
}} }}
> >
<GeneExpression /> <GeneExpression />
+85
View File
@@ -0,0 +1,85 @@
import React, { cloneElement } from "react";
import { Tooltip, Position } from "@blueprintjs/core";
import { tooltipHoverOpenDelayQuick } from "../../globals";
const SPLIT_STYLE = {
display: "flex",
overflow: "hidden",
justifyContent: "flex-start",
};
const FIRST_HALF_STYLE = {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flexShrink: 1,
minWidth: "5px",
};
const SECOND_HALF_INNER_STYLE = {
position: "absolute",
right: 0,
};
const SECOND_HALF_STYLE = {
color: "transparent",
position: "relative",
overflow: "hidden",
whiteSpace: "nowrap",
};
export default (props) => {
const { children } = props;
// Truncate only support a single child with a text child
if (
React.Children.count(children) !== 1 ||
React.Children.count(children.props?.children) !== 1
) {
throw Error("Only pass a single child with text to Truncate");
}
const originalString = children.props.children;
const firstString = originalString.substr(0, originalString.length / 2);
const secondString = originalString.substr(originalString.length / 2);
const inheritedColor = children.props.style.color;
const splitStyle = { ...children.props.style, ...SPLIT_STYLE };
const secondHalfInnerStyle = {
...SECOND_HALF_INNER_STYLE,
color: inheritedColor || "initial",
};
const truncatedJSX = (
<span style={splitStyle}>
<span style={FIRST_HALF_STYLE}>{firstString}</span>
<span style={SECOND_HALF_STYLE}>
{secondString}
<span style={secondHalfInnerStyle}>{secondString}</span>
</span>
</span>
);
// clone children, changing the children(text) to the truncated string
const newChildren = React.Children.map(children, (child) =>
cloneElement(child, {
children: truncatedJSX,
"aria-label": originalString,
})
);
return (
<Tooltip
content={originalString}
hoverOpenDelay={tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
{newChildren}
</Tooltip>
);
};
+1 -1
View File
@@ -31,7 +31,7 @@ const CategoricalSelection = (
const names = CH.selectableCategoryNames( const names = CH.selectableCategoryNames(
world.schema, world.schema,
CH.maxCategoryItems(prevSharedState.config), CH.maxCategoryItems(prevSharedState.config),
dataframe.colIndex.keys() dataframe.colIndex.labels()
); );
if (names.length === 0) return state; if (names.length === 0) return state;
return { return {
+1
View File
@@ -63,6 +63,7 @@ const centroidLabels = (state = initialState, action, sharedNextState) => {
}; };
case "color by continuous metadata": case "color by continuous metadata":
case "color by expression":
return { ...state, labels: [] }; return { ...state, labels: [] };
case "reset centroid labels": case "reset centroid labels":
+5 -5
View File
@@ -93,7 +93,7 @@ const WorldReducer = (
let worldValSlice = val; let worldValSlice = val;
if (!World.worldEqUniverse(state, universe)) { if (!World.worldEqUniverse(state, universe)) {
worldValSlice = universeVarData worldValSlice = universeVarData
.subset(state.obsAnnotations.rowIndex.keys(), [key], null) .subset(state.obsAnnotations.rowIndex.labels(), [key], null)
.icol(0) .icol(0)
.asArray(); .asArray();
} }
@@ -129,10 +129,10 @@ const WorldReducer = (
// //
let clippedVarData = state.varData; let clippedVarData = state.varData;
const keysToDrop = clippedVarData.colIndex const keysToDrop = clippedVarData.colIndex
.keys() .labels()
.filter((k) => !unclippedVarData.hasCol(k)); .filter((k) => !unclippedVarData.hasCol(k));
const keysToAdd = unclippedVarData.colIndex const keysToAdd = unclippedVarData.colIndex
.keys() .labels()
.filter((k) => !clippedVarData.hasCol(k)); .filter((k) => !clippedVarData.hasCol(k));
keysToDrop.forEach((k) => { keysToDrop.forEach((k) => {
clippedVarData = clippedVarData.dropCol(k); clippedVarData = clippedVarData.dropCol(k);
@@ -171,7 +171,7 @@ const WorldReducer = (
let newAnnotation = null; let newAnnotation = null;
if (!World.worldEqUniverse(state, universe)) { if (!World.worldEqUniverse(state, universe)) {
newAnnotation = universe.obsAnnotations newAnnotation = universe.obsAnnotations
.subset(state.obsAnnotations.rowIndex.keys(), [name], null) .subset(state.obsAnnotations.rowIndex.labels(), [name], null)
.icol(0) .icol(0)
.asArray(); .asArray();
} else { } else {
@@ -303,7 +303,7 @@ const WorldReducer = (
let schema = origSchema; let schema = origSchema;
// alias the names the server sent us, in case they were not the same as the schema // alias the names the server sent us, in case they were not the same as the schema
const embedingLabels = embedding.colIndex.keys(); const embedingLabels = embedding.colIndex.labels();
const labels = { const labels = {
[embedingLabels[0]]: dims[0], [embedingLabels[0]]: dims[0],
[embedingLabels[1]]: dims[1], [embedingLabels[1]]: dims[1],
+51 -73
View File
@@ -1,6 +1,5 @@
import { IdentityInt32Index, isLabelIndex } from "./labelIndex"; import { IdentityInt32Index, isLabelIndex } from "./labelIndex";
// weird cross-dependency that we should clean up someday... // weird cross-dependency that we should clean up someday...
import { sortArray } from "../typedCrossfilter/sort";
import { import {
isTypedArray, isTypedArray,
isArrayOrTypedArray, isArrayOrTypedArray,
@@ -389,7 +388,7 @@ class Dataframe {
let dstLabels; let dstLabels;
if (!labels) { if (!labels) {
// combine all columns // combine all columns
dstLabels = dataframe.colIndex.keys(); dstLabels = dataframe.colIndex.labels();
srcLabels = dstLabels; srcLabels = dstLabels;
} else if (Array.isArray(labels)) { } else if (Array.isArray(labels)) {
// combine subset of keys with no aliasing // combine subset of keys with no aliasing
@@ -537,7 +536,12 @@ class Dataframe {
} }
static empty(rowIndex = null, colIndex = null) { static empty(rowIndex = null, colIndex = null) {
return new Dataframe([0, 0], [], rowIndex, colIndex); const dims = [
rowIndex ? rowIndex.size() : 0,
colIndex ? colIndex.size() : 0,
];
if (dims[0] && dims[1]) throw new Error("not an empty dataframe");
return new Dataframe(dims, new Array(dims[1]), rowIndex, colIndex);
} }
static create(dims, columnarData) { static create(dims, columnarData) {
@@ -551,97 +555,59 @@ class Dataframe {
return new Dataframe(dims, columnarData, null, null); return new Dataframe(dims, columnarData, null, null);
} }
__subset(rowOffsets, colOffsets, withRowIndex) { __subset(newRowIndex, newColIndex) {
const dims = [...this.dims]; const dims = [...this.dims];
const getSortedLabelAndOffsets = (offsets, index) => { /* subset columns */
/* let { __columns, colIndex } = this;
Given offsets, return both offsets and associated lables, if (newColIndex) {
sorted by offset. const colOffsets = this.colIndex.getOffsets(newColIndex.labels());
*/ __columns = new Array(colOffsets.length);
if (!offsets) { for (let i = 0, l = colOffsets.length; i < l; i += 1) {
return [null, null]; __columns[i] = this.__columns[colOffsets[i]];
} }
const sortedOffsets = sortArray(offsets); colIndex = newColIndex;
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; dims[1] = colOffsets.length;
colIndex = this.colIndex.subsetLabels(colLabels);
} }
let { rowIndex } = this; let { rowIndex } = this;
if (withRowIndex) rowIndex = withRowIndex; if (newRowIndex) {
if (rowOffsets) { const rowOffsets = this.rowIndex.getOffsets(newRowIndex.labels());
let rowLabels; __columns = __columns.map((col) => {
[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); const newCol = new col.constructor(rowOffsets.length);
for (let i = 0, l = rowOffsets.length; i < l; i += 1) { for (let i = 0, l = rowOffsets.length; i < l; i += 1) {
newCol[i] = col[rowOffsets[i]]; newCol[i] = col[rowOffsets[i]];
} }
return newCol; return newCol;
}); });
rowIndex = newRowIndex;
dims[0] = rowOffsets.length;
} }
if (dims[0] === 0 || dims[1] === 0) return Dataframe.empty(); if (dims[0] === 0 || dims[1] === 0) return Dataframe.empty();
return new Dataframe(dims, columns, rowIndex, colIndex); return new Dataframe(dims, __columns, rowIndex, colIndex);
} }
subset(rowLabels, colLabels = null, withRowIndex = null) { subset(rowLabels, colLabels = null, withRowIndex = null) {
/* /*
Subset by row/col labels. Subset by row/col labels.
withRowIndex allows assignment of new row index during subset operation. withRowIndex allows subset with an index, rather than rowLabels.
If withRowIndex === null, it will reset the index to identity (offset) If withRowIndex is specified, rowLabels is ignored.
indexing. if withRowIndex is a label index object, it will be used
for the new dataframe.
*/ */
const toOffsets = (labels, index) => { let rowIndex = null;
if (!labels) { if (withRowIndex) {
return null; rowIndex = withRowIndex;
} } else if (rowLabels) {
return labels.map((label) => { rowIndex = this.rowIndex.subset(rowLabels);
const off = index.getOffset(label); }
if (off === undefined) {
throw new RangeError(`unknown label: ${label}`);
}
return off;
});
};
const rowOffsets = toOffsets(rowLabels, this.rowIndex); let colIndex = null;
const colOffsets = toOffsets(colLabels, this.colIndex); if (colLabels) {
return this.__subset(rowOffsets, colOffsets, withRowIndex); colIndex = this.colIndex.subset(colLabels);
}
return this.__subset(rowIndex, colIndex);
} }
isubset(rowOffsets, colOffsets = null, withRowIndex = null) { isubset(rowOffsets, colOffsets = null, withRowIndex = null) {
@@ -653,7 +619,19 @@ class Dataframe {
indexing. If withRowIndex is a label index object, it will be used indexing. If withRowIndex is a label index object, it will be used
for the new dataframe. for the new dataframe.
*/ */
return this.__subset(rowOffsets, colOffsets, withRowIndex); let rowIndex = null;
if (withRowIndex) {
rowIndex = withRowIndex;
} else if (rowOffsets) {
rowIndex = this.rowIndex.isubset(rowOffsets);
}
let colIndex = null;
if (colOffsets) {
colIndex = this.colIndex.isubset(colOffsets);
}
return this.__subset(rowIndex, colIndex);
} }
isubsetMask(rowMask, colMask = null, withRowIndex = null) { isubsetMask(rowMask, colMask = null, withRowIndex = null) {
@@ -690,7 +668,7 @@ class Dataframe {
}; };
const rowOffsets = toList(rowMask, nRows); const rowOffsets = toList(rowMask, nRows);
const colOffsets = toList(colMask, nCols); const colOffsets = toList(colMask, nCols);
return this.__subset(rowOffsets, colOffsets, withRowIndex); return this.isubset(rowOffsets, colOffsets, withRowIndex);
} }
/** /**
@@ -790,7 +768,7 @@ class Dataframe {
Return true if this is an empty dataframe, ie, has dimensions [0,0] Return true if this is an empty dataframe, ie, has dimensions [0,0]
*/ */
const [rows, cols] = this.dims; const [rows, cols] = this.dims;
return rows === 0 && cols === 0; return rows === 0 || cols === 0;
} }
/**** /****
+6 -1
View File
@@ -1,2 +1,7 @@
export { default as Dataframe } from "./dataframe"; export { default as Dataframe } from "./dataframe";
export { DenseInt32Index, IdentityInt32Index, KeyIndex } from "./labelIndex"; export {
DenseInt32Index,
IdentityInt32Index,
KeyIndex,
isLabelIndex,
} from "./labelIndex";
+123 -16
View File
@@ -32,10 +32,10 @@ class IdentityInt32Index {
this.maxOffset = maxOffset; this.maxOffset = maxOffset;
} }
keys() { labels() {
// memoize // memoize
const k = fillRange(new Int32Array(this.maxOffset)); const k = fillRange(new Int32Array(this.maxOffset));
this.keys = function keys() { this.labels = function labels() {
return k; return k;
}; };
return k; return k;
@@ -47,12 +47,24 @@ class IdentityInt32Index {
return i; return i;
} }
// eslint-disable-next-line class-methods-use-this
getOffsets(arr) {
// labels to offsets
return arr;
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
getLabel(i) { getLabel(i) {
// offset to label // offset to label
return i; return i;
} }
// eslint-disable-next-line class-methods-use-this
getLabels(arr) {
// offsets to labels
return arr;
}
size() { size() {
return this.maxOffset; return this.maxOffset;
} }
@@ -62,6 +74,9 @@ class IdentityInt32Index {
time/space decision - based on the resulting density time/space decision - based on the resulting density
*/ */
const [minLabel, maxLabel] = extent(labelArray); const [minLabel, maxLabel] = extent(labelArray);
if (minLabel === 0 && maxLabel === labelArray.length - 1)
return new IdentityInt32Index(labelArray.length);
const labelSpaceSize = maxLabel - minLabel + 1; const labelSpaceSize = maxLabel - minLabel + 1;
const density = labelSpaceSize / this.maxOffset; const density = labelSpaceSize / this.maxOffset;
/* 0.1 is a magic number, that needs testing to optimize */ /* 0.1 is a magic number, that needs testing to optimize */
@@ -71,30 +86,43 @@ class IdentityInt32Index {
return new DenseInt32Index(labelArray, [minLabel, maxLabel]); return new DenseInt32Index(labelArray, [minLabel, maxLabel]);
} }
subsetLabels(labelArray) { subset(labels) {
return this.__promote(labelArray); /* validate subset */
const { maxOffset } = this;
for (let i = 0, l = labels.length; i < l; i += 1) {
const label = labels[i];
if (label < 0 || label >= maxOffset)
throw new RangeError(`offset or label: ${label}`);
}
return this.__promote(labels);
}
/* identity index - labels are offsets */
isubset(offsets) {
return this.subset(offsets);
} }
withLabel(label) { withLabel(label) {
if (label === this.maxOffset) { if (label === this.maxOffset) {
return new IdentityInt32Index(label + 1); return new IdentityInt32Index(label + 1);
} }
return this.__promote([...this.keys(), label]); return this.__promote([...this.labels(), label]);
} }
withLabels(labels) { withLabels(labels) {
return this.__promote([...this.keys(), ...labels]); return this.__promote([...this.labels(), ...labels]);
} }
dropLabel(label) { dropLabel(label) {
if (label === this.maxOffset - 1) { if (label === this.maxOffset - 1) {
return new IdentityInt32Index(label); return new IdentityInt32Index(label);
} }
const labelArray = [...this.keys()]; const labelArray = [...this.labels()];
labelArray.splice(labelArray.indexOf(label), 1); labelArray.splice(labelArray.indexOf(label), 1);
return this.__promote(labelArray); return this.__promote(labelArray);
} }
} }
class DenseInt32Index { class DenseInt32Index {
/* /*
DenseInt32Index indexes integer labels, and uses Int32Array typed arrays DenseInt32Index indexes integer labels, and uses Int32Array typed arrays
@@ -129,12 +157,29 @@ class DenseInt32Index {
this.getOffset = function getOffset(l) { this.getOffset = function getOffset(l) {
return index[l - minLabel]; return index[l - minLabel];
}; };
this.getOffsets = function getOffsets(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = index[arr[i] - minLabel];
}
return res;
};
this.getLabel = function getLabel(i) { this.getLabel = function getLabel(i) {
return rindex[i]; return rindex[i];
}; };
this.getLabels = function getLabels(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = rindex[arr[i]];
}
return res;
};
} }
keys() { labels() {
return this.rindex; return this.rindex;
} }
@@ -158,20 +203,44 @@ class DenseInt32Index {
return new DenseInt32Index(labelArray, [minLabel, maxLabel]); return new DenseInt32Index(labelArray, [minLabel, maxLabel]);
} }
subsetLabels(labelArray) { subset(labels) {
return this.__promote(labelArray); /* validate subset */
for (let i = 0, l = labels.length; i < l; i += 1) {
const label = labels[i];
const offset = this.getOffset(label);
if (offset === undefined || offset === -1)
throw new RangeError(`unknown label: ${label}`);
}
return this.__promote(labels);
}
// eslint-disable-next-line class-methods-use-this
isubset(offsets) {
/* validate subset */
const { rindex } = this;
const maxOffset = rindex.length;
const labels = new Int32Array(offsets.length);
for (let i = 0, l = offsets.length; i < l; i += 1) {
const offset = offsets[i];
if (offset < 0 || offset >= maxOffset)
throw new RangeError(`out of bounds offset: ${offset}`);
labels[i] = rindex[offset];
}
return this.__promote(labels);
} }
withLabel(label) { withLabel(label) {
return this.__promote([...this.keys(), label]); return this.__promote([...this.labels(), label]);
} }
withLabels(labels) { withLabels(labels) {
return this.__promote([...this.keys(), ...labels]); return this.__promote([...this.labels(), ...labels]);
} }
dropLabel(label) { dropLabel(label) {
const labelArray = [...this.keys()]; const labelArray = [...this.labels()];
labelArray.splice(labelArray.indexOf(label), 1); labelArray.splice(labelArray.indexOf(label), 1);
return this.__promote(labelArray); return this.__promote(labelArray);
} }
@@ -207,12 +276,29 @@ class KeyIndex {
this.getOffset = function getOffset(k) { this.getOffset = function getOffset(k) {
return index.get(k); return index.get(k);
}; };
this.getOffsets = function getOffsets(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = index.get(arr[i]);
}
return res;
};
this.getLabel = function getLabel(i) { this.getLabel = function getLabel(i) {
return rindex[i]; return rindex[i];
}; };
this.getLabels = function getLabels(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = rindex[arr[i]];
}
return res;
};
} }
keys() { labels() {
return this.rindex; return this.rindex;
} }
@@ -220,9 +306,30 @@ class KeyIndex {
return this.rindex.length; return this.rindex.length;
} }
subset(labels) {
/* validate subset */
for (let i = 0, l = labels.length; i < l; i += 1) {
const label = labels[i];
const offset = this.getOffset(label);
if (offset === undefined) throw new RangeError(`unknown label: ${label}`);
}
return new KeyIndex(labels);
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
subsetLabels(labelArray) { isubset(offsets) {
return new KeyIndex(labelArray); const { rindex } = this;
const maxOffset = rindex.length;
const labels = new Array(offsets.length);
for (let i = 0, l = offsets.length; i < l; i += 1) {
const offset = offsets[i];
if (offset < 0 || offset >= maxOffset)
throw new RangeError(`out of bounds offset: ${offset}`);
labels[i] = rindex[offset];
}
return new KeyIndex(labels);
} }
withLabel(label) { withLabel(label) {
-12
View File
@@ -1,12 +0,0 @@
const maybeTruncateString = (str, maxLength) => {
let truncatedString = null;
if (str.length > maxLength) {
truncatedString = `${str.slice(0, maxLength / 2)}…${str.slice(
-maxLength / 2
)}`;
}
return truncatedString;
};
export default maybeTruncateString;
@@ -100,7 +100,7 @@ export function setLabelByValue(df, colName, fromLabel, toLabel) {
/* /*
in the dataframe column `colName`, set any value of `fromLabel` to `toLabel` in the dataframe column `colName`, set any value of `fromLabel` to `toLabel`
*/ */
const keys = df.colIndex.keys(); const keys = df.colIndex.labels();
const ndf = df.mapColumns((col, colIdx) => { const ndf = df.mapColumns((col, colIdx) => {
if (colName !== keys[colIdx]) return col; if (colName !== keys[colIdx]) return col;
@@ -118,7 +118,7 @@ export function setLabelByMask(df, colName, mask, label) {
/* /*
in the dataframe column `colName`, set the masked rows to 'label' in the dataframe column `colName`, set the masked rows to 'label'
*/ */
const keys = df.colIndex.keys(); const keys = df.colIndex.labels();
const ndf = df.mapColumns((col, colIdx) => { const ndf = df.mapColumns((col, colIdx) => {
if (colName !== keys[colIdx]) return col; if (colName !== keys[colIdx]) return col;
@@ -187,7 +187,7 @@ export function pruneVarDataCache(varData, needed) {
if (numOverWatermark <= 0) return varData; if (numOverWatermark <= 0) return varData;
const { colIndex } = varData; const { colIndex } = varData;
const all = colIndex.keys(); const all = colIndex.labels();
const unused = _.difference(all, needed); const unused = _.difference(all, needed);
if (unused.length > 0) { if (unused.length > 0) {
// sort by offset in the dataframe - ie, psuedo-LRU // sort by offset in the dataframe - ie, psuedo-LRU
@@ -203,9 +203,9 @@ export function pruneVarDataCache(varData, needed) {
export function subsetAndResetGeneLists(state) { export function subsetAndResetGeneLists(state) {
const { userDefinedGenes, diffexpGenes } = state; const { userDefinedGenes, diffexpGenes } = state;
const newUserDefinedGenes = [] const newUserDefinedGenes = _.uniq(
.concat(userDefinedGenes, diffexpGenes) [].concat(userDefinedGenes, diffexpGenes)
.slice(0, globals.maxGenes); ).slice(0, globals.maxGenes);
const newDiffExpGenes = []; const newDiffExpGenes = [];
return [newUserDefinedGenes, newDiffExpGenes]; return [newUserDefinedGenes, newDiffExpGenes];
} }
+85 -4
View File
@@ -1,7 +1,12 @@
import { flatbuffers } from "flatbuffers"; import { flatbuffers } from "flatbuffers";
import { NetEncoding } from "./matrix_generated"; import { NetEncoding } from "./matrix_generated";
import { isTypedArray } from "../typeHelpers"; import { isTypedArray, isFpTypedArray } from "../typeHelpers";
import { IdentityInt32Index, DenseInt32Index, KeyIndex } from "../dataframe"; import {
Dataframe,
IdentityInt32Index,
DenseInt32Index,
KeyIndex,
} from "../dataframe";
const utf8Decoder = new TextDecoder("utf-8"); const utf8Decoder = new TextDecoder("utf-8");
@@ -133,14 +138,14 @@ export function encodeMatrixFBS(df) {
encColIndex = encodeTypedArray( encColIndex = encodeTypedArray(
builder, builder,
encColIndexUType, encColIndexUType,
df.colIndex.keys() df.colIndex.labels()
); );
} else if (colIndexType === KeyIndex) { } else if (colIndexType === KeyIndex) {
encColIndexUType = NetEncoding.TypedArray.JSONEncodedArray; encColIndexUType = NetEncoding.TypedArray.JSONEncodedArray;
encColIndex = encodeTypedArray( encColIndex = encodeTypedArray(
builder, builder,
encColIndexUType, encColIndexUType,
utf8Encoder.encode(JSON.stringify(df.colIndex.keys())) utf8Encoder.encode(JSON.stringify(df.colIndex.labels()))
); );
} else { } else {
throw new Error("Index type FBS encoding unsupported"); throw new Error("Index type FBS encoding unsupported");
@@ -162,3 +167,79 @@ export function encodeMatrixFBS(df) {
builder.finish(root); builder.finish(root);
return builder.asUint8Array(); return builder.asUint8Array();
} }
function promoteTypedArray(o) {
/*
Decide what internal data type to use for the data returned from
the server.
TODO - future optimization: not all int32/uint32 data series require
promotion to float64. We COULD simply look at the data to decide.
*/
if (isFpTypedArray(o) || Array.isArray(o)) return o;
let TyepdArrayCtor;
switch (o.constructor) {
case Int8Array:
case Uint8Array:
case Uint8ClampedArray:
case Int16Array:
case Uint16Array:
TyepdArrayCtor = Float32Array;
break;
case Int32Array:
case Uint32Array:
TyepdArrayCtor = Float64Array;
break;
default:
throw new Error("Unexpected data type returned from server.");
}
if (o.constructor === TyepdArrayCtor) return o;
return new TyepdArrayCtor(o);
}
export function matrixFBSToDataframe(arrayBuffers) {
/*
Convert array of Matrix FBS to a Dataframe.
The application has strong assumptions that all scalar data will be
stored as a float32 or float64 (regardless of underlying data types).
For example, clipping of value ranges (eg, user-selected percentiles)
depends on the ability to use NaN in any numeric type.
All float data from the server is left as is. All non-float is promoted
to an appropriate float.
*/
if (!Array.isArray(arrayBuffers)) {
arrayBuffers = [arrayBuffers];
}
if (arrayBuffers.length === 0) {
return Dataframe.Dataframe.empty();
}
const fbs = arrayBuffers.map((ab) => decodeMatrixFBS(ab, true)); // leave in place
/* check that all FBS have same row dimensionality */
const { nRows } = fbs[0];
fbs.forEach((b) => {
if (b.nRows !== nRows)
throw new Error("FBS with inconsistent dimensionality");
});
const columns = fbs
.map((fb) =>
fb.columns.map((c) => {
if (isFpTypedArray(c) || Array.isArray(c)) return c;
return promoteTypedArray(c);
})
)
.flat();
// colIdx may be TypedArray or Array
const colIdx = fbs
.map((b) => (Array.isArray(b.colIdx) ? b.colIdx : Array.from(b.colIdx)))
.flat();
const nCols = columns.length;
const df = new Dataframe([nRows, nCols], columns, null, new KeyIndex(colIdx));
return df;
}
+1 -79
View File
@@ -40,84 +40,6 @@ 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 promoteTypedArray(o) {
/*
Decide what internal data type to use for the data returned from
the server.
TODO - future optimization: not all int32/uint32 data series require
promotion to float64. We COULD simply look at the data to decide.
*/
if (isFpTypedArray(o) || Array.isArray(o)) return o;
let TyepdArrayCtor;
switch (o.constructor) {
case Int8Array:
case Uint8Array:
case Uint8ClampedArray:
case Int16Array:
case Uint16Array:
TyepdArrayCtor = Float32Array;
break;
case Int32Array:
case Uint32Array:
TyepdArrayCtor = Float64Array;
break;
default:
throw new Error("Unexpected data type returned from server.");
}
if (o.constructor === TyepdArrayCtor) return o;
return new TyepdArrayCtor(o);
}
export function matrixFBSToDataframe(arrayBuffers) {
/*
Convert array of Matrix FBS to a Dataframe.
The application has strong assumptions that all scalar data will be
stored as a float32 or float64 (regardless of underlying data types).
For example, clipping of value ranges (eg, user-selected percentiles)
depends on the ability to use NaN in any numeric type.
All float data from the server is left as is. All non-float is promoted
to an appropriate float.
*/
if (!Array.isArray(arrayBuffers)) {
arrayBuffers = [arrayBuffers];
}
if (arrayBuffers.length === 0) {
return Dataframe.Dataframe.empty();
}
const fbs = arrayBuffers.map((ab) => decodeMatrixFBS(ab, true)); // leave in place
/* check that all FBS have same row dimensionality */
const { nRows } = fbs[0];
fbs.forEach((b) => {
if (b.nRows !== nRows)
throw new Error("FBS with inconsistent dimensionality");
});
const columns = fbs
.map((fb) =>
fb.columns.map((c) => {
if (isFpTypedArray(c) || Array.isArray(c)) return c;
return promoteTypedArray(c);
})
)
.flat();
const colIdx = fbs.map((b) => b.colIdx).flat();
const nCols = columns.length;
const df = new Dataframe.Dataframe(
[nRows, nCols],
columns,
null,
new Dataframe.KeyIndex(colIdx)
);
return df;
}
export function createUniverseFromResponse(configResponse, schemaResponse) { export function createUniverseFromResponse(configResponse, schemaResponse) {
/* /*
build & return universe from a REST 0.2 /config, /schema and /annotations/obs response build & return universe from a REST 0.2 /config, /schema and /annotations/obs response
@@ -178,7 +100,7 @@ export function addObsAnnotations(universe, df) {
// for all of the new data, reconcile with schema and sort categories. // for all of the new data, reconcile with schema and sort categories.
const dfs = Array.isArray(df) ? df : [df]; const dfs = Array.isArray(df) ? df : [df];
const keys = dfs.map((d) => d.colIndex.keys()).flat(); const keys = dfs.map((d) => d.colIndex.labels()).flat();
const { schema } = universe; const { schema } = universe;
keys.forEach((k) => { keys.forEach((k) => {
const colSchema = schema.annotations.obsByName[k]; const colSchema = schema.annotations.obsByName[k];
+3 -3
View File
@@ -99,7 +99,7 @@ function clipDataframe(
if (upperQuantile > 1) upperQuantile = 1; if (upperQuantile > 1) upperQuantile = 1;
if (lowerQuantile === 0 && upperQuantile === 1) return df; if (lowerQuantile === 0 && upperQuantile === 1) return df;
const keys = df.colIndex.keys(); const keys = df.colIndex.labels();
return df.mapColumns((col, colIdx) => { return df.mapColumns((col, colIdx) => {
const colLabel = keys[colIdx]; const colLabel = keys[colIdx];
if (!clipPredicate(df, colIdx, colLabel)) return col; if (!clipPredicate(df, colIdx, colLabel)) return col;
@@ -277,7 +277,7 @@ export function addObsDimensions(crossfilter, world) {
but not yet in the crossfilter but not yet in the crossfilter
*/ */
const schema = world.schema.annotations.obsByName; const schema = world.schema.annotations.obsByName;
const dimsWeNeed = world.obsAnnotations.colIndex.keys(); const dimsWeNeed = world.obsAnnotations.colIndex.labels();
crossfilter = dimsWeNeed.reduce((xfltr, name) => { crossfilter = dimsWeNeed.reduce((xfltr, name) => {
const dimName = obsAnnoDimensionName(name); const dimName = obsAnnoDimensionName(name);
if (xfltr.hasDimension(dimName)) return xfltr; if (xfltr.hasDimension(dimName)) return xfltr;
@@ -321,7 +321,7 @@ export function getSelectedByIndex(crossfilter) {
return array of obsIndex, containing all selected obs/cells. return array of obsIndex, containing all selected obs/cells.
*/ */
const selected = crossfilter.allSelectedMask(); // array of bool-ish const selected = crossfilter.allSelectedMask(); // array of bool-ish
const keys = crossfilter.data.rowIndex.keys(); // row keys, aka universe rowIndex const keys = crossfilter.data.rowIndex.labels(); // row keys, aka universe rowIndex
const set = new Int32Array(selected.length); const set = new Int32Array(selected.length);
let numElems = 0; let numElems = 0;
@@ -60,9 +60,7 @@ export default class ImmutableTypedCrossfilter {
} }
setData(data) { setData(data) {
const { selectionCache } = this; return new ImmutableTypedCrossfilter(data, this.dimensions);
this.selectionCache = {};
return new ImmutableTypedCrossfilter(data, this.dimensions, selectionCache);
} }
dimensionNames() { dimensionNames() {
+19 -10
View File
@@ -51,11 +51,17 @@ JEST_ENV=prod make pydist install-dist dev-env smoke-test
## Server dev ## Server dev
### Install ### Install
To install from the source tree
* Build the client and put static files in place: `make build-for-server-dev` * Build the client and put static files in place: `make build-for-server-dev`
* Install from local files: `make install-dev` * Install from local files: `make install-dev`
To install from a candidate python distribution
* Make the distribution: `make pydist`
* Install it: `make install-dist`
### Launch ### Launch
* `cellxgene launch [options] <datafile>` * `cellxgene launch [options] <datafile>` or `make start-server`
### Reloading ### Reloading
If you install cellxgene using `make install-dev` the server will be restarted every time you make changes on the server code. If changes affects the client, the browser must be reloaded. If you install cellxgene using `make install-dev` the server will be restarted every time you make changes on the server code. If changes affects the client, the browser must be reloaded.
@@ -78,13 +84,13 @@ If you would like to run the server tests individually, follow the steps below
## Client dev ## Client dev
### Install ### Install
1. Install prereqs for client: `make dev-env` 1. Install prereqs for client: `make dev-env`
2. Install cellxgene server: `make install-dev` Caveat: this will not build the production client package - you must use the [server install](#install) instructions above to serve web assets. 2. Install cellxgene server as described in the [server install](#install) instructions above.
### Launch ### Launch
To launch with hot reloading you need to launch the server and the client separately. Node's hot reloading starts the client on its own node server and auto-refreshes when changes are made. To launch with hot reloading, you need to launch the server and the client separately. Node's hot reloading starts the client on its own node server and auto-refreshes when changes are made to source files.
1. Launch server (the client relies on the REST API being available): `cellxgene launch [options] <datafile>` 1. Launch server (the client relies on the REST API being available): `cellxgene launch --debug [other_options] <datafile>` or `make start-server`
2. Launch client: in `client/` directory run `npm run start` 2. Launch client: in `client/` directory run `make start-frontend`
3. Client will be served on localhost:3000 3. Client will be served on `localhost:3000`
### Build ### Build
To build only the client: `make build-client` To build only the client: `make build-client`
@@ -95,10 +101,13 @@ We use `eslint` to lint the code and `prettier` as our code formatter.
### Test ### Test
If you would like to run the client tests individually, follow the steps below in the `client` directory If you would like to run the client tests individually, follow the steps below in the `client` directory
1. For unit tests run `npm run unit-test` or `make unit-test` 1. For unit tests run `make unit-test`
1. For the smoke test run `npm run smoke-test` or `make smoke-test` 1. For the smoke test run `make smoke-test` for the standard smoke test suite and `make smoke-test-annotations` for the annotations test suite.
If you would like to run the smoke tests against a hot-reloaded version of the client:
1. Start the hot-reloading servers as described in the [Client dev section](#client-dev). If you plan to run the standard test suite (without annotations), you'll have to start the backend server with annotations disabled (e.g. `CXG_OPTIONS='--debug --disable-annotations' make start-server`).
1. From the project root, `cd client`
1. Run either the standard E2E test suite with `CXG_CLIENT_PORT=3000 make e2e` or the annotations test suite with `CXG_CLIENT_PORT=3000 make e2e-annotations`
### Tips ### Tips
* You can also install/launch the server side code from npm scrips (requires python3.6 with virtualenv) with the `scripts/backend_dev` script. * You can also install/launch the server side code from npm scrips (requires python3.6 with virtualenv) with the `scripts/backend_dev` script.
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Index | cellxgene</title> <title>Index | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Index" /> <meta property="og:title" content="Index" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebSite","url":"https://chanzuckerberg.github.io/cellxgene/","headline":"Index","name":"cellxgene","@context":"https://schema.org"}</script> {"description":"An interactive explorer for single-cell transcriptomics data","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebSite","headline":"Index","url":"https://chanzuckerberg.github.io/cellxgene/","name":"cellxgene","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>annotations | cellxgene</title> <title>annotations | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="annotations" /> <meta property="og:title" content="annotations" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/annotations.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/annotations.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Creating annotations","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/annotations.html","headline":"annotations","@context":"https://schema.org"}</script> {"description":"Creating annotations","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"annotations","url":"https://chanzuckerberg.github.io/cellxgene/posts/annotations.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Contact | cellxgene</title> <title>Contact | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Contact" /> <meta property="og:title" content="Contact" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contact.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contact.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Contact","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/contact.html","headline":"Contact","@context":"https://schema.org"}</script> {"description":"Contact","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"Contact","url":"https://chanzuckerberg.github.io/cellxgene/posts/contact.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Code of conduct | cellxgene</title> <title>Code of conduct | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Code of conduct" /> <meta property="og:title" content="Code of conduct" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contribute.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contribute.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/contribute.html","headline":"Code of conduct","@context":"https://schema.org"}</script> {"description":"An interactive explorer for single-cell transcriptomics data","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"Code of conduct","url":"https://chanzuckerberg.github.io/cellxgene/posts/contribute.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>demo-data | cellxgene</title> <title>demo-data | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="demo-data" /> <meta property="og:title" content="demo-data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Demo datasets","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html","headline":"demo-data","@context":"https://schema.org"}</script> {"description":"Demo datasets","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"demo-data","url":"https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Gallery | cellxgene</title> <title>Gallery | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Gallery" /> <meta property="og:title" content="Gallery" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/gallery.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/gallery.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/gallery.html","headline":"Gallery","@context":"https://schema.org"}</script> {"description":"An interactive explorer for single-cell transcriptomics data","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"Gallery","url":"https://chanzuckerberg.github.io/cellxgene/posts/gallery.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Hosting cellxgene on the web | cellxgene</title> <title>Hosting cellxgene on the web | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Hosting cellxgene on the web" /> <meta property="og:title" content="Hosting cellxgene on the web" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/hosted.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/hosted.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/hosted.html","headline":"Hosting cellxgene on the web","@context":"https://schema.org"}</script> {"description":"An interactive explorer for single-cell transcriptomics data","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"Hosting cellxgene on the web","url":"https://chanzuckerberg.github.io/cellxgene/posts/hosted.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Install | cellxgene</title> <title>Install | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Install" /> <meta property="og:title" content="Install" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/install.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/install.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/install.html","headline":"Install","@context":"https://schema.org"}</script> {"description":"An interactive explorer for single-cell transcriptomics data","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"Install","url":"https://chanzuckerberg.github.io/cellxgene/posts/install.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>demo-data | cellxgene</title> <title>demo-data | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="demo-data" /> <meta property="og:title" content="demo-data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/launch.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/launch.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Demo datasets","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/launch.html","headline":"demo-data","@context":"https://schema.org"}</script> {"description":"Demo datasets","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"demo-data","url":"https://chanzuckerberg.github.io/cellxgene/posts/launch.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Methods | cellxgene</title> <title>Methods | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Methods" /> <meta property="og:title" content="Methods" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/methods.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/methods.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/methods.html","headline":"Methods","@context":"https://schema.org"}</script> {"description":"An interactive explorer for single-cell transcriptomics data","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"Methods","url":"https://chanzuckerberg.github.io/cellxgene/posts/methods.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>prepare | cellxgene</title> <title>prepare | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="prepare" /> <meta property="og:title" content="prepare" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/prepare.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/prepare.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Preparing your data","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/prepare.html","headline":"prepare","@context":"https://schema.org"}</script> {"description":"Preparing your data","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"prepare","url":"https://chanzuckerberg.github.io/cellxgene/posts/prepare.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>roadmap | cellxgene</title> <title>roadmap | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="roadmap" /> <meta property="og:title" content="roadmap" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Roadmap","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html","headline":"roadmap","@context":"https://schema.org"}</script> {"description":"Roadmap","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"roadmap","url":"https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+3 -3
View File
@@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.5.0 -->
<title>Troubleshooting | cellxgene</title> <title>Troubleshooting | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.5" /> <meta name="generator" content="Jekyll v3.8.5" />
<meta property="og:title" content="Troubleshooting" /> <meta property="og:title" content="Troubleshooting" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Troubleshooting","@type":"WebPage","url":"https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html","headline":"Troubleshooting","@context":"https://schema.org"}</script> {"description":"Troubleshooting","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"@type":"WebPage","headline":"Troubleshooting","url":"https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html","@context":"http://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=b45f000ecb36779dde84b689d463d17a077275d6"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=994a7b19bb6437b8e48d92683aab7fd8077477f7">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
+61 -3
View File
@@ -125,13 +125,71 @@ with a link to embed on your own site, please drop us a note at <mailto:cellxgen
</td> </td>
</tr> </tr>
<tr> <tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_atlas_of_peripheral_immune_response_to_SARS_CoV_2_infection-25.cxg/" target="_blank">A single-cell atlas of the peripheral immune response to severe COVID-19 <td><a href="https://cellxgene.cziscience.com/d/Single_cell_atlas_of_peripheral_immune_response_to_SARS_CoV_2_infection-25.cxg/" target="_blank">A single-cell atlas of the peripheral immune response to severe COVID-19</a></td>
</a></td>
<td> <td>
<a href="https://blishlab.sites.stanford.edu/">Blish Lab</a>, <a href="https://blishlab.sites.stanford.edu/">Blish Lab</a>,
<a href="https://www.medrxiv.org/content/10.1101/2020.04.17.20069930v1">medRxiv preprint</a> <a href="https://www.medrxiv.org/content/10.1101/2020.04.17.20069930v1">medRxiv preprint</a>
</td> </td>
</tr> </tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Atlas_of_Healthy_and_SHIV_Infected_Non_Human_Primate_Lung_and_Ileum_ACE2+_Cells_ileum-12.cxg/" target="_blank">Atlas of Healthy and SHIV-Infected Non-Human Primate Lung and Ileum ACE2+ Cells - Ileum</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP807/atlas-of-healthy-and-shiv-infected-non-human-primate-lung-and-ileum-ace2-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Atlas_of_Healthy_and_SHIV_Infected_Non_Human_Primate_Lung_and_Ileum_ACE2+_Cells_lung-11.cxg/" target="_blank">Atlas of Healthy and SHIV-Infected Non-Human Primate Lung and Ileum ACE2+ Cells - Lung</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP807/atlas-of-healthy-and-shiv-infected-non-human-primate-lung-and-ileum-ace2-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_epithelial-10.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - epithelial cells</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_scraping-9.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - nasal scrapings</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_surgical-8.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - surgical</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_nasalsss-26.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - nasal SSS</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/ACE2_and_TMPRSS2_expression_in_human_non_inflamed_terminal_ileum_epithelial-7.cxg/" target="_blank">ACE2 and TMPRSS2 expression in human non-inflamed terminal ileum - epithelial cells</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP812/ace2-and-tmprss2-expression-in-human-non-inflamed-terminal-ileum?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/ACE2_and_TMPRSS2_expression_in_human_non_inflamed_terminal_ileum-6.cxg/" target="_blank">ACE2 and TMPRSS2 expression in human non-inflamed terminal ileum</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP812/ace2-and-tmprss2-expression-in-human-non-inflamed-terminal-ileum?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Human_Lung_HIV_TB_Co_infection_ACE2+_Cells-5.cxg/" target="_blank">Human Lung HIV-TB Co-infection ACE2+ Cells</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP814/human-lung-hiv-tb-co-infection-ace2-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Epithelial_Cells_in_NHP_mTB_Granuloma_and_Uninvolved_Lung-4.cxg/" target="_blank">Epithelial Cells in NHP mTB Granuloma and Uninvolved Lung</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP806/epithelial-cells-in-nhp-mtb-granuloma-and-uninvolved-lung?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
</tbody> </tbody>
</table> </table>
+67 -39
View File
@@ -58,7 +58,7 @@ def cache_control_always(**cache_kwargs):
@webbp.route("/", methods=["GET"]) @webbp.route("/", methods=["GET"])
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, max_age=ONE_WEEK)
def dataset_index(dataset=None): def dataset_index(url_dataroot=None, dataset=None):
config = current_app.app_config config = current_app.app_config
if dataset is None: if dataset is None:
if config.single_dataset__datapath: if config.single_dataset__datapath:
@@ -66,7 +66,10 @@ def dataset_index(dataset=None):
else: else:
return dataroot_index() return dataroot_index()
else: else:
location = path_join(config.multi_dataset__dataroot, dataset) dataroot = config.multi_dataset__dataroot.get(url_dataroot)
if dataroot is None:
abort(HTTPStatus.NOT_FOUND)
location = path_join(dataroot, dataset)
scripts = config.server__scripts scripts = config.server__scripts
inline_scripts = config.server__inline_scripts inline_scripts = config.server__inline_scripts
@@ -91,18 +94,21 @@ def health():
return health_check(config) return health_check(config)
def get_data_adaptor(dataset=None): def get_data_adaptor(url_dataroot=None, dataset=None):
config = current_app.app_config config = current_app.app_config
if dataset is None: if dataset is None:
datapath = config.single_dataset__datapath datapath = config.single_dataset__datapath
else: else:
datapath = path_join(config.multi_dataset__dataroot, dataset) dataroot = config.multi_dataset__dataroot.get(url_dataroot)
if dataroot is None:
raise DatasetAccessError(f"Invalid dataset {url_dataroot}/{dataset}")
datapath = path_join(dataroot, dataset)
# path_join returns a normalized path. Therefore it is # path_join returns a normalized path. Therefore it is
# sufficient to check that the datapath starts with the # sufficient to check that the datapath starts with the
# dataroot to determine that the datapath is under the dataroot. # dataroot to determine that the datapath is under the dataroot.
if not datapath.startswith(config.multi_dataset__dataroot): if not datapath.startswith(dataroot):
raise DatasetAccessError("Invalid dataset {dataset}") raise DatasetAccessError("Invalid dataset {url_dataroot}/{dataset}")
if datapath is None: if datapath is None:
return common_rest.abort_and_log(HTTPStatus.BAD_REQUEST, "Invalid dataset NONE", loglevel=logging.INFO) return common_rest.abort_and_log(HTTPStatus.BAD_REQUEST, "Invalid dataset NONE", loglevel=logging.INFO)
@@ -115,7 +121,7 @@ def rest_get_data_adaptor(func):
@wraps(func) @wraps(func)
def wrapped_function(self, dataset=None): def wrapped_function(self, dataset=None):
try: try:
with get_data_adaptor(dataset) as data_adaptor: with get_data_adaptor(self.url_dataroot, dataset) as data_adaptor:
return func(self, data_adaptor) return func(self, data_adaptor)
except DatasetAccessError: except DatasetAccessError:
return common_rest.abort_and_log( return common_rest.abort_and_log(
@@ -132,22 +138,23 @@ def dataroot_test_index():
data += "<body><H1>Welcome to cellxgene</H1>" data += "<body><H1>Welcome to cellxgene</H1>"
config = current_app.app_config config = current_app.app_config
locator = DataLocator(config.multi_dataset__dataroot, region_name=config.data_locator__s3__region_name)
datasets = [] datasets = []
for fname in locator.ls(): for url_dataroot, dataroot in config.multi_dataset__dataroot.items():
location = path_join(config.multi_dataset__dataroot, fname) locator = DataLocator(dataroot, region_name=config.data_locator__s3__region_name)
try: for fname in locator.ls():
MatrixDataLoader(location, app_config=config) location = path_join(dataroot, fname)
datasets.append(fname) try:
except DatasetAccessError: MatrixDataLoader(location, app_config=config)
# skip over invalid datasets datasets.append((url_dataroot, fname))
pass except DatasetAccessError:
# skip over invalid datasets
pass
data += "<br/>Select one of these datasets...<br/>" data += "<br/>Select one of these datasets...<br/>"
data += "<ul>" data += "<ul>"
datasets.sort() datasets.sort()
for dataset in datasets: for url_dataroot, dataset in datasets:
data += f"<li><a href=d/{dataset}>{dataset}</a></li>" data += f"<li><a href={url_dataroot}/{dataset}>{dataset}</a></li>"
data += "</ul>" data += "</ul>"
data += "</body></html>" data += "</body></html>"
@@ -165,21 +172,29 @@ def dataroot_index():
return redirect(config.multi_dataset__index) return redirect(config.multi_dataset__index)
class SchemaAPI(Resource): class DatasetResource(Resource):
"""Base class for all Resources that act on datasets."""
def __init__(self, url_dataroot):
super().__init__()
self.url_dataroot = url_dataroot
class SchemaAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, max_age=ONE_WEEK)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
return common_rest.schema_get(data_adaptor, current_app.annotations) return common_rest.schema_get(data_adaptor, current_app.annotations)
class ConfigAPI(Resource): class ConfigAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, max_age=ONE_WEEK)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
return common_rest.config_get(current_app.app_config, data_adaptor, current_app.annotations) return common_rest.config_get(current_app.app_config, data_adaptor, current_app.annotations)
class AnnotationsObsAPI(Resource): class AnnotationsObsAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, max_age=ONE_WEEK)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
@@ -191,14 +206,14 @@ class AnnotationsObsAPI(Resource):
return common_rest.annotations_obs_put(request, data_adaptor, current_app.annotations) return common_rest.annotations_obs_put(request, data_adaptor, current_app.annotations)
class AnnotationsVarAPI(Resource): class AnnotationsVarAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, max_age=ONE_WEEK)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
return common_rest.annotations_var_get(request, data_adaptor, current_app.annotations) return common_rest.annotations_var_get(request, data_adaptor, current_app.annotations)
class DataVarAPI(Resource): class DataVarAPI(DatasetResource):
@cache_control(no_store=True) @cache_control(no_store=True)
@rest_get_data_adaptor @rest_get_data_adaptor
def put(self, data_adaptor): def put(self, data_adaptor):
@@ -210,21 +225,21 @@ class DataVarAPI(Resource):
return common_rest.data_var_get(request, data_adaptor) return common_rest.data_var_get(request, data_adaptor)
class ColorsAPI(Resource): class ColorsAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, max_age=ONE_WEEK)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
return common_rest.colors_get(data_adaptor) return common_rest.colors_get(data_adaptor)
class DiffExpObsAPI(Resource): class DiffExpObsAPI(DatasetResource):
@cache_control(no_store=True) @cache_control(no_store=True)
@rest_get_data_adaptor @rest_get_data_adaptor
def post(self, data_adaptor): def post(self, data_adaptor):
return common_rest.diffexp_obs_post(request, data_adaptor) return common_rest.diffexp_obs_post(request, data_adaptor)
class LayoutObsAPI(Resource): class LayoutObsAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, max_age=ONE_WEEK)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
@@ -236,20 +251,25 @@ class LayoutObsAPI(Resource):
return common_rest.layout_obs_put(request, data_adaptor) return common_rest.layout_obs_put(request, data_adaptor)
def get_api_resources(bp_api): def get_api_resources(bp_api, url_dataroot=None):
api = Api(bp_api) api = Api(bp_api)
def add_resource(resource, url):
"""convenience function to make the outer function less verbose"""
api.add_resource(resource, url, resource_class_args=(url_dataroot,))
# Initialization routes # Initialization routes
api.add_resource(SchemaAPI, "/schema") add_resource(SchemaAPI, "/schema")
api.add_resource(ConfigAPI, "/config") add_resource(ConfigAPI, "/config")
# Data routes # Data routes
api.add_resource(AnnotationsObsAPI, "/annotations/obs") add_resource(AnnotationsObsAPI, "/annotations/obs")
api.add_resource(AnnotationsVarAPI, "/annotations/var") add_resource(AnnotationsVarAPI, "/annotations/var")
api.add_resource(DataVarAPI, "/data/var") add_resource(DataVarAPI, "/data/var")
# Display routes # Display routes
api.add_resource(ColorsAPI, "/colors") add_resource(ColorsAPI, "/colors")
# Computation routes # Computation routes
api.add_resource(DiffExpObsAPI, "/diffexp/obs") add_resource(DiffExpObsAPI, "/diffexp/obs")
api.add_resource(LayoutObsAPI, "/layout/obs") add_resource(LayoutObsAPI, "/layout/obs")
return api return api
@@ -285,10 +305,18 @@ class Server:
# NOTE: These routes only allow the dataset to be in the directory # NOTE: These routes only allow the dataset to be in the directory
# of the dataroot, and not a subdirectory. We may want to change # of the dataroot, and not a subdirectory. We may want to change
# the route format at some point # the route format at some point
bp_api = Blueprint("api_dataset", __name__, url_prefix="/d/<dataset>" + api_version) for url_dataroot in app_config.multi_dataset__dataroot.keys():
resources = get_api_resources(bp_api) bp_api = Blueprint(
self.app.register_blueprint(resources.blueprint) f"api_dataset_{url_dataroot}", __name__, url_prefix=f"/{url_dataroot}/<dataset>" + api_version
self.app.add_url_rule("/d/<dataset>/", "dataset_index", dataset_index, methods=["GET"]) )
resources = get_api_resources(bp_api, url_dataroot)
self.app.register_blueprint(resources.blueprint)
self.app.add_url_rule(
f"/{url_dataroot}/<dataset>/",
f"dataset_index_{url_dataroot}",
lambda dataset: dataset_index(url_dataroot, dataset),
methods=["GET"],
)
self.app.matrix_data_cache_manager = app_config.matrix_data_cache_manager self.app.matrix_data_cache_manager = app_config.matrix_data_cache_manager
self.app.annotations = app_config.user_annotations self.app.annotations = app_config.user_annotations
self.app.app_config = app_config self.app.app_config = app_config
+48 -14
View File
@@ -1,9 +1,9 @@
from server import __version__ as cellxgene_version from server import __version__ as cellxgene_version
from flatten_dict import flatten from flatten_dict import flatten, unflatten
import os import os
from os.path import splitext, basename, isdir from os.path import splitext, basename, isdir
import sys import sys
from urllib.parse import urlparse from urllib.parse import urlparse, quote_plus
import yaml import yaml
import copy import copy
@@ -125,17 +125,23 @@ class AppConfig(object):
dc = copy.deepcopy(config) dc = copy.deepcopy(config)
mapping = {} mapping = {}
# special case for tiledb_ctx whose value is a dict. # special cases where the value could be a dict.
val = config.get("adaptor", {}).get("cxg_adaptor", {}).get("tiledb_ctx") # If its value is not None, the entry is added to the mapping, and not included
if val is not None: # in the flattening below.
mapping["adaptor__cxg_adaptor__tiledb_ctx"] = (("adaptor", "cxg_adaptor", "tiledb_ctx"), val) dictval_cases = [
del dc["adaptor"]["cxg_adaptor"]["tiledb_ctx"] ("adaptor", "cxg_adaptor", "tiledb_ctx"),
("server", "csp_directives"),
# special case for csp_directives whose value is a dict. ("multi_dataset", "dataroot"),
val = config.get("server", {}).get("csp_directives") ]
if val is not None: for dictval_case in dictval_cases:
mapping["server__csp_directives"] = (("server", "csp_directives"), val) cur = dc
del dc["server"]["csp_directives"] for part in dictval_case[:-1]:
cur = cur.get(part, {})
val = cur.get(dictval_case[-1])
if val is not None:
key = "__".join(dictval_case)
mapping[key] = (dictval_case, val)
del cur[dictval_case[-1]]
flat_config = flatten(dc) flat_config = flatten(dc)
for key, value in flat_config.items(): for key, value in flat_config.items():
@@ -162,6 +168,14 @@ class AppConfig(object):
self.is_completed = False self.is_completed = False
def write_config(self, config_file):
"""output the config to a yaml file"""
mapping = self.__mapping(self.default_config)
for attrname in mapping.keys():
mapping[attrname] = getattr(self, attrname)
config = unflatten(mapping, splitter=lambda key: key.split("__"))
yaml.dump(config, open(config_file, "w"))
def update(self, **kw): def update(self, **kw):
for key, value in kw.items(): for key, value in kw.items():
if not hasattr(self, key): if not hasattr(self, key):
@@ -302,6 +316,14 @@ class AppConfig(object):
self.__check_attr("data_locator__s3__region_name", (type(None), bool, str)) self.__check_attr("data_locator__s3__region_name", (type(None), bool, str))
if self.data_locator__s3__region_name is True: if self.data_locator__s3__region_name is True:
path = self.single_dataset__datapath or self.multi_dataset__dataroot path = self.single_dataset__datapath or self.multi_dataset__dataroot
if type(path) == dict:
# if multi_dataset__dataroot is a dict, then use the first key
# that is in s3. NOTE: it is not supported to have dataroots
# in different regions.
paths = path.values()
for path in paths:
if path.startswith("s3://"):
break
if path.startswith("s3://"): if path.startswith("s3://"):
region_name = discover_s3_region_name(path) region_name = discover_s3_region_name(path)
if region_name is None: if region_name is None:
@@ -366,7 +388,7 @@ class AppConfig(object):
) )
def handle_multi_dataset(self, context): def handle_multi_dataset(self, context):
self.__check_attr("multi_dataset__dataroot", (type(None), str)) self.__check_attr("multi_dataset__dataroot", (type(None), dict, str))
self.__check_attr("multi_dataset__index", (type(None), bool, str)) self.__check_attr("multi_dataset__index", (type(None), bool, str))
self.__check_attr("multi_dataset__allowed_matrix_types", list) self.__check_attr("multi_dataset__allowed_matrix_types", list)
self.__check_attr("multi_dataset__matrix_cache__max_datasets", int) self.__check_attr("multi_dataset__matrix_cache__max_datasets", int)
@@ -375,6 +397,18 @@ class AppConfig(object):
if self.multi_dataset__dataroot is None: if self.multi_dataset__dataroot is None:
return return
if type(self.multi_dataset__dataroot) == str:
self.multi_dataset__dataroot = dict(d=self.multi_dataset__dataroot)
for key in self.multi_dataset__dataroot.keys():
# sanity check for well formed keys
if type(key) != str:
raise ConfigurationError(f"error in multi_dataset__dataroot {key}")
if quote_plus(key) != key:
raise ConfigurationError(f"error in multi_dataset__dataroot {key}")
if os.path.split(os.path.normpath(key))[-1] != key:
raise ConfigurationError(f"error in multi_dataset__dataroot {key}")
# error checking # error checking
for mtype in self.multi_dataset__allowed_matrix_types: for mtype in self.multi_dataset__allowed_matrix_types:
try: try:
+17
View File
@@ -29,6 +29,23 @@ presentation:
custom_colors: true custom_colors: true
multi_dataset: multi_dataset:
# If dataroot is set, then cellxgene may serve multiple datasets. This parameter is not
# compatable with single_dataset/datapath.
# dataroot may be a string, representing the path to a directory or S3 prefix. In this
# case the datasets in that location are accessed from <server>/d/<datasetname>.
# example:
# dataroot: /path/to/datasets/
# or
# dataroot: s3://bucket/prefix/
#
# As an alternative, dataroot can be a dictionary, mapping url prefixes to dataroot paths.
# example:
# dataroot:
# set1 : /path/to/set1_datasets/
# set2 : /path/to/set2_datasets/
# In this case, datasets can be accessed from <server>/set1/<datasetname> or
# <server>/set2/<datasetname>.
dataroot: null dataroot: null
# The index page when in multi-dataset mode: # The index page when in multi-dataset mode:
+7 -6
View File
@@ -23,12 +23,13 @@ def health_check(config):
""" """
health = {"status": None, "version": "1", "releaseID": cellxgene_version} health = {"status": None, "version": "1", "releaseID": cellxgene_version}
checks = [ checks = False
(config.single_dataset__datapath is not None or config.multi_dataset__dataroot is not None), if config.single_dataset__datapath is not None:
_is_accessible(config.single_dataset__datapath, config), checks = _is_accessible(config.single_dataset__datapath, config)
_is_accessible(config.multi_dataset__dataroot, config), elif config.multi_dataset__dataroot is not None:
] checks = all([_is_accessible(datapath, config) for datapath in config.multi_dataset__dataroot.values()])
health["status"] = "pass" if all(checks) else "fail"
health["status"] = "pass" if checks else "fail"
code = HTTPStatus.OK if health["status"] == "pass" else HTTPStatus.BAD_REQUEST code = HTTPStatus.OK if health["status"] == "pass" else HTTPStatus.BAD_REQUEST
response = make_response(jsonify(health), code) response = make_response(jsonify(health), code)
response.headers["Content-Type"] = "application/health+json" response.headers["Content-Type"] = "application/health+json"
+99 -14
View File
@@ -3,6 +3,8 @@ import numpy as np
from server.compute.diffexp_generic import diffexp_ttest_from_mean_var, mean_var_n from server.compute.diffexp_generic import diffexp_ttest_from_mean_var, mean_var_n
from server.data_cxg.cxg_util import pack_selector_from_indices from server.data_cxg.cxg_util import pack_selector_from_indices
from server.common.errors import ComputeError from server.common.errors import ComputeError
from numba import jit
""" """
See the comments in diffexp_generic for a description of this algorithm See the comments in diffexp_generic for a description of this algorithm
@@ -33,26 +35,33 @@ def get_thread_executor():
def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01): def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
matrix = adaptor.open_array("X")
row_selector_A = np.where(maskA)[0] row_selector_A = np.where(maskA)[0]
row_selector_B = np.where(maskB)[0] row_selector_B = np.where(maskB)[0]
nA = len(row_selector_A) nA = len(row_selector_A)
nB = len(row_selector_B) nB = len(row_selector_B)
matrix = adaptor.open_array("X")
dtype = matrix.dtype dtype = matrix.dtype
cols = matrix.shape[1] cols = matrix.shape[1]
tile_extent = [dim.tile for dim in matrix.schema.domain] tile_extent = [dim.tile for dim in matrix.schema.domain]
# The rows from both row_selector_A and row_selector_B are gathered at the is_sparse = matrix.schema.sparse
# same time, then the mean and variance are computed by subsetting on that
# combined submatrix. Combining the gather reduces number of requests/bandwidth
# to the data source.
row_selector_AB = np.union1d(row_selector_A, row_selector_B)
row_selector_A_in_AB = np.in1d(row_selector_AB, row_selector_A, assume_unique=True)
row_selector_B_in_AB = np.in1d(row_selector_AB, row_selector_B, assume_unique=True)
row_selector_AB = pack_selector_from_indices(row_selector_AB)
# because all IO is done per-tile, and we are always dense and col-major, if is_sparse:
row_selector_A = pack_selector_from_indices(row_selector_A)
row_selector_B = pack_selector_from_indices(row_selector_B)
else:
# The rows from both row_selector_A and row_selector_B are gathered at the
# same time, then the mean and variance are computed by subsetting on that
# combined submatrix. Combining the gather reduces number of requests/bandwidth
# to the data source.
row_selector_AB = np.union1d(row_selector_A, row_selector_B)
row_selector_A_in_AB = np.in1d(row_selector_AB, row_selector_A, assume_unique=True)
row_selector_B_in_AB = np.in1d(row_selector_AB, row_selector_B, assume_unique=True)
row_selector_AB = pack_selector_from_indices(row_selector_AB)
# because all IO is done per-tile, and we are always col-major,
# use the tile column size as the unit of partition. Possibly access # use the tile column size as the unit of partition. Possibly access
# more than one column tile at a time based on the target_workunit. # more than one column tile at a time based on the target_workunit.
# Revisit partitioning if we change the X layout, or start using a non-local execution environment # Revisit partitioning if we change the X layout, or start using a non-local execution environment
@@ -62,6 +71,7 @@ def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
# the target_workunit. A potential improvement would be to partition by both columns and rows. # the target_workunit. A potential improvement would be to partition by both columns and rows.
# However partitioning the rows is slightly more complex due to the arbitrary distribution # However partitioning the rows is slightly more complex due to the arbitrary distribution
# of row selections that are passed into this algorithm. # of row selections that are passed into this algorithm.
cells_per_coltile = (nA + nB) * tile_extent[1] cells_per_coltile = (nA + nB) * tile_extent[1]
cols_per_partition = max(1, int(target_workunit / cells_per_coltile)) * tile_extent[1] cols_per_partition = max(1, int(target_workunit / cells_per_coltile)) * tile_extent[1]
col_partitions = [(c, min(c + cols_per_partition, cols)) for c in range(0, cols, cols_per_partition)] col_partitions = [(c, min(c + cols_per_partition, cols)) for c in range(0, cols, cols_per_partition)]
@@ -73,10 +83,15 @@ def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
executor = get_thread_executor() executor = get_thread_executor()
futures = [] futures = []
for cols in col_partitions:
futures.append( if is_sparse:
executor.submit(_mean_var_ab, matrix, row_selector_AB, row_selector_A_in_AB, row_selector_B_in_AB, cols) for cols in col_partitions:
) futures.append(executor.submit(_mean_var_sparse_ab, matrix, row_selector_A, nA, row_selector_B, nB, cols))
else:
for cols in col_partitions:
futures.append(
executor.submit(_mean_var_ab, matrix, row_selector_AB, row_selector_A_in_AB, row_selector_B_in_AB, cols)
)
for future in futures: for future in futures:
# returns tuple: (meanA, varA, meanB, varB, cols) # returns tuple: (meanA, varA, meanB, varB, cols)
@@ -92,6 +107,12 @@ def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
future.cancel() future.cancel()
raise ComputeError(str(e)) raise ComputeError(str(e))
if is_sparse:
if adaptor.has_array("X_col_shift"):
X_col_shift = adaptor.open_array("X_col_shift")[:]
meanA += X_col_shift
meanB += X_col_shift
r = diffexp_ttest_from_mean_var( r = diffexp_ttest_from_mean_var(
meanA.astype(dtype), meanA.astype(dtype),
varA.astype(dtype), varA.astype(dtype),
@@ -111,3 +132,67 @@ def _mean_var_ab(matrix, row_selector_AB, row_selector_A_in_AB, row_selector_B_i
meanA, varA, n = mean_var_n(X[row_selector_A_in_AB]) meanA, varA, n = mean_var_n(X[row_selector_A_in_AB])
meanB, varB, n = mean_var_n(X[row_selector_B_in_AB]) meanB, varB, n = mean_var_n(X[row_selector_B_in_AB])
return (meanA, varA, meanB, varB, col_range) return (meanA, varA, meanB, varB, col_range)
def _mean_var_sparse_ab(matrix, row_selector_A, nrows_A, row_selector_B, nrows_B, col_range):
meanA, varA = _mean_var_sparse(matrix, row_selector_A, nrows_A, col_range)
meanB, varB = _mean_var_sparse(matrix, row_selector_B, nrows_B, col_range)
return (meanA, varA, meanB, varB, col_range)
@jit(nopython=True)
def _mean_var_sparse_numba(x, var, nrows, ncols):
"""Kernel to compute the mean and variance. It was not clear if this function
could be written using numpy, thus avoiding the loops. Therefore numba is
used here to speed things up. With numba, this function takes a negligible amount
of time compared to reading in the sparse matrix"""
mean = np.zeros((ncols,), dtype=np.float64)
for col, val in zip(var, x):
mean[col] += val
mean /= nrows
# optimize the sumsq computation.
# since most entries in a sparse matrix are 0, then start by assuming
# all values are 0, so fill the sumsq array with nrows * (0 - mean)**2.
# as non-zero values are encountered, subtract off the (mean*mean) value
# and replace with (val-mean)**2. Simplifying the expression
# gives the following code.
sumsq = nrows * np.multiply(mean, mean)
for col, val in zip(var, x):
sumsq[col] += val * (val - 2 * mean[col])
v = sumsq / (nrows - 1)
return mean, v
def _mean_var_sparse(matrix, selector, nrows, col_range):
data = matrix.multi_index[selector, col_range[0] : col_range[1] - 1]
x = data[""]
# tiledb < 0.6.0 and >= 0.6.0 have slightly different interfaces.
# the following takes care of both cases:
# older: data["coords]["var"]
# newer: data["var"]
var = data.get("coords", data)["var"]
# shift the column indices to start at 0, this
# will become the index into the mean and var arrays.
var -= col_range[0]
fp_err_occurred = False
def fp_err_set(err, flag):
nonlocal fp_err_occurred
fp_err_occurred = True
ncols = col_range[1] - col_range[0]
with np.errstate(divide="call", invalid="call", call=fp_err_set):
mean, v = _mean_var_sparse_numba(x, var, nrows, ncols)
if fp_err_occurred:
mean[np.isfinite(mean) == False] = 0 # noqa: E712
v[np.isfinite(v) == False] = 0 # noqa: E712
else:
mean[np.isnan(mean)] = 0
v[np.isnan(v)] = 0
return mean, v
+171 -28
View File
@@ -8,8 +8,11 @@ The organization of the TileDB structure is:
├─ obs TileDB array containing cell (row) attributes, one attribute per ├─ obs TileDB array containing cell (row) attributes, one attribute per
│ dataframe column, shape (n_obs,) │ dataframe column, shape (n_obs,)
├─ var TileDB array containing gene (column) attributes, with one attribute per ├─ var TileDB array containing gene (column) attributes, with one attribute per
│ dataframe column, shape (n_obs,) │ dataframe column, shape (n_var,)
├─ X Main count matrix as a 2D TileDB array, single unnamed numeric attribute ├─ X Main count matrix as a 2D TileDB array, single unnamed numeric attribute
├─ X_col_shift TilebDB Array used in column shift encoding, shape (n_var,), dtype = X.dtype.
│ Single unnamed numeric attribute. If this array is sparse, and X_col_shift exists,
│ then all values in the i'th column were subtracted by X_col_shift[i].
├─ emb TileDB group, storing optional embeddings (group may be empty) ├─ emb TileDB group, storing optional embeddings (group may be empty)
│ └─ <name1> TileDB Array, single anon attribute, ND numeric array, shape (n_obs, N) │ └─ <name1> TileDB Array, single anon attribute, ND numeric array, shape (n_obs, N)
└─ cxg_group_metadata Empty array used only to stash metadata about the overall object. └─ cxg_group_metadata Empty array used only to stash metadata about the overall object.
@@ -70,6 +73,7 @@ import argparse
import numpy as np import numpy as np
from os.path import splitext, basename from os.path import splitext, basename
import json import json
from scipy.stats import mode
from server.common.colors import convert_anndata_category_colors_to_cxg_category_colors from server.common.colors import convert_anndata_category_colors_to_cxg_category_colors
from server.common.errors import ColorFormatException from server.common.errors import ColorFormatException
@@ -114,6 +118,13 @@ def main():
help="URL providing more information about the dataset (hint: must be a fully specified absolute URL).", help="URL providing more information about the dataset (hint: must be a fully specified absolute URL).",
) )
parser.add_argument("--out", "--output", "-o", help="output CXG file name") parser.add_argument("--out", "--output", "-o", help="output CXG file name")
parser.add_argument(
"--sparse-threshold",
"-s",
type=float,
default=0.0, # force dense by default
help="The X array will be sparse if the percent of non-zeros falls below this value",
)
args = parser.parse_args() args = parser.parse_args()
global log_level global log_level
@@ -135,12 +146,15 @@ def main():
obs_names=args.obs_names, obs_names=args.obs_names,
about=args.about, about=args.about,
extract_colors=not args.disable_custom_colors, extract_colors=not args.disable_custom_colors,
sparse_threshold=args.sparse_threshold,
) )
log(1, "done") log(1, "done")
def write_cxg(adata, container, title, var_names=None, obs_names=None, about=None, extract_colors=False): def write_cxg(
adata, container, title, var_names=None, obs_names=None, about=None, extract_colors=False, sparse_threshold=5.0
):
if not adata.var.index.is_unique: if not adata.var.index.is_unique:
raise ValueError("Variable index is not unique - unable to convert.") raise ValueError("Variable index is not unique - unable to convert.")
if not adata.obs.index.is_unique: if not adata.obs.index.is_unique:
@@ -195,7 +209,7 @@ def write_cxg(adata, container, title, var_names=None, obs_names=None, about=Non
log(1, "\t...embeddings created") log(1, "\t...embeddings created")
# X matrix # X matrix
save_X(container, adata, ctx) save_X(container, adata.X, ctx, sparse_threshold)
log(1, "\t...X created") log(1, "\t...X created")
@@ -366,7 +380,7 @@ def create_emb(e_name, emb):
dims = [] dims = []
for d in range(emb.ndim): for d in range(emb.ndim):
shape = emb.shape shape = emb.shape
dims.append(tiledb.Dim("", domain=(0, shape[d] - 1), tile=min(shape[d], 1000), dtype=np.uint32)) dims.append(tiledb.Dim(domain=(0, shape[d] - 1), tile=min(shape[d], 1000), dtype=np.uint32))
domain = tiledb.Domain(*dims) domain = tiledb.Domain(*dims)
schema = tiledb.ArraySchema( schema = tiledb.ArraySchema(
domain=domain, sparse=False, attrs=attrs, capacity=1_000_000, cell_order="row-major", tile_order="row-major" domain=domain, sparse=False, attrs=attrs, capacity=1_000_000, cell_order="row-major", tile_order="row-major"
@@ -398,46 +412,175 @@ def save_embeddings(container, adata, ctx):
log(1, f"\t\t...{name} embedding created") log(1, f"\t\t...{name} embedding created")
def create_X(X_name, shape): def create_X(X_name, shape, is_sparse):
""" """
Dense, always. Future task: explore if sparse encoding is worth the trouble The X matrix is accessed in both row and column oriented patterns, depending on the
below a sparsity threshold.
The X matrix is access in both row and column oriented patterns, depending on the
particular operation. Because of the data type, default compression works best. particular operation. Because of the data type, default compression works best.
The tile size (50, 100) and global layout (row/col) was choosen empirically, by benchmarking The tile size, (50, 100) for dense, and (512,2048) for sparse,
and global layout (row/col) was chosen empirically, by benchmarking
the current cellxgene backend. the current cellxgene backend.
""" """
filters = tiledb.FilterList([tiledb.ZstdFilter()]) filters = tiledb.FilterList([tiledb.ZstdFilter()])
attrs = [tiledb.Attr(dtype=np.float32, filters=filters)] attrs = [tiledb.Attr(dtype=np.float32, filters=filters)]
domain = tiledb.Domain( if is_sparse:
tiledb.Dim(name="obs", domain=(0, shape[0] - 1), tile=min(shape[0], 50), dtype=np.uint32), domain = tiledb.Domain(
tiledb.Dim(name="var", domain=(0, shape[1] - 1), tile=min(shape[1], 100), dtype=np.uint32), tiledb.Dim(name="obs", domain=(0, shape[0] - 1), tile=min(shape[0], 512), dtype=np.uint32),
) tiledb.Dim(name="var", domain=(0, shape[1] - 1), tile=min(shape[1], 2048), dtype=np.uint32),
)
else:
domain = tiledb.Domain(
tiledb.Dim(name="obs", domain=(0, shape[0] - 1), tile=min(shape[0], 50), dtype=np.uint32),
tiledb.Dim(name="var", domain=(0, shape[1] - 1), tile=min(shape[1], 100), dtype=np.uint32),
)
schema = tiledb.ArraySchema( schema = tiledb.ArraySchema(
domain=domain, sparse=False, attrs=attrs, cell_order="row-major", tile_order="col-major" domain=domain, sparse=is_sparse, attrs=attrs, cell_order="row-major", tile_order="col-major"
) )
tiledb.DenseArray.create(X_name, schema) if is_sparse:
tiledb.SparseArray.create(X_name, schema)
else:
tiledb.DenseArray.create(X_name, schema)
def save_X(container, adata, ctx): def evaluate_for_sparse_encoding(xdata, sparse_threshold):
"""
This function determines if the X matrix has a sparsity below the sparse_threshold.
This function also returns the number of non-zeros encountered and number
of elements evaluated. This function may return before evaluating the whole X matrix
if it can be determined that X is not sparse enough.
"""
shape = xdata.shape
stride = min(int(np.power(10, np.around(np.log10(1e9 / shape[1])))), 10_000)
nnz = 0
maxnnz = int(shape[0] * shape[1] * sparse_threshold / 100)
for row in range(0, shape[0], stride):
lim = min(row + stride, shape[0])
a = xdata[row:lim, :]
if type(a) is not np.ndarray:
a = a.toarray()
nnz += np.count_nonzero(a)
if nnz > maxnnz:
return (False, nnz, lim * shape[1])
log(2, "\t...rows", lim, "of", shape[0], "nnz", nnz, "nnz percent %5.2f%%" % (100 * nnz / (lim * shape[1])))
is_sparse = (100.0 * nnz / (shape[0] * shape[1])) < sparse_threshold
return (is_sparse, nnz, shape[0] * shape[1])
def evaluate_for_sparse_column_shift_encoding(xdata, sparse_threshold):
"""Column shift encoding works by taking the most common value in each column, then
subtracting that value from each element of the column. If each column mostly contains
its most common value, then the resulting matrix can be very sparse.
This function determines if column shift encoding can be used to transform
the X matrix into a sparse matrix with a sparsity below the sparse_threshold.
If so, return the col_shift array that stores this encoding.
This function also returns the number of non-zeros encountered and number
of elements evaluated. This function may return before evaluating the whole X matrix
if it can be determined that X cannot benefit from column shift encoding.
"""
shape = xdata.shape
stride = max(1, 128_000_000 // shape[0])
col_shift = np.zeros(shape[1])
nnz = 0
maxnnz = int(shape[0] * shape[1] * sparse_threshold / 100)
for col in range(0, shape[1], stride):
lim = min(col + stride, shape[1])
a = xdata[:, col:lim]
if type(a) is not np.ndarray:
a = a.toarray()
m = mode(a)
col_shift[col:lim] = m.mode
nnz += shape[0] * (lim - col) - np.sum(m.count)
if nnz > maxnnz:
return (None, nnz, shape[0] * lim)
log(2, "\t...cols", lim, "of", shape[1], "nnz",
nnz, "nnz percent %5.2f%%" % (100 * nnz / (lim * shape[0])))
is_sparse = (100.0 * nnz / (shape[0] * shape[1])) < sparse_threshold
return (col_shift if is_sparse else None, nnz, shape[0] * shape[1])
def save_X(container, xdata, ctx, sparse_threshold, expect_sparse=False):
# Save X count matrix # Save X count matrix
X_name = f"{container}/X" X_name = f"{container}/X"
shape = adata.X.shape
create_X(X_name, shape)
shape = xdata.shape
log(1, "\t...shape:", str(shape))
col_shift = None
if sparse_threshold == 100:
is_sparse = True
elif sparse_threshold == 0:
is_sparse = False
else:
is_sparse, nnz, nelem = evaluate_for_sparse_encoding(xdata, sparse_threshold)
percent = 100.0 * nnz / nelem
if nelem != shape[0] * shape[1]:
log(1, "\t...sparse=", is_sparse, "non-zeros percent (estimate): %6.2f" % percent)
else:
log(1, "\t...sparse=", is_sparse, "non-zeros:", nnz, "percent: %6.2f" % percent)
is_sparse = percent < sparse_threshold
if not is_sparse:
col_shift, nnz, nelem = evaluate_for_sparse_column_shift_encoding(xdata, sparse_threshold)
is_sparse = col_shift is not None
percent = 100.0 * nnz / nelem
if nelem != shape[0] * shape[1]:
log(1, "\t...sparse=", is_sparse, "col shift non-zeros percent (estimate): %6.2f" % percent)
else:
log(1, "\t...sparse=", is_sparse, "col shift non-zeros:", nnz, "percent: %6.2f" % percent)
if expect_sparse is True and is_sparse is False:
return False
create_X(X_name, shape, is_sparse)
stride = min(int(np.power(10, np.around(np.log10(1e9 / shape[1])))), 10_000) stride = min(int(np.power(10, np.around(np.log10(1e9 / shape[1])))), 10_000)
with tiledb.DenseArray(X_name, mode="w", ctx=ctx) as X: if is_sparse:
for row in range(0, shape[0], stride): if col_shift is not None:
lim = min(row + stride, shape[0]) log(1, "\t...output X as sparse matrix with column shift encoding")
a = adata.X[row:lim, :] X_col_shift_name = f"{container}/X_col_shift"
if type(a) is not np.ndarray: filters = tiledb.FilterList([tiledb.ZstdFilter()])
a = a.toarray() attrs = [tiledb.Attr(dtype=np.float32, filters=filters)]
X[row:lim, :] = a domain = tiledb.Domain(tiledb.Dim(domain=(0, shape[1] - 1), tile=min(shape[1], 5000), dtype=np.uint32))
log(2, "\t...rows", row, "to", lim) schema = tiledb.ArraySchema(domain=domain, attrs=attrs)
tiledb.consolidate(X_name, ctx=ctx) tiledb.DenseArray.create(X_col_shift_name, schema)
with tiledb.DenseArray(X_col_shift_name, mode="w", ctx=ctx) as X_col_shift:
X_col_shift[:] = col_shift
tiledb.consolidate(X_col_shift_name, ctx=ctx)
else:
log(1, "\t...output X as sparse matrix")
with tiledb.SparseArray(X_name, mode="w", ctx=ctx) as X:
nnz = 0
for row in range(0, shape[0], stride):
lim = min(row + stride, shape[0])
a = xdata[row:lim, :]
if type(a) is not np.ndarray:
a = a.toarray()
if col_shift is not None:
a = a - col_shift
indices = np.nonzero(a)
trow = indices[0] + row
nnz += indices[0].shape[0]
X[trow, indices[1]] = a[indices[0], indices[1]]
log(2, "\t...rows", lim, "of", shape[0], "nnz", nnz, "sparse", nnz / (lim * shape[1]))
else:
log(1, "\t...output X as dense matrix")
with tiledb.DenseArray(X_name, mode="w", ctx=ctx) as X:
for row in range(0, shape[0], stride):
lim = min(row + stride, shape[0])
a = xdata[row:lim, :]
if type(a) is not np.ndarray:
a = a.toarray()
X[row:lim, :] = a
log(2, "\t...rows", row, "to", lim)
tiledb.consolidate(X_name, ctx=ctx) tiledb.consolidate(X_name, ctx=ctx)
if hasattr(tiledb, "vacuum"):
tiledb.vacuum(X_name)
return is_sparse
def save_metadata(container, metadata_dict): def save_metadata(container, metadata_dict):
+62
View File
@@ -0,0 +1,62 @@
"""
Script to create a sparse dataset in CXG format based on an input dataset in CXG format.
The input dataset is not modified.
"""
import os
import shutil
import tiledb
import argparse
import sys
import server.converters.cxgtool as cxgtool
def main():
parser = argparse.ArgumentParser()
parser.add_argument("input", help="input cxg directory")
parser.add_argument("output", help="output cxg directory")
parser.add_argument("--overwrite", action="store_true", help="replace output cxg directory")
parser.add_argument("--verbose", "-v", action="count", default=0, help="verbose output")
parser.add_argument(
"--sparse-threshold",
"-s",
type=float,
default=5.0, # default is 5% non-zero values
help="The X array will be sparse if the percent of non-zeros falls below this value",
)
args = parser.parse_args()
if os.path.exists(args.output):
print("output dir exists:", args.output)
if args.overwrite:
print("output dir removed:", args.output)
shutil.rmtree(args.output)
else:
print("use the overwrite option to remove the output directory")
sys.exit(1)
if not os.path.isdir(args.input):
print("input is not a directory", args.input)
sys.exit(1)
shutil.copytree(args.input, args.output,
ignore=shutil.ignore_patterns("X", "X_col_shift"))
ctx = tiledb.Ctx(
{
"sm.num_reader_threads": 32,
"sm.num_writer_threads": 32,
"sm.consolidation.buffer_size": 1 * 1024 * 1024 * 1024,
}
)
with tiledb.DenseArray(os.path.join(args.input, "X"), mode="r", ctx=ctx) as X_in:
is_sparse = cxgtool.save_X(args.output, X_in, ctx, args.sparse_threshold, expect_sparse=True)
if is_sparse is False:
print("The array is not sparse, cleaning up, abort.")
shutil.rmtree(args.output)
sys.exit(1)
if __name__ == "__main__":
main()
+1 -2
View File
@@ -241,8 +241,7 @@ class DataAdaptor(metaclass=ABCMeta):
duplicate_columns = list(set(labels_df.columns) & set(obs_columns)) duplicate_columns = list(set(labels_df.columns) & set(obs_columns))
if len(duplicate_columns) > 0: if len(duplicate_columns) > 0:
raise KeyError( raise KeyError(
"Labels file may not contain column names which overlap " "Labels file may not contain column names which overlap " f"with h5ad obs columns {duplicate_columns}"
f"with h5ad obs columns {duplicate_columns}"
) )
# labels must have same count as obs annotations # labels must have same count as obs annotations
+77 -12
View File
@@ -133,6 +133,10 @@ class CxgAdaptor(DataAdaptor):
return False return False
return True return True
def has_array(self, name):
a_type = tiledb.object_type(path_join(self.url, name), ctx=self.tiledb_ctx)
return a_type == "array"
def _validate_and_initialize(self): def _validate_and_initialize(self):
""" """
remember, preload_validation() has already been called, so remember, preload_validation() has already been called, so
@@ -147,13 +151,7 @@ class CxgAdaptor(DataAdaptor):
* version 0.1 -- metadata attache to cxg_group_metadata array. * version 0.1 -- metadata attache to cxg_group_metadata array.
Same as 0, except it adds group metadata. Same as 0, except it adds group metadata.
""" """
a_type = tiledb.object_type(path_join(self.url, "cxg_group_metadata"), ctx=self.tiledb_ctx) if self.has_array("cxg_group_metadata"):
if a_type is None:
# version 0
cxg_version = "0.0"
title = None
about = None
elif a_type == "array":
# version >0 # version >0
gmd = self.open_array("cxg_group_metadata") gmd = self.open_array("cxg_group_metadata")
cxg_version = gmd.meta["cxg_version"] cxg_version = gmd.meta["cxg_version"]
@@ -161,6 +159,11 @@ class CxgAdaptor(DataAdaptor):
cxg_properties = json.loads(gmd.meta["cxg_properties"]) cxg_properties = json.loads(gmd.meta["cxg_properties"])
title = cxg_properties.get("title", None) title = cxg_properties.get("title", None)
about = cxg_properties.get("about", None) about = cxg_properties.get("about", None)
else:
# version 0
cxg_version = "0.0"
title = None
about = None
if cxg_version not in ["0.0", "0.1"]: if cxg_version not in ["0.0", "0.1"]:
raise DatasetAccessError(f"cxg matrix is not valid: {self.url}") raise DatasetAccessError(f"cxg matrix is not valid: {self.url}")
@@ -171,7 +174,11 @@ class CxgAdaptor(DataAdaptor):
@staticmethod @staticmethod
def _open_array(uri, tiledb_ctx): def _open_array(uri, tiledb_ctx):
return tiledb.DenseArray(uri, mode="r", ctx=tiledb_ctx) with tiledb.Array(uri, mode="r", ctx=tiledb_ctx) as array:
if array.schema.sparse:
return tiledb.SparseArray(uri, mode="r", ctx=tiledb_ctx)
else:
return tiledb.DenseArray(uri, mode="r", ctx=tiledb_ctx)
def open_array(self, name): def open_array(self, name):
try: try:
@@ -200,15 +207,73 @@ class CxgAdaptor(DataAdaptor):
meta = self.open_array("cxg_group_metadata").meta meta = self.open_array("cxg_group_metadata").meta
return json.loads(meta["cxg_category_colors"]) if "cxg_category_colors" in meta else dict() return json.loads(meta["cxg_category_colors"]) if "cxg_category_colors" in meta else dict()
def __remap_indices(self, coord_range, coord_mask, coord_data):
"""
This function maps the indices in coord_data, which could be in the range [0,coord_range), to
a range that only includes the number of indices encoded in coord_mask.
coord_range is the maxinum size of the range (e.g. get_shape()[0] or get_shape()[1])
coord_mask is a mask passed into the get_X_array, of size coord_range
coord_data are indices representing locations of non-zero values, in the range [0,coord_range).
For example, say
coord_mask = [1,0,1,0,0,1]
coord_data = [2,0,2,2,5]
The function computes the following:
indices = [0,2,5]
ncoord = 3
maprange = [0,1,2]
mapindex = [0,0,1,0,0,2]
coordindices = [1,0,1,1,2]
"""
if coord_mask is None:
return coord_range, coord_data
indices = np.where(coord_mask)[0]
ncoord = indices.shape[0]
maprange = np.arange(ncoord)
mapindex = np.zeros(indices[-1] + 1, dtype=int)
mapindex[indices] = maprange
coordindices = mapindex[coord_data]
return ncoord, coordindices
def get_X_array(self, obs_mask=None, var_mask=None): def get_X_array(self, obs_mask=None, var_mask=None):
obs_items = pack_selector_from_mask(obs_mask) obs_items = pack_selector_from_mask(obs_mask)
var_items = pack_selector_from_mask(var_mask) var_items = pack_selector_from_mask(var_mask)
if obs_items is None or var_items is None:
# If either zero rows or zero columns were selected, return an empty 2d array.
shape = self.get_shape()
obs_size = 0 if obs_items is None else shape[0] if obs_mask is None else np.count_nonzero(obs_mask)
var_size = 0 if var_items is None else shape[1] if var_mask is None else np.count_nonzero(var_mask)
return np.ndarray((obs_size, var_size))
X = self.open_array("X") X = self.open_array("X")
if obs_items == slice(None) and var_items == slice(None):
data = X[:, :] if X.schema.sparse:
if obs_items == slice(None) and var_items == slice(None):
data = X[:, :]
else:
data = X.multi_index[obs_items, var_items]
nrows, obsindices = self.__remap_indices(X.shape[0], obs_mask, data.get("coords", data)["obs"])
ncols, varindices = self.__remap_indices(X.shape[1], var_mask, data.get("coords", data)["var"])
densedata = np.zeros((nrows, ncols), dtype=self.get_X_array_dtype())
densedata[obsindices, varindices] = data[""]
if self.has_array("X_col_shift"):
X_col_shift = self.open_array("X_col_shift")
if var_items == slice(None):
densedata += X_col_shift[:]
else:
densedata += X_col_shift.multi_index[var_items][""]
return densedata
else: else:
data = X.multi_index[obs_items, var_items][""] if obs_items == slice(None) and var_items == slice(None):
return data data = X[:, :]
else:
data = X.multi_index[obs_items, var_items][""]
return data
def get_shape(self): def get_shape(self):
X = self.open_array("X") X = self.open_array("X")
+1 -1
View File
@@ -20,7 +20,7 @@ def pack_selector_from_mask(boolarray):
def pack_selector_from_indices(selector): def pack_selector_from_indices(selector):
if len(selector) == 0: if len(selector) == 0:
return slice(None) return None
result = [] result = []
current = slice(selector[0], selector[0]) current = slice(selector[0], selector[0])
+5
View File
@@ -0,0 +1,5 @@
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
-1
View File
@@ -67,7 +67,6 @@ class WSGIServer(Server):
"object-src": ["'none'"], "object-src": ["'none'"],
"base-uri": ["'none'"], "base-uri": ["'none'"],
"frame-ancestors": ["'none'"], "frame-ancestors": ["'none'"],
"require-trusted-types-for": ["'script'"],
} }
if not app.debug: if not app.debug:
+2 -1
View File
@@ -11,12 +11,13 @@ flask-talisman>=0.7.0
flatbuffers>=1.10.0 flatbuffers>=1.10.0
flatten-dict>=0.2.0 flatten-dict>=0.2.0
fsspec>=0.4.4 fsspec>=0.4.4
numba>=0.49.1
numpy>=1.16.0 numpy>=1.16.0
packaging>=20.0 packaging>=20.0
pandas>=0.24.2 pandas>=0.24.2
PyYAML>=5.3 PyYAML>=5.3
scipy>=1.3.0 scipy>=1.3.0
requests>=2.22.0 requests>=2.22.0
tiledb==0.5.9 tiledb>=0.5.9,!=0.6.0
s3fs>=0.4.2 s3fs>=0.4.2
gunicorn>=20.0.4 gunicorn>=20.0.4
+60 -2
View File
@@ -2,13 +2,19 @@ import random
import shutil import shutil
import string import string
import tempfile import tempfile
import requests
import time
import os
from subprocess import Popen
from os import path, popen from os import path, popen
from contextlib import contextmanager
import pandas as pd import pandas as pd
from server.common.annotations import AnnotationsLocalFile from server.common.annotations import AnnotationsLocalFile
from server.common.data_locator import DataLocator from server.common.data_locator import DataLocator
from server.common.app_config import AppConfig from server.common.app_config import AppConfig, DEFAULT_SERVER_PORT
from server.common.utils import find_available_port
from server.data_common.fbs.matrix import encode_matrix_fbs from server.data_common.fbs.matrix import encode_matrix_fbs
from server.data_common.matrix_loader import MatrixDataLoader, MatrixDataType from server.data_common.matrix_loader import MatrixDataLoader, MatrixDataType
@@ -60,7 +66,7 @@ def skip_if(condition, reason: str):
return decorator return decorator
def app_config(data_locator, backed=False): def app_config(data_locator, backed=False, extra={}):
args = { args = {
"embeddings__names": ["umap", "tsne", "pca"], "embeddings__names": ["umap", "tsne", "pca"],
"presentation__max_categories": 100, "presentation__max_categories": 100,
@@ -74,9 +80,61 @@ def app_config(data_locator, backed=False):
} }
config = AppConfig() config = AppConfig()
config.update(**args) config.update(**args)
config.update(**extra)
config.complete_config() config.complete_config()
return config return config
def random_string(n): def random_string(n):
return "".join(random.choice(string.ascii_letters) for _ in range(n)) return "".join(random.choice(string.ascii_letters) for _ in range(n))
@contextmanager
def test_server(command_line_args=[], app_config=None):
"""A context to run the cellxgene server.
Command line arguments can be passed in, as well as an app_config.
This function is meant to be used like this, for example:
with test_server(...) as server:
r = requests.get(f"{server}/...")
// check r
where the server can be accessed within the context, and is terminated when
the context is exited.
The port is automatically set using find_available_port.
The verbose flag is automatically set to True.
If an app_config is provided, then this function writes a temporary
yaml config file, which this server will read and parse.
"""
port = DEFAULT_SERVER_PORT
port = find_available_port("localhost", port)
command = ["cellxgene", "--no-upgrade-check", "launch", "--verbose", "--port=%d" % port] + command_line_args
tempdir = None
if app_config:
tempdir = tempfile.TemporaryDirectory()
config_file = os.path.join(tempdir.name, "config.yaml")
app_config.write_config(config_file)
command.extend(["-c", config_file])
server = f"http://localhost:{port}"
ps = Popen(command)
for _ in range(10):
try:
requests.get(f"{server}/health")
break
except requests.exceptions.ConnectionError:
time.sleep(1)
if tempdir:
tempdir.cleanup()
try:
yield server
finally:
try:
ps.terminate()
except ProcessLookupError:
pass
+44
View File
@@ -0,0 +1,44 @@
import anndata
import argparse
import random
import scipy
import numpy as np
def main():
parser = argparse.ArgumentParser("A command to generate test h5ad files")
parser.add_argument("output", help="Name of the output file")
parser.add_argument("nobs", type=int, help="Number of observations (rows)")
parser.add_argument("nvar", type=int, help="Number of variables (columns)")
parser.add_argument("-n", "--nnz-percent", type=float, default=100, help="percent of non-zeros")
parser.add_argument("-c", "--col-shift", action="store_true", help="add a random value to each column")
parser.add_argument("--seed", type=int, default=None, help="add a random value to each column")
args = parser.parse_args()
create_test_h5ad(args.output, args.nobs, args.nvar, args.nnz_percent, args.col_shift, args.seed)
def create_test_h5ad(outfile, nobs, nvar, nnz_percent=100, apply_col_shift=False, seed=None):
random.seed(seed)
np.random.seed(seed)
x = create_X_array(nobs, nvar, nnz_percent, apply_col_shift)
obsm = {"X_random": np.random.rand(nobs, 2).astype(np.float32)}
adata = anndata.AnnData(x, obsm=obsm)
adata.write(outfile)
def create_X_array(nobs, nvar, nnz_percent, apply_col_shift):
if nnz_percent < 100:
array = scipy.sparse.random(nobs, nvar, nnz_percent * 0.01, dtype=np.float32, format="csc")
else:
array = np.random.rand(nobs, nvar).astype(np.float32)
if apply_col_shift:
col_shift = np.random.rand((nvar))
array += col_shift
return array
if __name__ == "__main__":
main()
+43 -12
View File
@@ -15,8 +15,10 @@ from server.data_cxg.cxg_adaptor import CxgAdaptor
def main(): def main():
parser = argparse.ArgumentParser("A command to test diffexp") parser = argparse.ArgumentParser("A command to test diffexp")
parser.add_argument("dataset", help="name of a dataset to load") parser.add_argument("dataset", help="name of a dataset to load")
parser.add_argument("-na", "--numA", type=int, required=True, help="number of rows in group A") parser.add_argument("-na", "--numA", type=int, help="number of rows in group A")
parser.add_argument("-nb", "--numB", type=int, required=True, help="number of rows in group B") parser.add_argument("-nb", "--numB", type=int, help="number of rows in group B")
parser.add_argument("-va", "--varA", help="obs variable:value to use for group A")
parser.add_argument("-vb", "--varB", help="obs variable:value to use for group B")
parser.add_argument("-t", "--trials", default=1, type=int, help="number of trials") parser.add_argument("-t", "--trials", default=1, type=int, help="number of trials")
parser.add_argument( parser.add_argument(
"-a", "--alg", choices=("default", "generic", "cxg"), default="default", help="algorithm to use" "-a", "--alg", choices=("default", "generic", "cxg"), default="default", help="algorithm to use"
@@ -41,22 +43,34 @@ def main():
if isinstance(adaptor, CxgAdaptor): if isinstance(adaptor, CxgAdaptor):
adaptor.open_array("X").schema.dump() adaptor.open_array("X").schema.dump()
numA = args.numA random.seed(args.seed)
numB = args.numB np.random.seed(args.seed)
rows = adaptor.get_shape()[0] rows = adaptor.get_shape()[0]
random.seed(args.seed) if args.numA:
filterA = random.sample(range(rows), args.numA)
elif args.varA:
vname, vval = args.varA.split(":")
filterA = get_filter_from_obs(adaptor, vname, vval)
else:
print("must supply numA or varA")
sys.exit(1)
if not args.new_selection: if args.numB:
samples = random.sample(range(rows), numA + numB) filterB = random.sample(range(rows), args.numB)
filterA = samples[:numA] elif args.varB:
filterB = samples[numA:] vname, vval = args.varB.split(":")
filterB = get_filter_from_obs(adaptor, vname, vval)
else:
print("must supply numB or varB")
sys.exit(1)
for i in range(args.trials): for i in range(args.trials):
if args.new_selection: if args.new_selection:
samples = random.sample(range(rows), numA + numB) if args.numA:
filterA = samples[:numA] filterA = random.sample(range(rows), args.numA)
filterB = samples[numA:] if args.numB:
filterB = random.sample(range(rows), args.numB)
maskA = np.zeros(rows, dtype=bool) maskA = np.zeros(rows, dtype=bool)
maskA[filterA] = True maskA[filterA] = True
@@ -82,5 +96,22 @@ def main():
print(res) print(res)
def get_filter_from_obs(adaptor, obsname, obsval):
attrs = adaptor.get_obs_columns()
if obsname not in attrs:
print(f"Unknown obs attr {obsname}: expected on of {attrs}")
sys.exit(1)
obsvals = adaptor.query_obs_array(obsname)[:]
obsval = type(obsvals[0])(obsval)
vfilter = np.where(obsvals == obsval)[0]
if len(vfilter) == 0:
u = np.unique(obsvals)
print(f"Unknown value in variable {obsname}:{obsval}: expected one of {list(u)}")
sys.exit(1)
return vfilter
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+13
View File
@@ -244,6 +244,19 @@ class EndPoints(object):
self.assertEqual(df["n_rows"], 2638) self.assertEqual(df["n_rows"], 2638)
self.assertEqual(df["n_cols"], 1) self.assertEqual(df["n_cols"], 1)
def test_data_get_unknown_filter_fbs(self):
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
endpoint = "data/var"
query = f"var:{index_col_name}=UNKNOWN"
url = f"{self.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"], 0)
def test_data_put_single_var(self): def test_data_put_single_var(self):
endpoint = "data/var" endpoint = "data/var"
url = f"{self.URL_BASE}{endpoint}" url = f"{self.URL_BASE}{endpoint}"
+43
View File
@@ -1,5 +1,8 @@
import unittest import unittest
from server.common.app_config import AppConfig from server.common.app_config import AppConfig
from server.common.errors import ConfigurationError
from server.test import PROJECT_ROOT, test_server
import requests
# NOTE, there are more tests that should be written for AppConfig. # NOTE, there are more tests that should be written for AppConfig.
# this is just a start. # this is just a start.
@@ -26,3 +29,43 @@ class AppConfigTest(unittest.TestCase):
c.update(server__scripts=("a", "b"), server__inline_scripts=["c", "d"]) c.update(server__scripts=("a", "b"), server__inline_scripts=["c", "d"])
v = c.changes_from_default() v = c.changes_from_default()
self.assertCountEqual(v, [("server__scripts", ["a", "b"], []), ("server__inline_scripts", ["c", "d"], [])]) self.assertCountEqual(v, [("server__scripts", ["a", "b"], []), ("server__inline_scripts", ["c", "d"], [])])
def test_multi_dataset(self):
c = AppConfig()
# test for illegal url_dataroots
for illegal in ("a/b", "../b", "!$*", "\\n", "", "(bad)"):
c.update(multi_dataset__dataroot={illegal: f"{PROJECT_ROOT}/example-dataset"})
with self.assertRaises(ConfigurationError):
c.complete_config()
# test for legal url_dataroots
for legal in (
"d",
"this.is-okay_",
):
c.update(multi_dataset__dataroot={legal: f"{PROJECT_ROOT}/example-dataset"})
c.complete_config()
# test that multi dataroots work end to end
c.update(
multi_dataset__dataroot=dict(
set1=f"{PROJECT_ROOT}/example-dataset",
set2=f"{PROJECT_ROOT}/server/test/test_datasets"
)
)
c.complete_config()
with test_server(app_config=c) as server:
session = requests.Session()
r = session.get(f"{server}/set1/pbmc3k.h5ad/api/v0.2/config")
data_config = r.json()
assert data_config["config"]["displayNames"]["dataset"] == "pbmc3k"
r = session.get(f"{server}/set2/pbmc3k.cxg/api/v0.2/config")
data_config = r.json()
assert data_config["config"]["displayNames"]["dataset"] == "pbmc3k"
r = session.get(f"{server}/health")
assert r.json()["status"] == "pass"
+84 -15
View File
@@ -1,24 +1,24 @@
import unittest import unittest
from server.data_common.matrix_loader import MatrixDataLoader from server.data_common.matrix_loader import MatrixDataLoader
from server.common.app_config import AppConfig from server.test import PROJECT_ROOT, app_config
import server.compute.diffexp_cxg as diffexp_cxg import server.compute.diffexp_cxg as diffexp_cxg
import server.compute.diffexp_generic as diffexp_generic import server.compute.diffexp_generic as diffexp_generic
from server.converters.cxgtool import write_cxg
from server.test.create_test_matrix import create_test_h5ad
from server.data_common.fbs.matrix import encode_matrix_fbs, decode_matrix_fbs
import numpy as np import numpy as np
import tempfile
from server.test import PROJECT_ROOT import os
class DiffExpTest(unittest.TestCase): class DiffExpTest(unittest.TestCase):
"""Tests the diffexp returns the expected results for one test case, using different """Tests the diffexp returns the expected results for one test case, using different
adaptor types and different algorithms.""" adaptor types and different algorithms."""
def load_dataset(self, path): def load_dataset(self, path, extra={}):
app_config = AppConfig() config = app_config(path, extra=extra)
app_config.single_dataset__datapath = path
app_config.server__verbose = True
app_config.complete_config()
loader = MatrixDataLoader(path) loader = MatrixDataLoader(path)
adaptor = loader.open(app_config) adaptor = loader.open(config)
return adaptor return adaptor
def get_mask(self, adaptor, start, stride): def get_mask(self, adaptor, start, stride):
@@ -29,6 +29,14 @@ class DiffExpTest(unittest.TestCase):
mask[sel] = True mask[sel] = True
return mask return mask
def compare_diffexp_results(self, results, expects):
self.assertEqual(len(results), len(expects))
for result, expect in zip(results, expects):
self.assertEqual(result[0], expect[0])
self.assertTrue(np.isclose(result[1], expect[1], 1e-6, 1e-4))
self.assertTrue(np.isclose(result[2], expect[2], 1e-6, 1e-4))
self.assertTrue(np.isclose(result[3], expect[3], 1e-6, 1e-4))
def check_1_10_2_10(self, results): def check_1_10_2_10(self, results):
"""Checks the results for a specific set of rows selections""" """Checks the results for a specific set of rows selections"""
expects = [ expects = [
@@ -43,12 +51,12 @@ class DiffExpTest(unittest.TestCase):
[1575, 1.0317602, 0.007830310753043345, 1.0], [1575, 1.0317602, 0.007830310753043345, 1.0],
[576, 0.97873515, 0.008272092578813124, 1.0], [576, 0.97873515, 0.008272092578813124, 1.0],
] ]
self.assertEqual(len(results), len(expects)) self.compare_diffexp_results(results, expects)
for result, expect in zip(results, expects):
self.assertEqual(result[0], expect[0]) def get_X_col(self, adaptor, cols):
self.assertAlmostEqual(result[1], expect[1]) varmask = np.zeros(adaptor.get_shape()[1], dtype=bool)
self.assertAlmostEqual(result[2], expect[2]) varmask[cols] = True
self.assertAlmostEqual(result[3], expect[3]) return adaptor.get_X_array(None, varmask)
def test_anndata_default(self): def test_anndata_default(self):
"""Test an anndata adaptor with its default diffexp algorithm (diffexp_generic)""" """Test an anndata adaptor with its default diffexp algorithm (diffexp_generic)"""
@@ -80,3 +88,64 @@ class DiffExpTest(unittest.TestCase):
# run it directly # run it directly
results = diffexp_generic.diffexp_ttest(adaptor, maskA, maskB, 10) results = diffexp_generic.diffexp_ttest(adaptor, maskA, maskB, 10)
self.check_1_10_2_10(results) self.check_1_10_2_10(results)
def test_cxg_sparse(self):
self.sparse_diffexp(False)
def test_cxg_sparse_col_shift(self):
self.sparse_diffexp(True)
def sparse_diffexp(self, apply_col_shift):
with tempfile.TemporaryDirectory() as dirname:
# create a sparse matrix
h5adfile = os.path.join(dirname, "sparse.h5ad")
create_test_h5ad(h5adfile, 2000, 2000, 10, apply_col_shift)
adaptor_anndata = self.load_dataset(h5adfile, extra=dict(embeddings__names=[]))
adata = adaptor_anndata.data
sparsename = os.path.join(dirname, "sparse.cxg")
write_cxg(adata=adata, container=sparsename, title="sparse", sparse_threshold=11)
adaptor_sparse = self.load_dataset(sparsename)
assert adaptor_sparse.open_array("X").schema.sparse
assert adaptor_sparse.has_array("X_col_shift") == apply_col_shift
densename = os.path.join(dirname, "dense.cxg")
write_cxg(adata=adata, container=densename, title="dense", sparse_threshold=0)
adaptor_dense = self.load_dataset(densename)
assert not adaptor_dense.open_array("X").schema.sparse
assert not adaptor_dense.has_array("X_col_shift")
maskA = self.get_mask(adaptor_anndata, 1, 10)
maskB = self.get_mask(adaptor_anndata, 2, 10)
diffexp_results_anndata = diffexp_generic.diffexp_ttest(adaptor_anndata, maskA, maskB, 10)
diffexp_results_sparse = diffexp_cxg.diffexp_ttest(adaptor_sparse, maskA, maskB, 10)
diffexp_results_dense = diffexp_cxg.diffexp_ttest(adaptor_dense, maskA, maskB, 10)
self.compare_diffexp_results(diffexp_results_anndata, diffexp_results_sparse)
self.compare_diffexp_results(diffexp_results_anndata, diffexp_results_dense)
topcols = np.array([x[0] for x in diffexp_results_anndata])
cols_anndata = self.get_X_col(adaptor_anndata, topcols)
cols_sparse = self.get_X_col(adaptor_sparse, topcols)
cols_dense = self.get_X_col(adaptor_dense, topcols)
assert cols_anndata.shape[0] == adaptor_sparse.get_shape()[0]
assert cols_anndata.shape[1] == len(diffexp_results_anndata)
def convert(mat, cols):
return decode_matrix_fbs(encode_matrix_fbs(mat, col_idx=cols)).to_numpy()
cols_anndata = convert(cols_anndata, topcols)
cols_sparse = convert(cols_sparse, topcols)
cols_dense = convert(cols_dense, topcols)
x = adaptor_sparse.get_X_array()
assert x.shape == adaptor_sparse.get_shape()
for row in range(cols_anndata.shape[0]):
for col in range(cols_anndata.shape[1]):
vanndata = cols_anndata[row][col]
vsparse = cols_sparse[row][col]
vdense = cols_dense[row][col]
self.assertTrue(np.isclose(vanndata, vsparse, 1e-6, 1e-6))
self.assertTrue(np.isclose(vanndata, vdense, 1e-6, 1e-6))