mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-30 08:28:12 +08:00
Merge branch 'main' into colinmegill/geneset-prototype
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
include ../common.mk
|
||||
|
||||
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../server/test/test_datasets/pbmc3k-annotations.csv)
|
||||
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../server/test/fixtures/pbmc3k-annotations.csv)
|
||||
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
|
||||
|
||||
# Packaging
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
exports[`annotations stacked bar graph renders 1`] = `
|
||||
Array [
|
||||
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
|
||||
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas class=\\"bp3-popover-targer\\" width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2132</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
|
||||
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas class=\\"bp3-popover-targer\\" width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2133</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -13,3 +13,11 @@ Array [
|
||||
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas class=\\"bp3-popover-targer\\" width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2638</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`annotations truncate midpoint whitespace 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"> 456</span><span style=\\"position: absolute; right: 0px; color: black;\\"> 456</span></span></span></span>"`;
|
||||
|
||||
exports[`annotations truncate midpoint whitespace 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"> 456</span><span style=\\"position: absolute; right: 0px; color: black;\\"> 456</span></span></span></span>"`;
|
||||
|
||||
exports[`annotations truncate single character 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
|
||||
|
||||
exports[`annotations truncate single character 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
|
||||
|
||||
@@ -27,7 +27,7 @@ export const datasets = {
|
||||
lasso: [
|
||||
{
|
||||
"coordinates-as-percent": { x1: 0.1, y1: 0.25, x2: 0.7, y2: 0.75 },
|
||||
count: "1173",
|
||||
count: "1131",
|
||||
},
|
||||
],
|
||||
categorical: [
|
||||
@@ -121,8 +121,8 @@ export const datasets = {
|
||||
},
|
||||
newCount: {
|
||||
bySubsetConfig: {
|
||||
false: "599",
|
||||
true: "594",
|
||||
false: "668",
|
||||
true: "659",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -297,6 +297,28 @@ describe.each([
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("truncate midpoint whitespace", async () => {
|
||||
await setup(config);
|
||||
const newLabelName = "123 456";
|
||||
await renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
|
||||
const value = await waitByID(
|
||||
`categorical-value-${perTestCategoryName}-${newLabelName}`
|
||||
);
|
||||
const result = await page.evaluate((elem) => elem.outerHTML, value);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("truncate single character", async () => {
|
||||
await setup(config);
|
||||
const newLabelName = "T";
|
||||
await renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
|
||||
const value = await waitByID(
|
||||
`categorical-value-${perTestCategoryName}-${newLabelName}`
|
||||
);
|
||||
const result = await page.evaluate((elem) => elem.outerHTML, value);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
async function assertCategoryExists(categoryName) {
|
||||
const handle = await waitByID(`${categoryName}:category-label`);
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@ module.exports = {
|
||||
babelrc: false,
|
||||
cacheDirectory: true,
|
||||
presets: [
|
||||
["modern-browsers", { loose: true, modules: false }],
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "entry",
|
||||
corejs: 3,
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
"@babel/preset-react",
|
||||
],
|
||||
plugins: [
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
module.exports = {
|
||||
babelrc: false,
|
||||
presets: [
|
||||
["modern-browsers", { loose: true, modules: false }],
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "entry",
|
||||
corejs: 3,
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
"@babel/preset-react",
|
||||
],
|
||||
plugins: [
|
||||
|
||||
@@ -4,9 +4,21 @@ module.exports = {
|
||||
extends: [
|
||||
"airbnb",
|
||||
"plugin:eslint-comments/recommended",
|
||||
"plugin:compat/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/react",
|
||||
],
|
||||
settings: {
|
||||
polyfills: [
|
||||
"TextDecoder",
|
||||
"TextEncoder",
|
||||
"fetch",
|
||||
"Request",
|
||||
"Response",
|
||||
"Headers",
|
||||
"AbortController",
|
||||
],
|
||||
},
|
||||
env: { browser: true, commonjs: true, es6: true },
|
||||
globals: {
|
||||
expect: true,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies -- this file is a devDependency*/
|
||||
const cheerio = require("cheerio");
|
||||
const crypto = require("crypto");
|
||||
HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
const digest = (str) => {
|
||||
const hash = crypto.createHash("sha256").update(str, "utf8").digest("base64");
|
||||
return `sha256-${hash}`;
|
||||
};
|
||||
class CspHashPlugin {
|
||||
constructor(opts) {
|
||||
this.opts = { ...opts };
|
||||
@@ -19,10 +24,7 @@ class CspHashPlugin {
|
||||
if (filename) {
|
||||
const results = {};
|
||||
results["script-hashes"] = $("script:not([src]):not([no-csp-hash])")
|
||||
.map((i, elmt) => this.digest($(elmt).html()))
|
||||
.get();
|
||||
results["style-hashes"] = $("style:not([href]):not([no-csp-hash])")
|
||||
.map((i, elmt) => this.digest($(elmt).html()))
|
||||
.map((i, elmt) => digest($(elmt).html()))
|
||||
.get();
|
||||
|
||||
const json = JSON.stringify(results);
|
||||
@@ -34,13 +36,10 @@ class CspHashPlugin {
|
||||
|
||||
// Remove no-csp-hash attributes. Cheerio does not parse Jinja templates
|
||||
// correctly, so we brute force this with a regular expression.
|
||||
data.html = data.html
|
||||
.replace(/(<script .*)no-csp-hash(.*>)/, (match, p1, p2) =>
|
||||
[p1, p2].join("")
|
||||
)
|
||||
.replace(/(<style .*)no-csp-hash(.*>)/, (match, p1, p2) =>
|
||||
[p1, p2].join("")
|
||||
);
|
||||
data.html = data.html.replace(
|
||||
/(<script .*)no-csp-hash(.*>)/,
|
||||
(match, p1, p2) => [p1, p2].join("")
|
||||
);
|
||||
|
||||
// Tell webpack to move on
|
||||
cb(null, data);
|
||||
@@ -48,14 +47,7 @@ class CspHashPlugin {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
digest(str) {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(str, "utf8")
|
||||
.digest("base64");
|
||||
return `sha256-${hash}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CspHashPlugin;
|
||||
/* eslint-enable import/no-extraneous-dependencies -- enable*/
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script>
|
||||
/** Reflect ANY changes to this script in the script hash in `server/eb/app.py` **/
|
||||
var root = document.getElementById("root");
|
||||
root.remove();
|
||||
var portals = document.getElementsByClassName("bp3-portal");
|
||||
for (var i = 0; i < portals.length; i += 1) {
|
||||
portals[i].remove();
|
||||
}
|
||||
</script>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #8080801a;
|
||||
font-family: 'Roboto Condensed, sans serif';
|
||||
"
|
||||
>
|
||||
<img
|
||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/cellxgene-logo.png"
|
||||
style="width: 320px;"
|
||||
/>
|
||||
<div
|
||||
style="
|
||||
margin-top: 16px;
|
||||
background: white;
|
||||
width: 40vw;
|
||||
border-radius: 4px;
|
||||
padding: 24px 64px;
|
||||
-webkit-box-shadow: 0px 0px 3px 2px rgba(0, 0, 0, 0.38);
|
||||
-moz-box-shadow: 0px 0px 3px 2px rgba(0, 0, 0, 0.38);
|
||||
box-shadow: 0px 0px 3px 2px rgba(0, 0, 0, 0.38);
|
||||
max-width: 550px;
|
||||
"
|
||||
>
|
||||
<div style="margin-bottom: 0; font-weight: bolder; font-size: 1.2em;">
|
||||
Unsupported Browser
|
||||
</div>
|
||||
<div style="margin-top: 0;">
|
||||
cellxgene is currently supported on the following browsers
|
||||
</div>
|
||||
<div
|
||||
style="display: flex; justify-content: space-around; margin-top: 16px;"
|
||||
>
|
||||
<a
|
||||
href="https://www.google.com/chrome/?hl=en%22"
|
||||
aria-label="Download Google Chrome"
|
||||
>
|
||||
<img
|
||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/chrome.png"
|
||||
style="width: 80px; height: 80px;"
|
||||
/>
|
||||
<div>Chrome > 60</div>
|
||||
</a>
|
||||
<a href="https://www.apple.com/safari/" aria-label="Download Safari">
|
||||
<img
|
||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/safari.png"
|
||||
style="width: 80px; height: 80px;"
|
||||
/>
|
||||
<div>Safari ≥ 10.1</div>
|
||||
</a>
|
||||
<a href="https://www.mozilla.com/firefox/" aria-label="Download Firefox">
|
||||
<img
|
||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/firefox.png"
|
||||
style="width: 80px; height: 80px;"
|
||||
/>
|
||||
<div>Firefox ≥ 60</div>
|
||||
</a>
|
||||
<a href="//www.microsoft.com/edge" aria-label="Download Edge">
|
||||
<img
|
||||
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/edge.png"
|
||||
style="width: 80px; height: 80px;"
|
||||
/>
|
||||
<div>Edge ≥ 15</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,65 +1,32 @@
|
||||
// jshint esversion: 6
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const sharedConfig = require("./webpack.config.shared");
|
||||
const babelOptions = require("../babel/babel.dev");
|
||||
|
||||
const src = path.resolve("src");
|
||||
const fonts = path.resolve("src/fonts");
|
||||
const nodeModules = path.resolve("node_modules");
|
||||
|
||||
const babelOptions = require("../babel/babel.dev");
|
||||
|
||||
module.exports = {
|
||||
const devConfig = {
|
||||
mode: "development",
|
||||
devtool: "eval",
|
||||
entry: ["./src/index"],
|
||||
output: {
|
||||
path: path.resolve("build"),
|
||||
pathinfo: true,
|
||||
filename: "static/js/bundle.js",
|
||||
publicPath: "/",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: src,
|
||||
test: /\.jsx?$/,
|
||||
loader: "babel-loader",
|
||||
options: babelOptions,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: src,
|
||||
exclude: [path.resolve(src, "index.css")],
|
||||
loader: [
|
||||
{
|
||||
loader: "style-loader",
|
||||
},
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: "[name]__[local]___[hash:base64:5]",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /index\.css$/,
|
||||
include: [path.resolve(src, "index.css")],
|
||||
loader: [
|
||||
{
|
||||
loader: "style-loader",
|
||||
},
|
||||
{
|
||||
loader: "css-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{ test: /\.json$/, include: [src, nodeModules], loader: "json-loader" },
|
||||
{
|
||||
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
|
||||
loader: "file-loader",
|
||||
@@ -88,6 +55,9 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "static/[name].css",
|
||||
}),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
__REACT_DEVTOOLS_GLOBAL_HOOK__: "({ isDisabled: true })",
|
||||
@@ -97,5 +67,10 @@ module.exports = {
|
||||
process.env.CXG_SERVER_PORT
|
||||
),
|
||||
}),
|
||||
new ScriptExtHtmlWebpackPlugin({
|
||||
async: "obsolete",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = merge(sharedConfig, devConfig);
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
// jshint esversion: 6
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const TerserJSPlugin = require("terser-webpack-plugin");
|
||||
const CleanCss = require("clean-css");
|
||||
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
|
||||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const CspHashPlugin = require("./cspHashPlugin");
|
||||
|
||||
const src = path.resolve("src");
|
||||
const fonts = path.resolve("src/fonts");
|
||||
const nodeModules = path.resolve("node_modules");
|
||||
const { merge } = require("webpack-merge");
|
||||
|
||||
const babelOptions = require("../babel/babel.prod");
|
||||
|
||||
const publicPath = "/";
|
||||
const CspHashPlugin = require("./cspHashPlugin");
|
||||
const sharedConfig = require("./webpack.config.shared");
|
||||
|
||||
module.exports = {
|
||||
const fonts = path.resolve("src/fonts");
|
||||
const nodeModules = path.resolve("node_modules");
|
||||
|
||||
const prodConfig = {
|
||||
mode: "production",
|
||||
bail: true,
|
||||
cache: false,
|
||||
entry: ["./src/index.js"],
|
||||
output: {
|
||||
filename: "static/[name]-[contenthash].js",
|
||||
path: path.resolve("build"),
|
||||
publicPath,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
@@ -41,39 +37,10 @@ module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: src,
|
||||
test: /\.jsx?$/,
|
||||
loader: "babel-loader",
|
||||
options: babelOptions,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: src,
|
||||
exclude: [path.resolve(src, "index.css")],
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: "[name]__[local]___[hash:base64:5]",
|
||||
},
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /index\.css$/,
|
||||
include: [path.resolve(src, "index.css")],
|
||||
use: [MiniCssExtractPlugin.loader, "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
include: [src, nodeModules],
|
||||
loader: "json-loader",
|
||||
exclude: /manifest.json$/,
|
||||
},
|
||||
{
|
||||
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
|
||||
loader: "file-loader",
|
||||
@@ -121,3 +88,5 @@ module.exports = {
|
||||
maxAssetSize: 2000000,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = merge(sharedConfig, prodConfig);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const ObsoleteWebpackPlugin = require("obsolete-webpack-plugin");
|
||||
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
|
||||
|
||||
const src = path.resolve("src");
|
||||
const nodeModules = path.resolve("node_modules");
|
||||
|
||||
const publicPath = "/";
|
||||
|
||||
const rawObsoleteHTMLTemplate = fs.readFileSync(
|
||||
`${__dirname}/obsoleteHTMLTemplate.html`,
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const obsoleteHTMLTemplate = rawObsoleteHTMLTemplate.replace(/'/g, '"');
|
||||
|
||||
module.exports = {
|
||||
entry: [
|
||||
"core-js",
|
||||
"regenerator-runtime/runtime",
|
||||
"fastestsmallesttextencoderdecoder",
|
||||
"whatwg-fetch",
|
||||
"abort-controller/polyfill",
|
||||
"./src/index",
|
||||
],
|
||||
output: {
|
||||
path: path.resolve("build"),
|
||||
publicPath,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: src,
|
||||
exclude: [path.resolve(src, "index.css")],
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: {
|
||||
localIdentName: "[name]__[local]___[hash:base64:5]",
|
||||
},
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /index\.css$/,
|
||||
include: [path.resolve(src, "index.css")],
|
||||
use: [MiniCssExtractPlugin.loader, "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
include: [src, nodeModules],
|
||||
loader: "json-loader",
|
||||
exclude: /manifest.json$/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ObsoleteWebpackPlugin({
|
||||
name: "obsolete",
|
||||
template: obsoleteHTMLTemplate,
|
||||
promptOnNonTargetBrowser: false,
|
||||
}),
|
||||
new ScriptExtHtmlWebpackPlugin({
|
||||
async: "obsolete",
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -39,7 +39,7 @@
|
||||
<script type="text/javascript">
|
||||
window.CELLXGENE = {};
|
||||
window.CELLXGENE.API = {
|
||||
prefix: window.location.href + "api/",
|
||||
prefix: `${location.origin}${location.pathname}api/`,
|
||||
version: "v0.2/",
|
||||
};
|
||||
</script>
|
||||
|
||||
Generated
+233
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cellxgene",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -4069,6 +4069,24 @@
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@babel/runtime-corejs2": {
|
||||
"version": "7.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.10.5.tgz",
|
||||
"integrity": "sha512-LJwyb1ac//Jr2zrGTTaNJhrP1wYCgVw9rzHbQPogKXCTLQ60EEWgeNtuqs6cLsq64O557SYzziCrOxNp0rRi8w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-js": "^2.6.5",
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/runtime-corejs3": {
|
||||
"version": "7.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz",
|
||||
@@ -5686,6 +5704,14 @@
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
|
||||
"integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg=="
|
||||
},
|
||||
"abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"requires": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -6057,6 +6083,12 @@
|
||||
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
|
||||
"integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c="
|
||||
},
|
||||
"ast-metadata-inferer": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.4.0.tgz",
|
||||
"integrity": "sha512-tKHdBe8N/Vq2nLAm4YPBVREVZjMux6KrqyPfNQgIbDl0t7HaNSmy8w4OyVHYg/cvyn5BW7o7pVwpjPte89Zhcg==",
|
||||
"dev": true
|
||||
},
|
||||
"ast-types-flow": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
|
||||
@@ -6944,6 +6976,12 @@
|
||||
"lodash.uniq": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"caniuse-db": {
|
||||
"version": "1.0.30001107",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001107.tgz",
|
||||
"integrity": "sha512-ffbV17yvEamsNm4N4dDDHdj147tWwdKw+mGyeOmvQcnu+gu455xUg8degvUOCB+fIAm7Rv3gXVn7XlTiYKymMQ==",
|
||||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001039",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001039.tgz",
|
||||
@@ -7469,9 +7507,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"codecov": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.0.tgz",
|
||||
"integrity": "sha512-uIixKofG099NbUDyzRk1HdGtaG8O+PBUAg3wfmjwXw2+ek+PZp+puRvbTohqrVfuudaezivJHFgTtSC3M8MXww==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.1.tgz",
|
||||
"integrity": "sha512-JHWxyPTkMLLJn9SmKJnwAnvY09kg2Os2+Ux+GG7LwZ9g8gzDDISpIN5wAsH1UBaafA/yGcd3KofMaorE8qd6Lw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argv": "0.0.2",
|
||||
@@ -7725,10 +7763,9 @@
|
||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"dev": true
|
||||
"version": "3.6.5",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
|
||||
"integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.6.5",
|
||||
@@ -8946,9 +8983,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
|
||||
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
|
||||
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bn.js": "^4.4.0",
|
||||
@@ -9448,6 +9485,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-compat": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-3.8.0.tgz",
|
||||
"integrity": "sha512-5CuWUSZXZkXLCQJBriEpndn/YWrvggDSHTpRJq++kR8GVcsWbTdp8Eh+nBA7JlrNi7ZJ/+kniOVXmn3bpnxuRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ast-metadata-inferer": "^0.4.0",
|
||||
"browserslist": "^4.12.2",
|
||||
"caniuse-db": "^1.0.30001090",
|
||||
"core-js": "^3.6.5",
|
||||
"find-up": "^4.1.0",
|
||||
"lodash.memoize": "4.1.2",
|
||||
"mdn-browser-compat-data": "^1.0.28",
|
||||
"semver": "7.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"browserslist": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz",
|
||||
"integrity": "sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001093",
|
||||
"electron-to-chromium": "^1.3.488",
|
||||
"escalade": "^3.0.1",
|
||||
"node-releases": "^1.1.58"
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001107",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001107.tgz",
|
||||
"integrity": "sha512-86rCH+G8onCmdN4VZzJet5uPELII59cUzDphko3thQFgAQG1RNa+sVLDoALIhRYmflo5iSIzWY3vu1XTWtNMQQ==",
|
||||
"dev": true
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.510",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.510.tgz",
|
||||
"integrity": "sha512-sLtGB0znXdmo6lM8hy5wTVo+fLqvIuO8hEpgc0DvPmFZqvBu/WB7AarEwhxVKjf3rVbws/rC8Xf+AlsOb36lJQ==",
|
||||
"dev": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.60",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz",
|
||||
"integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==",
|
||||
"dev": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-eslint-comments": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz",
|
||||
@@ -9716,6 +9841,11 @@
|
||||
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
|
||||
"dev": true
|
||||
},
|
||||
"event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
|
||||
@@ -10067,6 +10197,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
|
||||
},
|
||||
"fastestsmallesttextencoderdecoder": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz",
|
||||
"integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw=="
|
||||
},
|
||||
"favicons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/favicons/-/favicons-5.5.0.tgz",
|
||||
@@ -13886,6 +14021,15 @@
|
||||
"safe-buffer": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"mdn-browser-compat-data": {
|
||||
"version": "1.0.32",
|
||||
"resolved": "https://registry.npmjs.org/mdn-browser-compat-data/-/mdn-browser-compat-data-1.0.32.tgz",
|
||||
"integrity": "sha512-dqIstpk2ysqa6XcI8/fz1yB6bOKrIs61RIEE00Dj7+WHReXlGrCIiol1NBPsLUNE+HC/4y2f8va8vy1WsiCkAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"extend": "3.0.2"
|
||||
}
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
|
||||
@@ -14769,6 +14913,26 @@
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"obsolete-web": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/obsolete-web/-/obsolete-web-0.5.6.tgz",
|
||||
"integrity": "sha512-rrs7kSJxOVFvvY7wuCfjg/ngXbO6q1m7Llq30RaN9WYIDH1zAoA1xWLIY39D3e5967g/NeCasI/o8ojhMMkaaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime-corejs2": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"obsolete-webpack-plugin": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/obsolete-webpack-plugin/-/obsolete-webpack-plugin-0.5.6.tgz",
|
||||
"integrity": "sha512-oKlRW4ycxJfF/mojtpGuQwaP+J4JwIgjFuFnMgURB6AaKxAVaRwiO0oWhqYjwwJ5LxhVybOl+CnGAlhHBHBdEQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"browserslist": "^4.0.0",
|
||||
"obsolete-web": "^0.5.6",
|
||||
"webpack-sources": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"omggif": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
||||
@@ -16491,9 +16655,9 @@
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
"version": "0.13.7",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||
},
|
||||
"regenerator-transform": {
|
||||
"version": "0.14.5",
|
||||
@@ -17147,6 +17311,14 @@
|
||||
"ajv-keywords": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"script-ext-html-webpack-plugin": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/script-ext-html-webpack-plugin/-/script-ext-html-webpack-plugin-2.1.4.tgz",
|
||||
"integrity": "sha512-7MAv3paAMfh9y2Rg+yQKp9jEGC5cEcmdge4EomRqri10qoczmliYEVPVNz0/5e9QQ202e05qDll9B8zZlY9N1g==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
@@ -19513,6 +19685,14 @@
|
||||
"minimist": "^1.2.0",
|
||||
"request": "^2.88.0",
|
||||
"rx": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"wait-port": {
|
||||
@@ -20168,6 +20348,35 @@
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"webpack-merge": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.0.9.tgz",
|
||||
"integrity": "sha512-P4teh6O26xIDPugOGX61wPxaeP918QOMjmzhu54zTVcLtOS28ffPWtnv+ilt3wscwBUCL2WNMnh97XkrKqt9Fw==",
|
||||
"requires": {
|
||||
"clone-deep": "^4.0.1",
|
||||
"wildcard": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clone-deep": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
|
||||
"integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
|
||||
"requires": {
|
||||
"is-plain-object": "^2.0.4",
|
||||
"kind-of": "^6.0.2",
|
||||
"shallow-clone": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"shallow-clone": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
||||
"integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
|
||||
"requires": {
|
||||
"kind-of": "^6.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"webpack-sources": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
|
||||
@@ -20194,6 +20403,11 @@
|
||||
"iconv-lite": "0.4.24"
|
||||
}
|
||||
},
|
||||
"whatwg-fetch": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz",
|
||||
"integrity": "sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w=="
|
||||
},
|
||||
"whatwg-mimetype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
|
||||
@@ -20319,6 +20533,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"wildcard": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
|
||||
"integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw=="
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
|
||||
+20
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cellxgene",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"license": "MIT",
|
||||
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
||||
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
||||
@@ -29,12 +29,23 @@
|
||||
"resolutions": {
|
||||
"eslint-scope": "3.7.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Chrome > 60",
|
||||
"Safari >= 10.1",
|
||||
"iOS >= 10.3",
|
||||
"Firefox >= 60",
|
||||
"Edge >= 15",
|
||||
"not Explorer > 0"
|
||||
],
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.30.0",
|
||||
"@blueprintjs/icons": "^3.19.0",
|
||||
"@blueprintjs/select": "^3.13.5",
|
||||
"abort-controller": "^3.0.0",
|
||||
"core-js": "^3.6.5",
|
||||
"d3": "^4.10.0",
|
||||
"d3-scale-chromatic": "^1.5.0",
|
||||
"fastestsmallesttextencoderdecoder": "^1.0.22",
|
||||
"flatbuffers": "^1.11.0",
|
||||
"fuzzysort": "^1.1.4",
|
||||
"gl-mat4": "^1.2.0",
|
||||
@@ -52,8 +63,12 @@
|
||||
"react-redux": "^7.2.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"regl": "^1.6.1",
|
||||
"tinyqueue": "^2.0.3"
|
||||
"script-ext-html-webpack-plugin": "^2.1.4",
|
||||
"tinyqueue": "^2.0.3",
|
||||
"webpack-merge": "^5.0.9",
|
||||
"whatwg-fetch": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.5",
|
||||
@@ -78,13 +93,14 @@
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"clean-css": "^4.2.3",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"codecov": "^3.7.0",
|
||||
"codecov": "^3.7.1",
|
||||
"connect-history-api-fallback": "^1.6.0",
|
||||
"css-loader": "^3.6.0",
|
||||
"eslint": "^7.4.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-loader": "^3.0.4",
|
||||
"eslint-plugin-compat": "^3.8.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
@@ -107,6 +123,7 @@
|
||||
"json-loader": "^0.5.7",
|
||||
"lint-staged": "^10.2.11",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"obsolete-webpack-plugin": "^0.5.6",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"prettier": "^2.0.5",
|
||||
"puppeteer": "^3.3.0",
|
||||
|
||||
@@ -34,7 +34,8 @@ export const annotationCreateCategoryAction = (
|
||||
throw new Error("name collision on annotation category create");
|
||||
|
||||
let initialValue;
|
||||
let categories;
|
||||
let newSchema;
|
||||
let ctor;
|
||||
if (categoryToDuplicate) {
|
||||
/* if we are duplicating a category, retrieve it */
|
||||
const catDupSchema = schema.annotations.obsByName[categoryToDuplicate];
|
||||
@@ -47,25 +48,33 @@ export const annotationCreateCategoryAction = (
|
||||
.fetch("obs", categoryToDuplicate);
|
||||
const col = catToDupDf.col(categoryToDuplicate);
|
||||
initialValue = col.asArray();
|
||||
({ categories } = col.summarize());
|
||||
const { categories } = col.summarizeCategorical();
|
||||
// all user-created annotations must have the unassigned category
|
||||
if (!categories.includes(globals.unassignedCategoryLabel)) {
|
||||
categories.push(globals.unassignedCategoryLabel);
|
||||
}
|
||||
ctor = initialValue.constructor;
|
||||
newSchema = {
|
||||
...catDupSchema,
|
||||
name: newCategoryName,
|
||||
categories,
|
||||
writable: true,
|
||||
};
|
||||
} else {
|
||||
/* else assign to the standard default value */
|
||||
initialValue = globals.unassignedCategoryLabel;
|
||||
categories = [globals.unassignedCategoryLabel];
|
||||
ctor = Array;
|
||||
newSchema = {
|
||||
name: newCategoryName,
|
||||
type: "categorical",
|
||||
categories: [globals.unassignedCategoryLabel],
|
||||
writable: true,
|
||||
};
|
||||
}
|
||||
|
||||
const obsCrossfilter = prevObsCrossfilter.addObsColumn(
|
||||
{
|
||||
name: newCategoryName,
|
||||
type: "categorical",
|
||||
categories,
|
||||
writable: true,
|
||||
},
|
||||
Array,
|
||||
newSchema,
|
||||
ctor,
|
||||
initialValue
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
action creators related to embeddings choice
|
||||
*/
|
||||
|
||||
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
||||
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
|
||||
|
||||
export async function _switchEmbedding(prevAnnoMatrix, newEmbeddingName) {
|
||||
/*
|
||||
DRY helper used by this and reembedding action creators
|
||||
*/
|
||||
const base = prevAnnoMatrix.base();
|
||||
const embeddingDf = await base.fetch("emb", newEmbeddingName);
|
||||
const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
|
||||
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix).select(
|
||||
"emb",
|
||||
newEmbeddingName,
|
||||
{
|
||||
mode: "all",
|
||||
}
|
||||
);
|
||||
return [annoMatrix, obsCrossfilter];
|
||||
}
|
||||
|
||||
export const layoutChoiceAction = (newLayoutChoice) => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
/*
|
||||
On layout choice, make sure we have selected all on the previous layout, AND the new
|
||||
layout.
|
||||
*/
|
||||
const { annoMatrix: prevAnnoMatrix } = getState();
|
||||
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
|
||||
prevAnnoMatrix,
|
||||
newLayoutChoice
|
||||
);
|
||||
dispatch({
|
||||
type: "set layout choice",
|
||||
layoutChoice: newLayoutChoice,
|
||||
obsCrossfilter,
|
||||
annoMatrix,
|
||||
});
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { loadUserColorConfig } from "../util/stateManager/colorHelpers";
|
||||
import * as selnActions from "./selection";
|
||||
import * as annoActions from "./annotation";
|
||||
import * as viewActions from "./viewStack";
|
||||
import * as embActions from "./embedding";
|
||||
|
||||
/*
|
||||
return promise fetching user-configured colors
|
||||
@@ -40,6 +41,15 @@ async function configFetch(dispatch) {
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchEmbeddings(annoMatrix) {
|
||||
/*
|
||||
prefetch requests for all embeddings
|
||||
*/
|
||||
const { schema } = annoMatrix;
|
||||
const available = schema.layout.obs.map((v) => v.name);
|
||||
available.forEach((embName) => annoMatrix.prefetch("emb", embName));
|
||||
}
|
||||
|
||||
/*
|
||||
Application bootstrap
|
||||
*/
|
||||
@@ -48,7 +58,7 @@ const doInitialDataLoad = () =>
|
||||
dispatch({ type: "initial data load start" });
|
||||
|
||||
try {
|
||||
const [, schema] = await Promise.all([
|
||||
const [config, schema] = await Promise.all([
|
||||
configFetch(dispatch),
|
||||
schemaFetch(dispatch),
|
||||
userColorsFetchAndLoad(dispatch),
|
||||
@@ -57,12 +67,23 @@ const doInitialDataLoad = () =>
|
||||
const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
|
||||
const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema);
|
||||
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
||||
prefetchEmbeddings(annoMatrix);
|
||||
|
||||
dispatch({
|
||||
type: "annoMatrix: init complete",
|
||||
annoMatrix,
|
||||
obsCrossfilter,
|
||||
});
|
||||
dispatch({ type: "initial data load complete" });
|
||||
|
||||
const defaultEmbedding = config?.parameters?.["default_embedding"];
|
||||
const layoutSchema = schema?.schema?.layout?.obs ?? [];
|
||||
if (
|
||||
defaultEmbedding &&
|
||||
layoutSchema.some((s) => s.name === defaultEmbedding)
|
||||
) {
|
||||
dispatch(embActions.layoutChoiceAction(defaultEmbedding));
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch({ type: "initial data load error", error });
|
||||
}
|
||||
@@ -210,6 +231,6 @@ export default {
|
||||
annotationLabelCurrentSelection: annoActions.annotationLabelCurrentSelection,
|
||||
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
|
||||
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
|
||||
layoutChoiceAction: selnActions.layoutChoiceAction,
|
||||
layoutChoiceAction: embActions.layoutChoiceAction,
|
||||
setCellSetFromSelection: selnActions.setCellSetFromSelection,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { API } from "../globals";
|
||||
import { MatrixFBS } from "../util/stateManager";
|
||||
import {
|
||||
postNetworkErrorToast,
|
||||
postAsyncSuccessToast,
|
||||
postAsyncFailureToast,
|
||||
} from "../components/framework/toasters";
|
||||
import { _switchEmbedding } from "./embedding";
|
||||
|
||||
function abortableFetch(request, opts, timeout = 0) {
|
||||
const controller = new AbortController();
|
||||
@@ -24,7 +24,7 @@ function abortableFetch(request, opts, timeout = 0) {
|
||||
|
||||
async function doReembedFetch(dispatch, getState) {
|
||||
const state = getState();
|
||||
let cells = state.world.obsAnnotations.rowIndex.labels();
|
||||
let cells = state.annoMatrix.rowIndex.labels();
|
||||
|
||||
// These lines ensure that we convert any TypedArray to an Array.
|
||||
// This is necessary because JSON.stringify() does some very strange
|
||||
@@ -54,10 +54,7 @@ async function doReembedFetch(dispatch, getState) {
|
||||
});
|
||||
const res = await af.ready();
|
||||
|
||||
if (
|
||||
res.ok &&
|
||||
res.headers.get("Content-Type").includes("application/octet-stream")
|
||||
) {
|
||||
if (res.ok && res.headers.get("Content-Type").includes("application/json")) {
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -67,7 +64,6 @@ async function doReembedFetch(dispatch, getState) {
|
||||
if (body && body.length > 0) {
|
||||
msg = `${msg} -- ${body}`;
|
||||
}
|
||||
postNetworkErrorToast(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
@@ -78,17 +74,24 @@ export function requestReembed() {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const res = await doReembedFetch(dispatch, getState);
|
||||
const schema = JSON.parse(res.headers.get("CxG-Schema"));
|
||||
const buffer = await res.arrayBuffer();
|
||||
const df = MatrixFBS.matrixFBSToDataframe(buffer);
|
||||
const schema = await res.json();
|
||||
dispatch({
|
||||
type: "reembed: request completed",
|
||||
});
|
||||
|
||||
const { annoMatrix: prevAnnoMatrix } = getState();
|
||||
const base = prevAnnoMatrix.base().addEmbedding(schema);
|
||||
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
|
||||
base,
|
||||
schema.name
|
||||
);
|
||||
dispatch({
|
||||
type: "reembed: add reembedding",
|
||||
embedding: df,
|
||||
schema,
|
||||
annoMatrix,
|
||||
obsCrossfilter,
|
||||
});
|
||||
|
||||
postAsyncSuccessToast("Re-embedding has completed.");
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
@@ -103,13 +106,3 @@ export function requestReembed() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* disabled until reimplementation occurs
|
||||
export function reembedResetWorldToUniverse(dispatch, getState) {
|
||||
const { reembedController } = getState();
|
||||
if (reembedController.pendingFetch) reembedController.pendingFetch.abort();
|
||||
dispatch({
|
||||
type: "reembed: clear all reembeddings",
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -185,31 +185,6 @@ export const graphLassoEndAction = (embName, polygon) => async (
|
||||
});
|
||||
};
|
||||
|
||||
export const layoutChoiceAction = (newLayoutChoice) => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
/*
|
||||
On layout choice, make sure we have selected all on the previous layout, AND the new
|
||||
layout.
|
||||
*/
|
||||
const { obsCrossfilter: prevObsCrossfilter, layoutChoice } = getState();
|
||||
|
||||
let obsCrossfilter = await prevObsCrossfilter.select(
|
||||
"emb",
|
||||
layoutChoice.current,
|
||||
{ mode: "all" }
|
||||
);
|
||||
obsCrossfilter = await obsCrossfilter.select("emb", newLayoutChoice, {
|
||||
mode: "all",
|
||||
});
|
||||
dispatch({
|
||||
type: "set layout choice",
|
||||
layoutChoice: newLayoutChoice,
|
||||
obsCrossfilter,
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Differential expression set selection
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,12 @@ stack multiple subsets.
|
||||
If these conventions change, code elsewhere (eg. menubar/clip.js) will need to
|
||||
change as well.
|
||||
*/
|
||||
import { AnnoMatrixObsCrossfilter, clip, isubsetMask } from "../annoMatrix";
|
||||
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
||||
import {
|
||||
_clipAnnoMatrix,
|
||||
_userSubsetAnnoMatrix,
|
||||
_userResetSubsetAnnoMatrix,
|
||||
} from "../util/stateManager/viewStackHelpers";
|
||||
|
||||
export const clipAction = (min, max) => (dispatch, getState) => {
|
||||
/*
|
||||
@@ -19,9 +24,7 @@ export const clipAction = (min, max) => (dispatch, getState) => {
|
||||
view is ALWAYS the top view.
|
||||
*/
|
||||
const { annoMatrix: prevAnnoMatrix } = getState();
|
||||
const annoMatrix = prevAnnoMatrix.isClipped
|
||||
? clip(prevAnnoMatrix.viewOf, min, max)
|
||||
: clip(prevAnnoMatrix, min, max);
|
||||
const annoMatrix = _clipAnnoMatrix(prevAnnoMatrix, min, max);
|
||||
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
||||
dispatch({
|
||||
type: "set clip quantiles",
|
||||
@@ -43,24 +46,10 @@ export const subsetAction = () => (dispatch, getState) => {
|
||||
annoMatrix: prevAnnoMatrix,
|
||||
obsCrossfilter: prevObsCrossfilter,
|
||||
} = getState();
|
||||
|
||||
let annoMatrix;
|
||||
if (prevAnnoMatrix.isClipped) {
|
||||
// if there is a clip view, pop it and reapply after we subset
|
||||
const { clipRange } = prevAnnoMatrix;
|
||||
annoMatrix = isubsetMask(
|
||||
prevAnnoMatrix.viewOf,
|
||||
prevObsCrossfilter.allSelectedMask()
|
||||
);
|
||||
annoMatrix = clip(annoMatrix, ...clipRange);
|
||||
} else {
|
||||
// else just push a subset view.
|
||||
annoMatrix = isubsetMask(
|
||||
prevAnnoMatrix,
|
||||
prevObsCrossfilter.allSelectedMask()
|
||||
);
|
||||
}
|
||||
|
||||
const annoMatrix = _userSubsetAnnoMatrix(
|
||||
prevAnnoMatrix,
|
||||
prevObsCrossfilter.allSelectedMask()
|
||||
);
|
||||
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
||||
dispatch({
|
||||
type: "subset to selection",
|
||||
@@ -77,20 +66,7 @@ export const resetSubsetAction = () => (dispatch, getState) => {
|
||||
*/
|
||||
|
||||
const { annoMatrix: prevAnnoMatrix } = getState();
|
||||
|
||||
const clipRange = prevAnnoMatrix.isClipped ? prevAnnoMatrix.clipRange : null;
|
||||
|
||||
/* pop all views */
|
||||
let annoMatrix = prevAnnoMatrix;
|
||||
while (annoMatrix.isView) {
|
||||
annoMatrix = annoMatrix.viewOf;
|
||||
}
|
||||
|
||||
/* re-apply the clip, if any */
|
||||
if (clipRange !== null) {
|
||||
annoMatrix = clip(annoMatrix, ...clipRange);
|
||||
}
|
||||
|
||||
const annoMatrix = _userResetSubsetAnnoMatrix(prevAnnoMatrix);
|
||||
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
|
||||
dispatch({
|
||||
type: "reset subset",
|
||||
|
||||
@@ -70,6 +70,8 @@ export default class AnnoMatrix {
|
||||
The row index labels are as defined by the base dataset from the server.
|
||||
* isView - true if this is a view, false if not.
|
||||
* viewOf - pointer to parent annomatrix if a view, undefined/null if not a view.
|
||||
* userFlags - container for any additional state a user of this API wants to hang
|
||||
off of an annoMatrix, and have propagated by the (shallow) cloning protocol.
|
||||
*/
|
||||
this.schema = indexEntireSchema(schema);
|
||||
this.nObs = nObs;
|
||||
@@ -77,6 +79,7 @@ export default class AnnoMatrix {
|
||||
this.rowIndex = rowIndex || new IdentityInt32Index(nObs);
|
||||
this.isView = false;
|
||||
this.viewOf = undefined;
|
||||
this.userFlags = {};
|
||||
|
||||
/*
|
||||
Private instance variables.
|
||||
@@ -394,6 +397,19 @@ export default class AnnoMatrix {
|
||||
_subclassResponsibility();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars -- make sure subclass implements
|
||||
addEmbedding(colSchema) {
|
||||
/*
|
||||
Add a new obs embedding to the AnnoMatrix, with provided schema.
|
||||
Returns a new annomatrix.
|
||||
|
||||
Typical use will be to add a re-embedding that the server has calculated.
|
||||
|
||||
Will throw if the column schema is invalid (eg, duplicate name).
|
||||
*/
|
||||
_subclassResponsibility();
|
||||
}
|
||||
|
||||
/**
|
||||
** Private interfaces below.
|
||||
**/
|
||||
|
||||
@@ -118,6 +118,11 @@ export default class AnnoMatrixObsCrossfilter {
|
||||
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
|
||||
}
|
||||
|
||||
addEmbedding(colSchema) {
|
||||
const annoMatrix = this.annoMatrix.addEmbedding(colSchema);
|
||||
return new AnnoMatrixObsCrossfilter(annoMatrix, this.obsCrossfilter);
|
||||
}
|
||||
|
||||
/**
|
||||
Selection state - API is identical to ImmutableTypedCrossfilter, as these
|
||||
are just wrappers to lazy create indices.
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
removeObsAnnoColumn,
|
||||
addObsAnnoCategory,
|
||||
removeObsAnnoCategory,
|
||||
addObsLayout,
|
||||
} from "../util/stateManager/schemaHelpers";
|
||||
import { isArrayOrTypedArray } from "../util/typeHelpers";
|
||||
import { _whereCacheCreate } from "./whereCache";
|
||||
@@ -47,9 +48,9 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||
_writableCategoryTypeCheck(colSchema); // throws on error
|
||||
|
||||
const o = this._clone();
|
||||
o.schema = addObsAnnoCategory(this.schema, col, category);
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, category);
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
async removeObsAnnoCategory(col, category, unassignedCategory) {
|
||||
@@ -59,13 +60,17 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||
_writableCategoryTypeCheck(colSchema); // throws on error
|
||||
|
||||
const o = await this.resetObsColumnValues(
|
||||
const newAnnoMatrix = await this.resetObsColumnValues(
|
||||
col,
|
||||
category,
|
||||
unassignedCategory
|
||||
);
|
||||
o.schema = removeObsAnnoCategory(o.schema, col, category);
|
||||
return o;
|
||||
newAnnoMatrix.schema = removeObsAnnoCategory(
|
||||
newAnnoMatrix.schema,
|
||||
col,
|
||||
category
|
||||
);
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
dropObsColumn(col) {
|
||||
@@ -75,10 +80,10 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
const colSchema = _getColumnSchema(this.schema, "obs", col);
|
||||
_writableCheck(colSchema); // throws on error
|
||||
|
||||
const o = this._clone();
|
||||
o._cache.obs = this._cache.obs.dropCol(col);
|
||||
o.schema = removeObsAnnoColumn(this.schema, col);
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
||||
newAnnoMatrix.schema = removeObsAnnoColumn(this.schema, col);
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
addObsColumn(colSchema, Ctor, value) {
|
||||
@@ -90,15 +95,15 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
If an array, it must be of same size as nObs and same type as Ctor
|
||||
*/
|
||||
colSchema.writable = true;
|
||||
const col = colSchema.name;
|
||||
const colName = colSchema.name;
|
||||
if (
|
||||
_getColumnSchema(this.schema, "obs", col) ||
|
||||
this._cache.obs.hasCol(col)
|
||||
_getColumnSchema(this.schema, "obs", colName) ||
|
||||
this._cache.obs.hasCol(colName)
|
||||
) {
|
||||
throw new Error("column already exists");
|
||||
}
|
||||
|
||||
const o = this._clone();
|
||||
const newAnnoMatrix = this._clone();
|
||||
let data;
|
||||
if (isArrayOrTypedArray(value)) {
|
||||
if (value.constructor !== Ctor)
|
||||
@@ -109,12 +114,13 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
} else {
|
||||
data = new Ctor(this.nObs).fill(value);
|
||||
}
|
||||
o._cache.obs = this._cache.obs.withCol(col, data);
|
||||
o.schema = addObsAnnoColumn(this.schema, col, {
|
||||
...colSchema,
|
||||
writable: true,
|
||||
});
|
||||
return o;
|
||||
newAnnoMatrix._cache.obs = this._cache.obs.withCol(colName, data);
|
||||
_normalizeCategoricalSchema(
|
||||
colSchema,
|
||||
newAnnoMatrix._cache.obs.col(colName)
|
||||
);
|
||||
newAnnoMatrix.schema = addObsAnnoColumn(this.schema, colName, colSchema);
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
renameObsColumn(oldCol, newCol) {
|
||||
@@ -157,13 +163,13 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
data[idx] = value;
|
||||
}
|
||||
|
||||
const o = this._clone();
|
||||
o._cache.obs = this._cache.obs.replaceColData(col, data);
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix._cache.obs = this._cache.obs.replaceColData(col, data);
|
||||
const { categories } = colSchema;
|
||||
if (!categories?.includes(value)) {
|
||||
o.schema = addObsAnnoCategory(this.schema, col, value);
|
||||
newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, value);
|
||||
}
|
||||
return o;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
async resetObsColumnValues(col, oldValue, newValue) {
|
||||
@@ -187,13 +193,27 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
|
||||
if (data[i] === oldValue) data[i] = newValue;
|
||||
}
|
||||
|
||||
const o = this._clone();
|
||||
o._cache.obs = this._cache.obs.replaceColData(col, data);
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix._cache.obs = this._cache.obs.replaceColData(col, data);
|
||||
const { categories } = colSchema;
|
||||
if (!categories?.includes(newValue)) {
|
||||
o.schema = addObsAnnoCategory(this.schema, col, newValue);
|
||||
newAnnoMatrix.schema = addObsAnnoCategory(this.schema, col, newValue);
|
||||
}
|
||||
return o;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
addEmbedding(colSchema) {
|
||||
/*
|
||||
add new layout to the obs embeddings
|
||||
*/
|
||||
const { name: colName } = colSchema;
|
||||
if (_getColumnSchema(this.schema, "emb", colName)) {
|
||||
throw new Error("column already exists");
|
||||
}
|
||||
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.schema = addObsLayout(this.schema, colSchema);
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,9 +66,14 @@ export function _isContinuousType(schema) {
|
||||
|
||||
export function _normalizeCategoricalSchema(colSchema, col) {
|
||||
const { type, writable } = colSchema;
|
||||
if (type === "string" || type === "boolean" || type === "categorical") {
|
||||
if (
|
||||
type === "string" ||
|
||||
type === "boolean" ||
|
||||
type === "categorical" ||
|
||||
writable
|
||||
) {
|
||||
const categorySet = new Set(
|
||||
col.summarize().categories.concat(colSchema.categories ?? [])
|
||||
col.summarizeCategorical().categories.concat(colSchema.categories ?? [])
|
||||
);
|
||||
if (writable && !categorySet.has(unassignedCategoryLabel)) {
|
||||
categorySet.add(unassignedCategoryLabel);
|
||||
@@ -79,4 +84,5 @@ export function _normalizeCategoricalSchema(colSchema, col) {
|
||||
if (colSchema.categories) {
|
||||
colSchema.categories = catLabelSort(writable, colSchema.categories);
|
||||
}
|
||||
return colSchema;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ export function subset(annoMatrix, obsLabels) {
|
||||
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
||||
}
|
||||
|
||||
export function subsetByIndex(annoMatrix, obsIndex) {
|
||||
/*
|
||||
subset based upon the new obs index.
|
||||
*/
|
||||
return new AnnoMatrixRowSubsetView(annoMatrix, obsIndex);
|
||||
}
|
||||
|
||||
export function clip(annoMatrix, qmin, qmax) {
|
||||
/*
|
||||
Create a view that clips all continuous data to the [min, max] range.
|
||||
@@ -59,5 +66,5 @@ function _maskToList(mask) {
|
||||
elems += 1;
|
||||
}
|
||||
}
|
||||
return new Int32Array(list.buffer, 0, elems);
|
||||
return list.subarray(0, elems);
|
||||
}
|
||||
|
||||
@@ -17,59 +17,74 @@ class AnnoMatrixView extends AnnoMatrix {
|
||||
}
|
||||
|
||||
addObsAnnoCategory(col, category) {
|
||||
const o = this._clone();
|
||||
o.viewOf = this.viewOf.addObsAnnoCategory(col, category);
|
||||
o.schema = o.viewOf.schema;
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = this.viewOf.addObsAnnoCategory(col, category);
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
async removeObsAnnoCategory(col, category, unassignedCategory) {
|
||||
const o = this._clone();
|
||||
o.viewOf = await this.viewOf.removeObsAnnoCategory(
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = await this.viewOf.removeObsAnnoCategory(
|
||||
col,
|
||||
category,
|
||||
unassignedCategory
|
||||
);
|
||||
o.schema = o.viewOf.schema;
|
||||
return o;
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
dropObsColumn(col) {
|
||||
const o = this._clone();
|
||||
o.viewOf = this.viewOf.dropObsColumn(col);
|
||||
o._cache.obs = this._cache.obs.dropCol(col);
|
||||
o.schema = o.viewOf.schema;
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = this.viewOf.dropObsColumn(col);
|
||||
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
addObsColumn(colSchema, Ctor, value) {
|
||||
const o = this._clone();
|
||||
o.viewOf = this.viewOf.addObsColumn(colSchema, Ctor, value);
|
||||
o.schema = o.viewOf.schema;
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = this.viewOf.addObsColumn(colSchema, Ctor, value);
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
renameObsColumn(oldCol, newCol) {
|
||||
const o = this._clone();
|
||||
o.viewOf = this.viewOf.renameObsColumn(oldCol, newCol);
|
||||
o.schema = o.viewOf.schema;
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = this.viewOf.renameObsColumn(oldCol, newCol);
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
async setObsColumnValues(col, rowLabels, value) {
|
||||
const o = this._clone();
|
||||
o.viewOf = await this.viewOf.setObsColumnValues(col, rowLabels, value);
|
||||
o._cache.obs = this._cache.obs.dropCol(col);
|
||||
o.schema = o.viewOf.schema;
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = await this.viewOf.setObsColumnValues(
|
||||
col,
|
||||
rowLabels,
|
||||
value
|
||||
);
|
||||
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
async resetObsColumnValues(col, oldValue, newValue) {
|
||||
const o = this._clone();
|
||||
o.viewOf = await this.viewOf.resetObsColumnValues(col, oldValue, newValue);
|
||||
o._cache.obs = this._cache.obs.dropCol(col);
|
||||
o.schema = o.viewOf.schema;
|
||||
return o;
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = await this.viewOf.resetObsColumnValues(
|
||||
col,
|
||||
oldValue,
|
||||
newValue
|
||||
);
|
||||
newAnnoMatrix._cache.obs = this._cache.obs.dropCol(col);
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
|
||||
addEmbedding(colSchema) {
|
||||
const newAnnoMatrix = this._clone();
|
||||
newAnnoMatrix.viewOf = this.viewOf.addEmbedding(colSchema);
|
||||
newAnnoMatrix.schema = newAnnoMatrix.viewOf.schema;
|
||||
return newAnnoMatrix;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import Legend from "./continuousLegend";
|
||||
import Graph from "./graph/graph";
|
||||
import MenuBar from "./menubar";
|
||||
import Autosave from "./autosave";
|
||||
import Embedding from "./embedding";
|
||||
import TermsOfServicePrompt from "./termsPrompt";
|
||||
|
||||
import actions from "../actions";
|
||||
@@ -73,6 +74,7 @@ class App extends React.Component {
|
||||
{(viewportRef) => (
|
||||
<>
|
||||
<MenuBar />
|
||||
<Embedding />
|
||||
<Autosave />
|
||||
<TermsOfServicePrompt />
|
||||
<Legend viewportRef={viewportRef} />
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
@connect((state) => ({
|
||||
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
|
||||
annotations: state.annotations,
|
||||
auth: state.config?.authentication,
|
||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||
}))
|
||||
class FilenameDialog extends React.Component {
|
||||
@@ -90,12 +91,13 @@ class FilenameDialog extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { writableCategoriesEnabled, annotations, idhash } = this.props;
|
||||
const { writableCategoriesEnabled, annotations, idhash, auth } = this.props;
|
||||
const { filenameText } = this.state;
|
||||
|
||||
return writableCategoriesEnabled &&
|
||||
!annotations.dataCollectionNameIsReadOnly &&
|
||||
!annotations.dataCollectionName ? (
|
||||
!annotations.dataCollectionName &&
|
||||
auth.is_authenticated ? (
|
||||
<Dialog
|
||||
icon="tag"
|
||||
title="Annotations Collection"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, useEffect } from "react";
|
||||
import { connect, shallowEqual } from "react-redux";
|
||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
||||
import { AnchorButton, Button, Tooltip } from "@blueprintjs/core";
|
||||
import { AnchorButton, Button, Tooltip, Position } from "@blueprintjs/core";
|
||||
import { Flipper, Flipped } from "react-flip-toolkit";
|
||||
import Async from "react-async";
|
||||
import memoize from "memoize-one";
|
||||
@@ -438,9 +438,13 @@ const CategoryHeader = React.memo(
|
||||
? `Coloring by ${metadataField} is disabled, as it exceeds the limit of ${globals.maxCategoricalOptionsToDisplay} labels`
|
||||
: "Use as color scale"
|
||||
}
|
||||
position="bottom"
|
||||
usePortal={false}
|
||||
position={Position.LEFT}
|
||||
usePortal
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<AnchorButton
|
||||
data-testclass="colorby"
|
||||
|
||||
@@ -453,7 +453,9 @@ class CategoryValue extends React.Component {
|
||||
label,
|
||||
CHART_WIDTH,
|
||||
VALUE_HEIGHT
|
||||
) ?? {};
|
||||
) ?? {}; // if createHistogramBins returns empty object assign null to deconstructed
|
||||
|
||||
if (!xScale || !yScale || !bins) return null;
|
||||
|
||||
return (
|
||||
<MiniHistogram
|
||||
|
||||
@@ -16,6 +16,7 @@ class Continuous extends React.PureComponent {
|
||||
const allContinuousNames = schema.annotations.obs.columns
|
||||
.filter((col) => col.type === "int32" || col.type === "float32")
|
||||
.filter((col) => col.name !== obsIndex)
|
||||
.filter((col) => !col.writable) // skip user annotations - they will be treated as categorical
|
||||
.map((col) => col.name);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { useAsync } from "react-async";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Popover,
|
||||
Button,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tooltip,
|
||||
Position,
|
||||
} from "@blueprintjs/core";
|
||||
import * as globals from "../../globals";
|
||||
import actions from "../../actions";
|
||||
import { getDiscreteCellEmbeddingRowIndex } from "../../util/stateManager/viewStackHelpers";
|
||||
|
||||
@connect((state) => {
|
||||
return {
|
||||
layoutChoice: state.layoutChoice, // TODO: really should clean up naming, s/layout/embedding/g
|
||||
schema: state.annoMatrix?.schema,
|
||||
crossfilter: state.obsCrossfilter,
|
||||
};
|
||||
})
|
||||
class Embedding extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
handleLayoutChoiceChange = (e) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(actions.layoutChoiceAction(e.currentTarget.value));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { layoutChoice, schema, crossfilter } = this.props;
|
||||
const { annoMatrix } = crossfilter;
|
||||
return (
|
||||
<ButtonGroup
|
||||
style={{
|
||||
position: "absolute",
|
||||
display: "inherit",
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Popover
|
||||
target={
|
||||
<Tooltip
|
||||
content="Select embedding for visualization"
|
||||
position="top"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="layout-choice"
|
||||
icon="heatmap"
|
||||
// minimal
|
||||
id="embedding"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{layoutChoice?.current}: {crossfilter.countSelected()} out of{" "}
|
||||
{crossfilter.size()} cells
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
// minimal /* removes arrow */
|
||||
position={Position.TOP_LEFT}
|
||||
content={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
padding: 10,
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<h1>Embedding Choice</h1>
|
||||
<p style={{ fontStyle: "italic" }}>
|
||||
There are {schema?.dataframe?.nObs} cells in the entire dataset.
|
||||
</p>
|
||||
<EmbeddingChoices
|
||||
onChange={this.handleLayoutChoiceChange}
|
||||
annoMatrix={annoMatrix}
|
||||
layoutChoice={layoutChoice}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Embedding;
|
||||
|
||||
const loadAllEmbeddingCounts = async ({ annoMatrix, available }) => {
|
||||
const embeddings = await Promise.all(
|
||||
available.map((name) => annoMatrix.base().fetch("emb", name))
|
||||
);
|
||||
return available.map((name, idx) => ({
|
||||
embeddingName: name,
|
||||
embedding: embeddings[idx],
|
||||
discreteCellIndex: getDiscreteCellEmbeddingRowIndex(embeddings[idx]),
|
||||
}));
|
||||
};
|
||||
|
||||
const EmbeddingChoices = ({ onChange, annoMatrix, layoutChoice }) => {
|
||||
const { available } = layoutChoice;
|
||||
const { data, error, isPending } = useAsync({
|
||||
promiseFn: loadAllEmbeddingCounts,
|
||||
annoMatrix,
|
||||
available,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
/* log, as this is unexpected */
|
||||
console.error(error);
|
||||
}
|
||||
if (error || isPending) {
|
||||
/* still loading, or errored out - just omit counts (TODO: spinner?) */
|
||||
return (
|
||||
<RadioGroup onChange={onChange} selectedValue={layoutChoice.current}>
|
||||
{layoutChoice.available.map((name) => (
|
||||
<Radio label={`${name}`} value={name} key={name} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
if (data) {
|
||||
return (
|
||||
<RadioGroup onChange={onChange} selectedValue={layoutChoice.current}>
|
||||
{data.map((summary) => {
|
||||
const { discreteCellIndex, embeddingName } = summary;
|
||||
const sizeHint = `${discreteCellIndex.size()} cells`;
|
||||
return (
|
||||
<Radio
|
||||
label={`${embeddingName}: ${sizeHint}`}
|
||||
value={embeddingName}
|
||||
key={embeddingName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -34,8 +34,10 @@ function createProjectionTF(viewportWidth, viewportHeight) {
|
||||
the projection transform accounts for the screen size & other layout
|
||||
*/
|
||||
const fractionToUse = 0.95; // fraction of min dimension to use
|
||||
const topGutterSizePx = 32; // toolbar box height
|
||||
const heightMinusGutter = viewportHeight - topGutterSizePx;
|
||||
const topGutterSizePx = 32; // top gutter for tools
|
||||
const bottomGutterSizePx = 32; // bottom gutter for tools
|
||||
const heightMinusGutter =
|
||||
viewportHeight - topGutterSizePx - bottomGutterSizePx;
|
||||
const minDim = Math.min(viewportWidth, heightMinusGutter);
|
||||
const aspectScale = [
|
||||
(fractionToUse * minDim) / viewportWidth,
|
||||
@@ -44,7 +46,7 @@ function createProjectionTF(viewportWidth, viewportHeight) {
|
||||
const m = mat3.create();
|
||||
mat3.fromTranslation(m, [
|
||||
0,
|
||||
-topGutterSizePx / viewportHeight / aspectScale[1],
|
||||
(bottomGutterSizePx - topGutterSizePx) / viewportHeight / aspectScale[1],
|
||||
]);
|
||||
mat3.scale(m, m, aspectScale);
|
||||
return m;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
|
||||
const Auth = React.memo((props) => {
|
||||
const { auth } = props;
|
||||
|
||||
if (!auth || (auth && !auth.requires_client_login)) return null;
|
||||
|
||||
return (
|
||||
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
||||
<Tooltip
|
||||
content="Log in or log out of cellxgene"
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
data-testid="auth-button"
|
||||
disabled={false}
|
||||
icon={!auth.is_authenticated ? "log-in" : "log-out"}
|
||||
href={!auth.is_authenticated ? auth.login : auth.logout}
|
||||
>
|
||||
{!auth.is_authenticated ? "Log In" : "Log Out"}
|
||||
</AnchorButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Auth;
|
||||
@@ -1,118 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Popover,
|
||||
Button,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tooltip,
|
||||
Position,
|
||||
} from "@blueprintjs/core";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
import actions from "../../actions";
|
||||
|
||||
@connect((state) => ({
|
||||
layoutChoice: state.layoutChoice,
|
||||
// disabled temporarily. TODO - issue #1606
|
||||
// reembedController: state.reembedController,
|
||||
// enableReembedding: state.config?.parameters?.["enable-reembedding"] ?? false,
|
||||
enableReembedding: false,
|
||||
}))
|
||||
class Embedding extends React.PureComponent {
|
||||
handleLayoutChoiceChange = (e) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(actions.layoutChoiceAction(e.currentTarget.value));
|
||||
};
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this -- temporary disable
|
||||
renderReembedding() {
|
||||
return null;
|
||||
/* disabled pending rewrite. TODO - issue #1606
|
||||
const {
|
||||
enableReembedding,
|
||||
world,
|
||||
universe,
|
||||
dispatch,
|
||||
reembedController,
|
||||
} = this.props;
|
||||
|
||||
if (!enableReembedding) return null;
|
||||
|
||||
const loading = !!reembedController?.pendingFetch;
|
||||
const disabled = World.worldEqUniverse(world, universe);
|
||||
const tipContent = disabled
|
||||
? "Subset cells first, then click to recompute UMAP embedding."
|
||||
: "Click to recompute UMAP embedding on the current cell subset.";
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tipContent}
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
icon="new-object"
|
||||
style={{ marginRight: 10 }}
|
||||
disabled={disabled}
|
||||
onClick={() => dispatch(actions.requestReembed())}
|
||||
loading={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
render() {
|
||||
const { layoutChoice } = this.props;
|
||||
|
||||
return (
|
||||
<ButtonGroup className={styles.menubarButton}>
|
||||
<Popover
|
||||
target={
|
||||
<Tooltip
|
||||
content="Select embedding for visualization"
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="layout-choice"
|
||||
icon="heatmap"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
content={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
<RadioGroup
|
||||
label="Embedding Choice"
|
||||
onChange={this.handleLayoutChoiceChange}
|
||||
selectedValue={layoutChoice.current}
|
||||
>
|
||||
{layoutChoice.available.map((name) => (
|
||||
<Radio label={name} value={name} key={name} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{this.renderReembedding()}
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Embedding;
|
||||
@@ -6,11 +6,13 @@ import * as globals from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
import actions from "../../actions";
|
||||
import Clip from "./clip";
|
||||
import Embedding from "./embedding";
|
||||
import AuthButtons from "./authButtons";
|
||||
import InformationMenu from "./infoMenu";
|
||||
import Subset from "./subset";
|
||||
import UndoRedoReset from "./undoRedo";
|
||||
import DiffexpButtons from "./diffexpButtons";
|
||||
import Reembedding from "./reembedding";
|
||||
import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
|
||||
|
||||
@connect((state) => {
|
||||
const { annoMatrix } = state;
|
||||
@@ -18,9 +20,11 @@ import DiffexpButtons from "./diffexpButtons";
|
||||
const selectedCount = crossfilter.countSelected();
|
||||
|
||||
const subsetPossible =
|
||||
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all are selected
|
||||
const subsetResetPossible =
|
||||
annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs;
|
||||
selectedCount !== 0 && selectedCount !== crossfilter.size(); // ie, not all and not none are selected
|
||||
const embSubsetView = getEmbSubsetView(annoMatrix);
|
||||
const subsetResetPossible = !embSubsetView
|
||||
? annoMatrix.nObs !== annoMatrix.schema.dataframe.nObs
|
||||
: annoMatrix.nObs !== embSubsetView.nObs;
|
||||
|
||||
return {
|
||||
subsetPossible,
|
||||
@@ -37,6 +41,7 @@ import DiffexpButtons from "./diffexpButtons";
|
||||
celllist1: state.differential.celllist1,
|
||||
celllist2: state.differential.celllist2,
|
||||
libraryVersions: state.config?.["library_versions"],
|
||||
auth: state.config?.authentication,
|
||||
undoDisabled: state["@@undoable/past"].length === 0,
|
||||
redoDisabled: state["@@undoable/future"].length === 0,
|
||||
aboutLink: state.config?.links?.["about-dataset"],
|
||||
@@ -47,6 +52,8 @@ import DiffexpButtons from "./diffexpButtons";
|
||||
tosURL: state.config?.parameters?.["about_legal_tos"],
|
||||
privacyURL: state.config?.parameters?.["about_legal_privacy"],
|
||||
categoricalSelection: state.categoricalSelection,
|
||||
enableReembedding:
|
||||
state.config?.parameters?.["enable-reembedding"] ?? false,
|
||||
};
|
||||
})
|
||||
class MenuBar extends React.PureComponent {
|
||||
@@ -212,6 +219,8 @@ class MenuBar extends React.PureComponent {
|
||||
colorAccessor,
|
||||
subsetPossible,
|
||||
subsetResetPossible,
|
||||
enableReembedding,
|
||||
auth,
|
||||
} = this.props;
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
|
||||
@@ -237,6 +246,7 @@ class MenuBar extends React.PureComponent {
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<AuthButtons auth={auth} />
|
||||
<InformationMenu
|
||||
libraryVersions={libraryVersions}
|
||||
aboutLink={aboutLink}
|
||||
@@ -264,7 +274,7 @@ class MenuBar extends React.PureComponent {
|
||||
this.handleClipPercentileMinValueChange
|
||||
}
|
||||
/>
|
||||
<Embedding />
|
||||
{enableReembedding ? <Reembedding /> : null}
|
||||
<Tooltip
|
||||
content="When a category is colored by, show labels on the graph"
|
||||
position="bottom"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core";
|
||||
import * as globals from "../../globals";
|
||||
import actions from "../../actions";
|
||||
import styles from "./menubar.css";
|
||||
|
||||
@connect((state) => ({
|
||||
reembedController: state.reembedController,
|
||||
annoMatrix: state.annoMatrix,
|
||||
}))
|
||||
class Reembedding extends React.PureComponent {
|
||||
render() {
|
||||
const { dispatch, annoMatrix, reembedController } = this.props;
|
||||
const loading = !!reembedController?.pendingFetch;
|
||||
const disabled = annoMatrix.nObs === annoMatrix.schema.dataframe.nObs;
|
||||
const tipContent = disabled
|
||||
? "Subset cells first, then click to recompute UMAP embedding."
|
||||
: "Click to recompute UMAP embedding on the current cell subset.";
|
||||
|
||||
return (
|
||||
<ButtonGroup className={styles.menubarButton}>
|
||||
<Tooltip
|
||||
content={tipContent}
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
icon="new-object"
|
||||
disabled={disabled}
|
||||
onClick={() => dispatch(actions.requestReembed())}
|
||||
loading={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Reembedding;
|
||||
@@ -40,10 +40,11 @@ export default class MiniHistogram extends React.PureComponent {
|
||||
};
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
const { obsOrVarContinuousFieldDisplayName } = this.props;
|
||||
const { obsOrVarContinuousFieldDisplayName, bins } = this.props;
|
||||
if (
|
||||
prevProps.obsOrVarContinuousFieldDisplayName !==
|
||||
obsOrVarContinuousFieldDisplayName
|
||||
obsOrVarContinuousFieldDisplayName ||
|
||||
prevProps.bins !== bins
|
||||
)
|
||||
this.drawHistogram();
|
||||
};
|
||||
|
||||
@@ -438,18 +438,19 @@ class Scatterplot extends React.PureComponent {
|
||||
pointDilation,
|
||||
} = this.props;
|
||||
const { minimized, regl, viewport } = this.state;
|
||||
const bottomToolbarGutter = 48; // gutter for bottom tool bar
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: minimized ? -height + -margin.top - 2 : 0,
|
||||
bottom: bottomToolbarGutter,
|
||||
borderRadius: "3px 3px 0px 0px",
|
||||
left: globals.leftSidebarWidth + globals.scatterplotMarginLeft,
|
||||
padding: "0px 20px 20px 0px",
|
||||
background: "white",
|
||||
/* x y blur spread color */
|
||||
boxShadow: "0px 0px 6px 2px rgba(153,153,153,0.4)",
|
||||
boxShadow: "0px 0px 3px 2px rgba(153,153,153,0.2)",
|
||||
zIndex: 2,
|
||||
}}
|
||||
id="scatterplot_wrapper"
|
||||
@@ -488,7 +489,9 @@ class Scatterplot extends React.PureComponent {
|
||||
id="scatterplot"
|
||||
style={{
|
||||
width: `${width + margin.left + margin.right}px`,
|
||||
height: `${height + margin.top + margin.bottom}px`,
|
||||
height: `${
|
||||
(minimized ? 0 : height + margin.top) + margin.bottom
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
@@ -498,6 +501,7 @@ class Scatterplot extends React.PureComponent {
|
||||
style={{
|
||||
marginLeft: margin.left,
|
||||
marginTop: margin.top,
|
||||
display: minimized ? "none" : null,
|
||||
}}
|
||||
ref={this.setReglCanvas}
|
||||
/>
|
||||
@@ -523,9 +527,7 @@ class Scatterplot extends React.PureComponent {
|
||||
}
|
||||
return (
|
||||
<ScatterplotAxis
|
||||
width={width}
|
||||
height={height}
|
||||
margin={margin}
|
||||
minimized={minimized}
|
||||
scatterplotYYaccessor={scatterplotXXaccessor}
|
||||
scatterplotXXaccessor={scatterplotYYaccessor}
|
||||
xScale={asyncProps.xScale}
|
||||
@@ -544,7 +546,13 @@ class Scatterplot extends React.PureComponent {
|
||||
export default Scatterplot;
|
||||
|
||||
const ScatterplotAxis = React.memo(
|
||||
({ scatterplotYYaccessor, scatterplotXXaccessor, xScale, yScale }) => {
|
||||
({
|
||||
minimized,
|
||||
scatterplotYYaccessor,
|
||||
scatterplotXXaccessor,
|
||||
xScale,
|
||||
yScale,
|
||||
}) => {
|
||||
/*
|
||||
Axis for the scatterplot, rendered with SVG/D3. Props:
|
||||
* scatterplotXXaccessor - name of X axis
|
||||
@@ -559,7 +567,7 @@ const ScatterplotAxis = React.memo(
|
||||
const svgRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef.current) return;
|
||||
if (!svgRef.current || minimized) return;
|
||||
const svg = d3.select(svgRef.current);
|
||||
|
||||
svg.selectAll("*").remove();
|
||||
@@ -608,6 +616,9 @@ const ScatterplotAxis = React.memo(
|
||||
width={width + margin.left + margin.right}
|
||||
height={height + margin.top + margin.bottom}
|
||||
data-testid="scatterplot-svg"
|
||||
style={{
|
||||
display: minimized ? "none" : null,
|
||||
}}
|
||||
>
|
||||
<g ref={svgRef} transform={`translate(${margin.left},${margin.top})`} />
|
||||
</svg>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const maxControlsWidth = 800;
|
||||
export const graphMargin = { top: 20, right: 10, bottom: 30, left: 40 };
|
||||
export const graphWidth = 700;
|
||||
export const graphHeight = 700;
|
||||
export const scatterplotMarginLeft = 25;
|
||||
export const scatterplotMarginLeft = 11;
|
||||
|
||||
export const rightSidebarWidth = 365;
|
||||
export const leftSidebarWidth = 365;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { makeContinuousDimensionName } from "../util/nameCreators";
|
||||
const ContinuousSelection = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case "reset subset":
|
||||
case "subset to selection":
|
||||
case "set clip quantiles": {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ const GraphSelection = (
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "set clip quantiles":
|
||||
case "subset to selection":
|
||||
case "reset subset":
|
||||
case "set layout choice": {
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,7 @@ import autosave from "./autosave";
|
||||
import ontology from "./ontology";
|
||||
import centroidLabels from "./centroidLabels";
|
||||
import pointDialation from "./pointDilation";
|
||||
import { reembedController, reembedding } from "./reembed";
|
||||
import { reembedController } from "./reembed";
|
||||
import { gcMiddleware as annoMatrixGC } from "../annoMatrix";
|
||||
|
||||
import undoableConfig from "./undoableConfig";
|
||||
@@ -30,7 +30,6 @@ const Reducer = undoable(
|
||||
["obsCrossfilter", obsCrossfilter],
|
||||
["ontology", ontology],
|
||||
["annotations", annotations],
|
||||
["reembedding", reembedding],
|
||||
["layoutChoice", layoutChoice],
|
||||
["categoricalSelection", categoricalSelection],
|
||||
["continuousSelection", continuousSelection],
|
||||
@@ -55,7 +54,6 @@ const Reducer = undoable(
|
||||
"layoutChoice",
|
||||
"centroidLabels",
|
||||
"annotations",
|
||||
"reembedding",
|
||||
],
|
||||
undoableConfig
|
||||
);
|
||||
|
||||
@@ -48,27 +48,15 @@ const LayoutChoice = (
|
||||
}
|
||||
|
||||
case "reembed: add reembedding": {
|
||||
const { schema } = nextSharedState.annoMatrix;
|
||||
const { name } = action.schema;
|
||||
const available = Array.from(new Set(state.available).add(name));
|
||||
const currentDimNames = schema.layout.obsByName[name].dims;
|
||||
return {
|
||||
...state,
|
||||
available,
|
||||
};
|
||||
}
|
||||
|
||||
case "reembed: clear all reembeddings": {
|
||||
const { annoMatrix } = nextSharedState;
|
||||
const { current } = state;
|
||||
const dflt = setToDefaultLayout(annoMatrix.schema);
|
||||
if (dflt.available.includes(current)) {
|
||||
return {
|
||||
...state,
|
||||
available: dflt.available,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...dflt,
|
||||
current: name,
|
||||
currentDimNames,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,38 +27,3 @@ export const reembedController = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
actual reembedding data is part of the undo/redo history
|
||||
*/
|
||||
export const reembedding = (
|
||||
state = {
|
||||
reembeddings: new Map(),
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "reembed: add reembedding": {
|
||||
const { schema, embedding } = action;
|
||||
const { name } = schema.name;
|
||||
const { reembeddings } = state;
|
||||
return {
|
||||
...state,
|
||||
reembeddings: new Map(reembeddings).set(name, {
|
||||
name,
|
||||
schema,
|
||||
embedding,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case "reembed: clear all reembeddings": {
|
||||
return {
|
||||
...state,
|
||||
reembeddings: new Map(),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,7 +64,12 @@ function topNCategories(colSchema, summary, N) {
|
||||
|
||||
export function isSelectableCategoryName(schema, name) {
|
||||
const { index } = schema.annotations.obs;
|
||||
return name && name !== index && isCategoricalAnnotation(schema, name);
|
||||
const colSchema = schema.annotations.obsByName[name];
|
||||
return (
|
||||
name &&
|
||||
name !== index &&
|
||||
(isCategoricalAnnotation(schema, name) || colSchema.writable)
|
||||
);
|
||||
}
|
||||
|
||||
export function selectableCategoryNames(schema, names) {
|
||||
|
||||
@@ -170,11 +170,11 @@ export function encodeMatrixFBS(df) {
|
||||
|
||||
function promoteTypedArray(o) {
|
||||
/*
|
||||
Decide what internal data type to use for the data returned from
|
||||
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.
|
||||
promotion to float64. We COULD simply look at the data to decide.
|
||||
*/
|
||||
if (isFpTypedArray(o) || Array.isArray(o)) return o;
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
The annoMatrix view stack has a set of conventions which are assumed elsewhere in the
|
||||
application. These helper functions make it simple for action creators to manage
|
||||
the stack.
|
||||
|
||||
The annoMatrix module does not care about this order, but we maintain it as
|
||||
a convention to make it simpler to manipulate the views.
|
||||
|
||||
Terminology:
|
||||
- clip view: AnnoMatrixClipView
|
||||
- subset view: AnnoMatrixRowSubsetView
|
||||
- user subset view: create by the user explicitly subsetting by selection
|
||||
- embedding subset view: implicitly created by switching the current embedding
|
||||
- loader, or base annoMatrix: the root, which loads data
|
||||
|
||||
Rules:
|
||||
1. there will be zero or one clip view
|
||||
2. there will be zero or more subset views
|
||||
3. there will be zero or one embedding view
|
||||
4. there will be one loader/base, which is always the bottom view
|
||||
5. the view ordering MUST be (top to bottom):
|
||||
|
||||
[clip] -> [user subset] -> [embedding subset] -> loader
|
||||
|
||||
There is code elsewhere in the app (eg, menubar/clip.js) which assumes this order.
|
||||
|
||||
Views can be interogated for their type with the following:
|
||||
|
||||
* is a view: annoMatrix.isView
|
||||
* is the loader: !anonMatrix.isView (or annoMatrix === annoMatrix.base())
|
||||
* is a clip view: annoMatrix.isClipped (or annoMatrix.clipRange)
|
||||
* is a subset view: (annoMatrix.isView && !annoMatrix.isClipped)
|
||||
* is a user subset view: annoMatrix.userFlags?.isUserSubsetView
|
||||
* is an embedding subset view: annomatrix.userFlags?.isEmbSubsetView
|
||||
|
||||
*/
|
||||
|
||||
import { clip, isubsetMask, isubset } from "../../annoMatrix";
|
||||
import { memoize } from "../dataframe/util";
|
||||
|
||||
export function _clipAnnoMatrix(annoMatrix, min, max) {
|
||||
/*
|
||||
clip the annoMatrix.
|
||||
*/
|
||||
return annoMatrix.isClipped
|
||||
? clip(annoMatrix.viewOf, min, max)
|
||||
: clip(annoMatrix, min, max);
|
||||
}
|
||||
|
||||
export function _userSubsetAnnoMatrix(annoMatrix, mask) {
|
||||
/*
|
||||
user-requested row subset of annoMatrix, to be added on top of any
|
||||
other previous row subsets.
|
||||
*/
|
||||
const { clipRange } = annoMatrix;
|
||||
if (clipRange) {
|
||||
annoMatrix = annoMatrix.viewOf;
|
||||
}
|
||||
|
||||
annoMatrix = isubsetMask(annoMatrix, mask);
|
||||
annoMatrix.userFlags.isUserSubsetView = true;
|
||||
|
||||
if (clipRange) {
|
||||
annoMatrix = clip(annoMatrix, ...clipRange);
|
||||
}
|
||||
|
||||
return annoMatrix;
|
||||
}
|
||||
|
||||
export function _userResetSubsetAnnoMatrix(annoMatrix) {
|
||||
/*
|
||||
Reset/remove all user-requested subsets. Do not remove clip or embedding subset.
|
||||
*/
|
||||
|
||||
/* stash clipping info, if any */
|
||||
const { clipRange } = annoMatrix;
|
||||
if (clipRange) {
|
||||
annoMatrix = annoMatrix.viewOf;
|
||||
}
|
||||
|
||||
/* pop all views except embedding subset and loader */
|
||||
while (annoMatrix.isView && annoMatrix.userFlags.isUserSubsetView) {
|
||||
annoMatrix = annoMatrix.viewOf;
|
||||
}
|
||||
|
||||
/* re-apply the clip, if any */
|
||||
if (clipRange) {
|
||||
annoMatrix = clip(annoMatrix, ...clipRange);
|
||||
}
|
||||
|
||||
return annoMatrix;
|
||||
}
|
||||
|
||||
export function _setEmbeddingSubset(annoMatrix, embeddingDf) {
|
||||
/*
|
||||
Set the embedding subset view. Only create a subset view for the embedding
|
||||
when it is needed, ie, there are NaN values in the embeddings.
|
||||
*/
|
||||
const embRowOffsets = _getEmbeddingRowOffsets(
|
||||
annoMatrix.rowIndex,
|
||||
embeddingDf
|
||||
);
|
||||
|
||||
const curEmbSubsetView = getEmbSubsetView(annoMatrix);
|
||||
|
||||
/* if no current embedding subset, and no new embedding subset, just noop */
|
||||
if (!embRowOffsets && !curEmbSubsetView) return annoMatrix;
|
||||
|
||||
// ... otherwise, do the work
|
||||
|
||||
/* stash clipping info, if any */
|
||||
const clipRange = annoMatrix.isClipped ? annoMatrix.clipRange : null;
|
||||
|
||||
/* pop all subsets, user or embedding */
|
||||
while (annoMatrix.isView) {
|
||||
annoMatrix = annoMatrix.viewOf;
|
||||
}
|
||||
|
||||
/* apply new embedding row index, if needed */
|
||||
if (embRowOffsets) {
|
||||
annoMatrix = isubset(annoMatrix, embRowOffsets);
|
||||
annoMatrix.userFlags.isEmbSubsetView = true;
|
||||
}
|
||||
|
||||
/* re-apply clip, if needed */
|
||||
if (clipRange) {
|
||||
annoMatrix = clip(annoMatrix, ...clipRange);
|
||||
}
|
||||
|
||||
return annoMatrix;
|
||||
}
|
||||
|
||||
function _getEmbeddingRowOffsets(baseRowIndex, embeddingDf) {
|
||||
/*
|
||||
given a dataframe containing an embedding:
|
||||
- if the embedding contains no NaN coordinates, return null
|
||||
- if the embedding contains NaN coordinates, return a rowIndex
|
||||
that contains only the rows with discrete valued coordinates.
|
||||
|
||||
Currently assumes that there will be onl two dimensions in the embedding.
|
||||
*/
|
||||
const X = embeddingDf.icol(0).asArray();
|
||||
const Y = embeddingDf.icol(1).asArray();
|
||||
const offsets = new Int32Array(X.length);
|
||||
let numOffsets = 0;
|
||||
|
||||
for (let i = 0, l = X.length; i < l; i += 1) {
|
||||
if (!Number.isNaN(X[i]) && !Number.isNaN(Y[i])) {
|
||||
offsets[numOffsets] = i;
|
||||
numOffsets += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (numOffsets === X.length) return null;
|
||||
return offsets.subarray(0, numOffsets);
|
||||
}
|
||||
|
||||
export function _getDiscreteCellEmbeddingRowIndex(embeddingDf) {
|
||||
const idx = _getEmbeddingRowOffsets(embeddingDf.rowIndex, embeddingDf);
|
||||
if (idx === null) return embeddingDf.rowIndex;
|
||||
return embeddingDf.rowIndex.isubset(idx);
|
||||
}
|
||||
export const getDiscreteCellEmbeddingRowIndex = memoize(
|
||||
_getDiscreteCellEmbeddingRowIndex,
|
||||
(df) => df.__id
|
||||
);
|
||||
|
||||
export function getEmbSubsetView(annoMatrix) {
|
||||
/* if there is an embedding subset in the view stack, return it. Falsish if not. */
|
||||
while (annoMatrix.isView) {
|
||||
if (annoMatrix.userFlags.isEmbSubsetView) return annoMatrix;
|
||||
annoMatrix = annoMatrix.viewOf;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user