mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-30 04:28:11 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95fd20b405 | ||
|
|
5c6cc597c8 | ||
|
|
3bfeadc2b9 | ||
|
|
296ed752fa | ||
|
|
1e66ec2b89 | ||
|
|
4a50f0ed46 | ||
|
|
af0d1f6fb2 | ||
|
|
a83ec60308 | ||
|
|
76c0cbf16b | ||
|
|
455d987398 | ||
|
|
00b9a8fa72 | ||
|
|
28f5263c88 | ||
|
|
933fac5fff | ||
|
|
0c26f227fe | ||
|
|
2b90c747f5 | ||
|
|
43a4e087ef | ||
|
|
f2612707bb | ||
|
|
138d30909a |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.2.0
|
current_version = 0.3.0
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
[bumpversion:file:setup.py]
|
||||||
search = version="{current_version}"
|
search = version="{current_version}"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
`cellxgene` is an interactive data explorer for single-cell transcriptomics datasets, such as those coming from the [Human Cell Atlas](https://humancellatlas.org). Leveraging modern web development techniques to enable fast visualizations of at least 1 million cells, we hope to enable biologists and computational researchers to explore their data, and to demonstrate general, scalable, and reusable patterns for scientific data visualization.
|
`cellxgene` is an interactive data explorer for single-cell transcriptomics datasets, such as those coming from the [Human Cell Atlas](https://humancellatlas.org). Leveraging modern web development techniques to enable fast visualizations of at least 1 million cells, we hope to enable biologists and computational researchers to explore their data, and to demonstrate general, scalable, and reusable patterns for scientific data visualization.
|
||||||
|
|
||||||
<img src="https://github.com/chanzuckerberg/cellxgene/blob/master/docs/cellxgene-demo-1.gif" width="200" height="200" hspace="30"><img src="https://github.com/chanzuckerberg/cellxgene/blob/master/docs/cellxgene-demo-2.gif" width="200" height="200" hspace="30"><img src="https://github.com/chanzuckerberg/cellxgene/blob/master/docs/cellxgene-demo-3.gif" width="200" height="200" hspace="30">
|
<img src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-demo-1.gif" width="200" height="200" hspace="30"><img src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-demo-2.gif" width="200" height="200" hspace="30"><img src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-demo-3.gif" width="200" height="200" hspace="30">
|
||||||
|
|
||||||
## getting started
|
## getting started
|
||||||
|
|
||||||
@@ -27,9 +27,10 @@ If you want an example dataset download [this file](https://github.com/chanzucke
|
|||||||
```
|
```
|
||||||
cellxgene launch pbmc3k.h5ad --open
|
cellxgene launch pbmc3k.h5ad --open
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see your web browser open with the following
|
You should see your web browser open with the following
|
||||||
|
|
||||||
<img width="450" src="https://github.com/chanzuckerberg/cellxgene/blob/master/docs/cellxgene-opening-screenshot.png" pad="50px">
|
<img width="450" src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-opening-screenshot.png" pad="50px">
|
||||||
|
|
||||||
**Note**: automatic opening of the browser with the `--open` flag only works on OS X, on other platforms you'll need to directly point to the provided link in your browser.
|
**Note**: automatic opening of the browser with the `--open` flag only works on OS X, on other platforms you'll need to directly point to the provided link in your browser.
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ To prepare from an existing `.h5ad` file use
|
|||||||
cellxgene prepare dataset.h5ad --output=dataset-processed.h5ad
|
cellxgene prepare dataset.h5ad --output=dataset-processed.h5ad
|
||||||
```
|
```
|
||||||
|
|
||||||
This will load the input data, perform PCA and nearest neighbor calculations, compute `umap` and `tsne` layouts and `louvain` cluster assignments, and save the results in a new file called `dataset-processed.h5ad` that can be loaded using `cellxgene launch`. Data can be loaded from several formats, including `.h5ad` `.loom` and a `10-Genomics-formatted` `mtx` directory. Several options are available, including running one of the preprocessing `recipes` included with `scanpy`, which include steps like cell filtering and gene selection.
|
This will load the input data, perform PCA and nearest neighbor calculations, compute `umap` and `tsne` layouts and `louvain` cluster assignments, and save the results in a new file called `dataset-processed.h5ad` that can be loaded using `cellxgene launch`. Data can be loaded from several formats, including `.h5ad` `.loom` and a `10-Genomics-formatted` `mtx` directory. Several options are available, including running one of the preprocessing `recipes` included with `scanpy`, which include steps like cell filtering and gene selection. To learn more about the `recipes` please see the `scanpy` [documentation](https://github.com/theislab/scanpy/blob/master/scanpy/preprocessing/recipes.py).
|
||||||
|
|
||||||
Depending on the options chosen, `prepare` can take a long time to run (a few minutes for datasets with 10-100k cells, up to an hour or more for datasets with >100k cells). If you want `prepare` to run faster we recommend using the `sparse` option and only computing the layout for `umap`, using a call like this
|
Depending on the options chosen, `prepare` can take a long time to run (a few minutes for datasets with 10-100k cells, up to an hour or more for datasets with >100k cells). If you want `prepare` to run faster we recommend using the `sparse` option and only computing the layout for `umap`, using a call like this
|
||||||
|
|
||||||
@@ -153,12 +154,24 @@ This may happen, especially as we work out bugs in our installation process! Ple
|
|||||||
|
|
||||||
> How are you computing and sorting differential expression results?
|
> How are you computing and sorting differential expression results?
|
||||||
|
|
||||||
Currently we use a [Welch's *t*-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) implementation including the same variance overestimation correction as used in `scanpy`. We sort the `tscore` to identify the top N genes, and then filter to remove any that fall below a cutoff log fold change value, which can help remove spurious test results. The default threshold is `0.01` and can be changed using the option `--diffexp-lfc-cutoff`. We can explore adding support for other test types in the future.
|
Currently we use a [Welch's _t_-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) implementation including the same variance overestimation correction as used in `scanpy`. We sort the `tscore` to identify the top N genes, and then filter to remove any that fall below a cutoff log fold change value, which can help remove spurious test results. The default threshold is `0.01` and can be changed using the option `--diffexp-lfc-cutoff`. We can explore adding support for other test types in the future.
|
||||||
|
|
||||||
> I'm following the developer instructions and get an error about "missing files and directories” when trying to build the client
|
> I'm following the developer instructions and get an error about "missing files and directories” when trying to build the client
|
||||||
|
|
||||||
This is likely because you do not have node and npm installed, we recommend using [nvm](https://github.com/creationix/nvm) if you're new to using these tools.
|
This is likely because you do not have node and npm installed, we recommend using [nvm](https://github.com/creationix/nvm) if you're new to using these tools.
|
||||||
|
|
||||||
|
> What part of the anndata objects does cellxgene pull in for visualization?
|
||||||
|
|
||||||
|
- `.obs` and `.var` annotations are use to extract metadata for filtering
|
||||||
|
- `.X` is used to display expression (histograms, scatterplot & colorscale) and to compute differential expression
|
||||||
|
- `.obsm` is used for layout
|
||||||
|
|
||||||
|
> When I start cellxgene, I get an error `Unexpected HTTP response 500, INTERNAL SERVER ERROR -- Out of range float values are not JSON compliant` in the web UI, or `Warning: JSON encoding failure - suggest trying --nan-to-num command line option` in the CLI. What can I do?
|
||||||
|
|
||||||
|
At the moment, cellxgene is unable to transmit floating point NaN or Inifinty values to the web UI (due to a limitation on data serialization method in use). We expect to resolve this in a future release, but in the meantime, you can work around this issue by starting cellxgene with the `--nan-to-num` command line option, ie, `cellxgene launch data.h5ad --nan-to-num`.
|
||||||
|
|
||||||
|
This option will convert all NaNs to zero, and all positive/negative infinities to the min/max of the data element within which the value was found (eg, +Infinity within an `obs` annotation will be converted to the maximum finite value in that annotation). This option will increase startup time, so we recommend only using it when the dataset contains NaN/Infinities.
|
||||||
|
|
||||||
## developer guide
|
## developer guide
|
||||||
|
|
||||||
This project has made a few key design choices
|
This project has made a few key design choices
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import summarizeAnnotations from "../../../src/util/stateManager/summarizeAnnotations";
|
||||||
|
|
||||||
|
describe("summarizeAnnotations", () => {
|
||||||
|
const schema = {
|
||||||
|
annotations: {
|
||||||
|
obs: [
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "nameString", type: "string" },
|
||||||
|
{ name: "nameBoolean", type: "boolean" },
|
||||||
|
{ name: "nameFloat32", type: "float32" },
|
||||||
|
{ name: "nameInt32", type: "int32" },
|
||||||
|
{
|
||||||
|
name: "nameCategorical",
|
||||||
|
type: "categorical",
|
||||||
|
categories: [true, false, 1, 0, 0.00001, 4383.4833, "test", "", "0"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
var: [{ name: "name", type: "string" }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test("empty test", () => {
|
||||||
|
const summary = summarizeAnnotations(schema, [], []);
|
||||||
|
expect(summary).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
obs: {
|
||||||
|
nameString: {
|
||||||
|
categorical: true,
|
||||||
|
categories: [],
|
||||||
|
categoryCounts: new Map(),
|
||||||
|
numCategories: 0
|
||||||
|
},
|
||||||
|
nameBoolean: {
|
||||||
|
categorical: true,
|
||||||
|
categories: [],
|
||||||
|
categoryCounts: new Map(),
|
||||||
|
numCategories: 0
|
||||||
|
},
|
||||||
|
nameFloat32: {
|
||||||
|
categorical: false,
|
||||||
|
range: {
|
||||||
|
max: Number.NEGATIVE_INFINITY,
|
||||||
|
min: Number.POSITIVE_INFINITY
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nameInt32: {
|
||||||
|
categorical: false,
|
||||||
|
range: {
|
||||||
|
max: Number.NEGATIVE_INFINITY,
|
||||||
|
min: Number.POSITIVE_INFINITY
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nameCategorical: {
|
||||||
|
categorical: true,
|
||||||
|
categories: [],
|
||||||
|
categoryCounts: new Map(),
|
||||||
|
numCategories: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
var: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple test", () => {
|
||||||
|
const obsAnnotations = [
|
||||||
|
{
|
||||||
|
__index__: 0,
|
||||||
|
name: "n1",
|
||||||
|
nameString: "hi",
|
||||||
|
nameBoolean: true,
|
||||||
|
nameFloat32: 39.3,
|
||||||
|
nameInt32: 99,
|
||||||
|
nameCategorical: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const varAnnotations = [];
|
||||||
|
|
||||||
|
const summary = summarizeAnnotations(
|
||||||
|
schema,
|
||||||
|
obsAnnotations,
|
||||||
|
varAnnotations
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(summary).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
obs: {
|
||||||
|
nameString: {
|
||||||
|
categorical: true,
|
||||||
|
categories: ["hi"],
|
||||||
|
categoryCounts: new Map([["hi", 1]]),
|
||||||
|
numCategories: 1
|
||||||
|
},
|
||||||
|
nameBoolean: {
|
||||||
|
categorical: true,
|
||||||
|
categories: [true],
|
||||||
|
categoryCounts: new Map([[true, 1]]),
|
||||||
|
numCategories: 1
|
||||||
|
},
|
||||||
|
nameFloat32: {
|
||||||
|
categorical: false,
|
||||||
|
range: { min: 39.3, max: 39.3 }
|
||||||
|
},
|
||||||
|
nameInt32: {
|
||||||
|
categorical: false,
|
||||||
|
range: { min: 99, max: 99 }
|
||||||
|
},
|
||||||
|
nameCategorical: {
|
||||||
|
categorical: true,
|
||||||
|
categories: [1],
|
||||||
|
categoryCounts: new Map([[1, 1]]),
|
||||||
|
numCategories: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
var: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("multi test", () => {
|
||||||
|
const obsAnnotations = [
|
||||||
|
{
|
||||||
|
__index__: 0,
|
||||||
|
name: "n0",
|
||||||
|
nameString: "hi",
|
||||||
|
nameBoolean: false,
|
||||||
|
nameFloat32: 39.3,
|
||||||
|
nameInt32: 99,
|
||||||
|
nameCategorical: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__index__: 1,
|
||||||
|
name: "n1",
|
||||||
|
nameString: "hi",
|
||||||
|
nameBoolean: true,
|
||||||
|
nameFloat32: 39.3,
|
||||||
|
nameInt32: 99,
|
||||||
|
nameCategorical: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__index__: 2,
|
||||||
|
name: "n2",
|
||||||
|
nameString: "bye",
|
||||||
|
nameBoolean: true,
|
||||||
|
nameFloat32: 0,
|
||||||
|
nameInt32: 99,
|
||||||
|
nameCategorical: "0"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const varAnnotations = [];
|
||||||
|
|
||||||
|
const summary = summarizeAnnotations(
|
||||||
|
schema,
|
||||||
|
obsAnnotations,
|
||||||
|
varAnnotations
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(summary).toMatchObject(
|
||||||
|
expect.objectContaining({
|
||||||
|
obs: {
|
||||||
|
nameString: {
|
||||||
|
categorical: true,
|
||||||
|
categories: expect.arrayContaining(["hi", "bye"]),
|
||||||
|
categoryCounts: new Map([["hi", 2], ["bye", 1]]),
|
||||||
|
numCategories: 2
|
||||||
|
},
|
||||||
|
nameBoolean: {
|
||||||
|
categorical: true,
|
||||||
|
categories: expect.arrayContaining([true, false]),
|
||||||
|
categoryCounts: new Map([[true, 2], [false, 1]]),
|
||||||
|
numCategories: 2
|
||||||
|
},
|
||||||
|
nameFloat32: {
|
||||||
|
categorical: false,
|
||||||
|
range: { min: 0, max: 39.3 }
|
||||||
|
},
|
||||||
|
nameInt32: {
|
||||||
|
categorical: false,
|
||||||
|
range: { min: 99, max: 99 }
|
||||||
|
},
|
||||||
|
nameCategorical: {
|
||||||
|
categorical: true,
|
||||||
|
categories: expect.arrayContaining([1, false, "0"]),
|
||||||
|
categoryCounts: new Map([[1, 1], [false, 1], ["0", 1]]),
|
||||||
|
numCategories: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
var: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
countCategoryValues2D,
|
||||||
|
clearCaches
|
||||||
|
} from "../../../src/util/stateManager/worldUtil";
|
||||||
|
|
||||||
|
describe("WorldUtil cache management", () => {
|
||||||
|
test("empty", () => {
|
||||||
|
const count = countCategoryValues2D("a", "b", []);
|
||||||
|
expect(count).toMatchObject(new Map());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple couts", () => {
|
||||||
|
const rows = [{ a: 0, b: false }, { a: 0, b: true }, { a: 1, b: false }];
|
||||||
|
const count = countCategoryValues2D("a", "b", rows);
|
||||||
|
expect(count).toMatchObject(
|
||||||
|
new Map([
|
||||||
|
[0, new Map([[true, 1], [false, 1]])],
|
||||||
|
[1, new Map([[false, 1]])]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("memo cache clear", () => {
|
||||||
|
clearCaches();
|
||||||
|
const row1 = [];
|
||||||
|
const row2 = [{ a: 0, b: false }, { a: 0, b: true }, { a: 1, b: false }];
|
||||||
|
const count1 = countCategoryValues2D("a", "b", row1);
|
||||||
|
const count2 = countCategoryValues2D("a", "b", row1);
|
||||||
|
const count3 = countCategoryValues2D("a", "b", []);
|
||||||
|
const count4 = countCategoryValues2D("a", "b", row2);
|
||||||
|
|
||||||
|
clearCaches();
|
||||||
|
const count10 = countCategoryValues2D("a", "b", row1);
|
||||||
|
const count11 = countCategoryValues2D("a", "b", row2);
|
||||||
|
|
||||||
|
expect(count1).toEqual(count2);
|
||||||
|
expect(count1).toEqual(count3);
|
||||||
|
expect(count1).toEqual(count10);
|
||||||
|
expect(count1).not.toBe(count3);
|
||||||
|
expect(count1).not.toBe(count10);
|
||||||
|
|
||||||
|
expect(count4).toEqual(count11);
|
||||||
|
expect(count4).not.toBe(count11);
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+64
-40
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cellxgene",
|
"name": "cellxgene",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2194,7 +2194,7 @@
|
|||||||
},
|
},
|
||||||
"babel-plugin-syntax-object-rest-spread": {
|
"babel-plugin-syntax-object-rest-spread": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
|
||||||
"integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=",
|
"integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -2627,7 +2627,7 @@
|
|||||||
},
|
},
|
||||||
"browserify-aes": {
|
"browserify-aes": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||||
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
|
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -2664,7 +2664,7 @@
|
|||||||
},
|
},
|
||||||
"browserify-rsa": {
|
"browserify-rsa": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||||
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
|
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -2718,7 +2718,7 @@
|
|||||||
},
|
},
|
||||||
"buffer": {
|
"buffer": {
|
||||||
"version": "4.9.1",
|
"version": "4.9.1",
|
||||||
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
|
||||||
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
|
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -3394,7 +3394,7 @@
|
|||||||
},
|
},
|
||||||
"create-hash": {
|
"create-hash": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||||
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -3407,7 +3407,7 @@
|
|||||||
},
|
},
|
||||||
"create-hmac": {
|
"create-hmac": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
||||||
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
|
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -4090,7 +4090,7 @@
|
|||||||
},
|
},
|
||||||
"diffie-hellman": {
|
"diffie-hellman": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||||
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
|
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -4705,7 +4705,7 @@
|
|||||||
},
|
},
|
||||||
"load-json-file": {
|
"load-json-file": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||||
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
|
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -4891,7 +4891,7 @@
|
|||||||
},
|
},
|
||||||
"events": {
|
"events": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||||
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
|
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
|
||||||
},
|
},
|
||||||
"evp_bytestokey": {
|
"evp_bytestokey": {
|
||||||
@@ -5206,7 +5206,7 @@
|
|||||||
},
|
},
|
||||||
"finalhandler": {
|
"finalhandler": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
|
||||||
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
|
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -5401,7 +5401,8 @@
|
|||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@@ -5422,12 +5423,14 @@
|
|||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -5442,17 +5445,20 @@
|
|||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -5569,7 +5575,8 @@
|
|||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@@ -5581,6 +5588,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -5595,6 +5603,7 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@@ -5602,12 +5611,14 @@
|
|||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@@ -5626,6 +5637,7 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@@ -5706,7 +5718,8 @@
|
|||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -5718,6 +5731,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@@ -5803,7 +5817,8 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@@ -5839,6 +5854,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@@ -5858,6 +5874,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
@@ -5901,12 +5918,14 @@
|
|||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5922,6 +5941,11 @@
|
|||||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fuzzysort": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ=="
|
||||||
|
},
|
||||||
"get-caller-file": {
|
"get-caller-file": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
|
||||||
@@ -5936,7 +5960,7 @@
|
|||||||
},
|
},
|
||||||
"get-stream": {
|
"get-stream": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
|
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -6323,7 +6347,7 @@
|
|||||||
},
|
},
|
||||||
"html-webpack-plugin": {
|
"html-webpack-plugin": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
|
||||||
"integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
|
"integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -6399,7 +6423,7 @@
|
|||||||
},
|
},
|
||||||
"http-errors": {
|
"http-errors": {
|
||||||
"version": "1.6.3",
|
"version": "1.6.3",
|
||||||
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
|
||||||
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
|
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -6622,7 +6646,7 @@
|
|||||||
},
|
},
|
||||||
"is-builtin-module": {
|
"is-builtin-module": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
|
||||||
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
|
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -7629,7 +7653,7 @@
|
|||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
|
||||||
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
|
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -7719,7 +7743,7 @@
|
|||||||
},
|
},
|
||||||
"load-json-file": {
|
"load-json-file": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||||
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
|
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -8043,7 +8067,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
@@ -8168,7 +8192,7 @@
|
|||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -8213,7 +8237,7 @@
|
|||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -9939,7 +9963,7 @@
|
|||||||
},
|
},
|
||||||
"parse-asn1": {
|
"parse-asn1": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
|
||||||
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
|
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -10647,7 +10671,7 @@
|
|||||||
},
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -11669,7 +11693,7 @@
|
|||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -11856,7 +11880,7 @@
|
|||||||
},
|
},
|
||||||
"sha.js": {
|
"sha.js": {
|
||||||
"version": "2.4.11",
|
"version": "2.4.11",
|
||||||
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -12310,7 +12334,7 @@
|
|||||||
},
|
},
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -12546,7 +12570,7 @@
|
|||||||
},
|
},
|
||||||
"through": {
|
"through": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
|
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -13202,7 +13226,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
@@ -13865,7 +13889,7 @@
|
|||||||
},
|
},
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||||
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
|
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -13973,7 +13997,7 @@
|
|||||||
},
|
},
|
||||||
"yargs": {
|
"yargs": {
|
||||||
"version": "11.1.0",
|
"version": "11.1.0",
|
||||||
"resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
|
||||||
"integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==",
|
"integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cellxgene",
|
"name": "cellxgene",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
|
||||||
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
"repository": "https://github.com/chanzuckerberg/cellxgene",
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"d3": "^4.10.0",
|
"d3": "^4.10.0",
|
||||||
"d3-scale-chromatic": "^1.3.0",
|
"d3-scale-chromatic": "^1.3.0",
|
||||||
"font-color-contrast": "^1.0.3",
|
"font-color-contrast": "^1.0.3",
|
||||||
|
"fuzzysort": "^1.1.4",
|
||||||
"gl-mat4": "^1.1.4",
|
"gl-mat4": "^1.1.4",
|
||||||
"gl-matrix": "^2.7.1",
|
"gl-matrix": "^2.7.1",
|
||||||
"key-pressed": "0.0.1",
|
"key-pressed": "0.0.1",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const doInitialDataLoad = () =>
|
|||||||
"config",
|
"config",
|
||||||
"schema",
|
"schema",
|
||||||
"annotations/obs",
|
"annotations/obs",
|
||||||
"annotations/var",
|
"annotations/var?annotation-name=name",
|
||||||
"layout/obs"
|
"layout/obs"
|
||||||
])
|
])
|
||||||
.map(r => `${globals.API.prefix}${globals.API.version}${r}`)
|
.map(r => `${globals.API.prefix}${globals.API.version}${r}`)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class HistogramBrush extends React.Component {
|
|||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.range([this.height - this.marginBottom, 0]);
|
.range([this.height - this.marginBottom, 0]);
|
||||||
|
|
||||||
if (obsAnnotations[0][field]) {
|
if (obsAnnotations[0][field] !== undefined) {
|
||||||
// recalculate expensive stuff
|
// recalculate expensive stuff
|
||||||
const allValuesForContinuousFieldAsArray = _.map(obsAnnotations, field);
|
const allValuesForContinuousFieldAsArray = _.map(obsAnnotations, field);
|
||||||
|
|
||||||
|
|||||||
@@ -5,34 +5,13 @@ import { connect } from "react-redux";
|
|||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Category from "./category";
|
import Category from "./category";
|
||||||
|
|
||||||
/* Cap the max number of displayed categories */
|
|
||||||
const truncateCategories = options => {
|
|
||||||
const numOptions = _.size(options);
|
|
||||||
if (numOptions <= globals.maxCategoricalOptionsToDisplay) {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
return _(options)
|
|
||||||
.map((v, k) => ({ name: k, val: v }))
|
|
||||||
.sortBy("val")
|
|
||||||
.slice(numOptions - globals.maxCategoricalOptionsToDisplay)
|
|
||||||
.transform((r, v) => {
|
|
||||||
r[v.name] = v.val;
|
|
||||||
}, {})
|
|
||||||
.value();
|
|
||||||
};
|
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
ranges: _.get(state.controls.world, "summary.obs", null),
|
categoricalSelectionState: state.controls.categoricalSelectionState
|
||||||
categorySelectionLimit: _.get(
|
|
||||||
state.config,
|
|
||||||
"parameters.max-category-items",
|
|
||||||
globals.configDefaults.parameters["max-category-items"]
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
class Categories extends React.Component {
|
class Categories extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { ranges, categorySelectionLimit } = this.props;
|
const { categoricalSelectionState } = this.props;
|
||||||
if (!ranges) return null;
|
if (!categoricalSelectionState) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -47,27 +26,9 @@ class Categories extends React.Component {
|
|||||||
>
|
>
|
||||||
Categorical Metadata
|
Categorical Metadata
|
||||||
</p>
|
</p>
|
||||||
{_.map(ranges, (value, key) => {
|
{_.map(categoricalSelectionState, (catState, catName) => (
|
||||||
const isColorField = key.includes("color") || key.includes("Color");
|
<Category key={catName} metadataField={catName} />
|
||||||
const isSelectableCategory =
|
))}
|
||||||
value.options &&
|
|
||||||
!isColorField &&
|
|
||||||
key !== "name" &&
|
|
||||||
value.numOptions < categorySelectionLimit;
|
|
||||||
|
|
||||||
if (isSelectableCategory) {
|
|
||||||
const categoryOptions = truncateCategories(value.options);
|
|
||||||
return (
|
|
||||||
<Category
|
|
||||||
key={key}
|
|
||||||
metadataField={key}
|
|
||||||
values={categoryOptions}
|
|
||||||
isTruncated={categoryOptions !== value.options}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,15 @@ import React from "react";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
||||||
import memoize from "memoize-one";
|
import { Button, Tooltip } from "@blueprintjs/core";
|
||||||
import { Button, Tooltip, Position } from "@blueprintjs/core";
|
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Value from "./value";
|
import Value from "./value";
|
||||||
import alphabeticallySortedValues from "./util";
|
import alphabeticallySortedValues from "./util";
|
||||||
|
|
||||||
const countCategories = (values, optsAsBools) =>
|
|
||||||
_.reduce(
|
|
||||||
values,
|
|
||||||
(r, v, k) => {
|
|
||||||
r.total += 1;
|
|
||||||
if (optsAsBools[k]) {
|
|
||||||
r.on += 1;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
},
|
|
||||||
{ total: 0, on: 0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
colorAccessor: state.controls.colorAccessor,
|
colorAccessor: state.controls.colorAccessor,
|
||||||
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap
|
categoricalSelectionState: state.controls.categoricalSelectionState
|
||||||
}))
|
}))
|
||||||
class Category extends React.Component {
|
class Category extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -33,24 +19,28 @@ class Category extends React.Component {
|
|||||||
isChecked: true,
|
isChecked: true,
|
||||||
isExpanded: false
|
isExpanded: false
|
||||||
};
|
};
|
||||||
this.countCategories = memoize((values, optsAsBools) =>
|
|
||||||
countCategories(values, optsAsBools)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
const { categoricalAsBooleansMap, metadataField, values } = this.props;
|
const { categoricalSelectionState, metadataField } = this.props;
|
||||||
const categoryCount = this.countCategories(
|
const cat = categoricalSelectionState[metadataField];
|
||||||
values,
|
const categoryCount = {
|
||||||
categoricalAsBooleansMap[metadataField]
|
// total number of categories in this dimension
|
||||||
);
|
totalCatCount: cat.numCategories,
|
||||||
if (categoryCount.on === categoryCount.total) {
|
// number of selected options in this category
|
||||||
|
selectedCatCount: _.reduce(
|
||||||
|
cat.categorySelected,
|
||||||
|
(res, cond) => (cond ? res + 1 : res),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if (categoryCount.selectedCatCount === categoryCount.totalCatCount) {
|
||||||
/* everything is on, so not indeterminate */
|
/* everything is on, so not indeterminate */
|
||||||
this.checkbox.indeterminate = false;
|
this.checkbox.indeterminate = false;
|
||||||
} else if (categoryCount.on === 0) {
|
} else if (categoryCount.selectedCatCount === 0) {
|
||||||
/* nothing is on, so no */
|
/* nothing is on, so no */
|
||||||
this.checkbox.indeterminate = false;
|
this.checkbox.indeterminate = false;
|
||||||
} else if (categoryCount.on < categoryCount.total) {
|
} else if (categoryCount.selectedCatCount < categoryCount.totalCatCount) {
|
||||||
/* to be explicit... */
|
/* to be explicit... */
|
||||||
this.checkbox.indeterminate = true;
|
this.checkbox.indeterminate = true;
|
||||||
}
|
}
|
||||||
@@ -74,11 +64,10 @@ class Category extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleNone() {
|
toggleNone() {
|
||||||
const { dispatch, metadataField, value } = this.props;
|
const { dispatch, metadataField } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "categorical metadata filter none of these",
|
type: "categorical metadata filter none of these",
|
||||||
metadataField,
|
metadataField
|
||||||
value
|
|
||||||
});
|
});
|
||||||
this.setState({ isChecked: false });
|
this.setState({ isChecked: false });
|
||||||
}
|
}
|
||||||
@@ -94,13 +83,15 @@ class Category extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCategoryItems() {
|
renderCategoryItems() {
|
||||||
const { values, metadataField } = this.props;
|
const { categoricalSelectionState, metadataField } = this.props;
|
||||||
return _.map(alphabeticallySortedValues(values), (v, i) => (
|
|
||||||
|
const cat = categoricalSelectionState[metadataField];
|
||||||
|
const optTuples = alphabeticallySortedValues([...cat.categoryIndices]);
|
||||||
|
return _.map(optTuples, (tuple, i) => (
|
||||||
<Value
|
<Value
|
||||||
key={v}
|
key={tuple[1]}
|
||||||
metadataField={metadataField}
|
metadataField={metadataField}
|
||||||
count={values[v]}
|
categoryIndex={tuple[1]}
|
||||||
value={v}
|
|
||||||
i={i}
|
i={i}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@@ -108,12 +99,15 @@ class Category extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isExpanded, isChecked } = this.state;
|
const { isExpanded, isChecked } = this.state;
|
||||||
const { metadataField, colorAccessor, isTruncated } = this.props;
|
const {
|
||||||
|
metadataField,
|
||||||
|
colorAccessor,
|
||||||
|
categoricalSelectionState
|
||||||
|
} = this.props;
|
||||||
|
const { isTruncated } = categoricalSelectionState[metadataField];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
// display: "flex",
|
|
||||||
// alignItems: "baseline",
|
|
||||||
maxWidth: globals.maxControlsWidth
|
maxWidth: globals.maxControlsWidth
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
|
|
||||||
|
// values is [ [optVal, optIdx], ...]
|
||||||
|
// index is range array
|
||||||
|
// return sorted index
|
||||||
export default values =>
|
export default values =>
|
||||||
Object.keys(values).sort((a, b) => {
|
values.sort((a, b) => {
|
||||||
const textA = a.toUpperCase();
|
const textA = String(a[0]).toUpperCase();
|
||||||
const textB = b.toUpperCase();
|
const textB = String(b[0]).toUpperCase();
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,45 +4,51 @@ import React from "react";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap,
|
categoricalSelectionState: state.controls.categoricalSelectionState,
|
||||||
colorScale: state.controls.colorScale,
|
colorScale: state.controls.colorScale,
|
||||||
colorAccessor: state.controls.colorAccessor,
|
colorAccessor: state.controls.colorAccessor,
|
||||||
schema: _.get(state.controls.world, "schema", null)
|
schema: _.get(state.controls.world, "schema", null)
|
||||||
}))
|
}))
|
||||||
class CategoryValue extends React.Component {
|
class CategoryValue extends React.Component {
|
||||||
toggleOff() {
|
toggleOff() {
|
||||||
const { dispatch, metadataField, value } = this.props;
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "categorical metadata filter deselect",
|
type: "categorical metadata filter deselect",
|
||||||
metadataField,
|
metadataField,
|
||||||
value
|
categoryIndex
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOn() {
|
toggleOn() {
|
||||||
const { dispatch, metadataField, value } = this.props;
|
const { dispatch, metadataField, categoryIndex } = this.props;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "categorical metadata filter select",
|
type: "categorical metadata filter select",
|
||||||
metadataField,
|
metadataField,
|
||||||
value
|
categoryIndex
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
categoricalAsBooleansMap,
|
categoricalSelectionState,
|
||||||
metadataField,
|
metadataField,
|
||||||
count,
|
categoryIndex,
|
||||||
value,
|
|
||||||
colorAccessor,
|
colorAccessor,
|
||||||
colorScale,
|
colorScale,
|
||||||
i,
|
i,
|
||||||
schema
|
schema
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!categoricalAsBooleansMap) return null;
|
if (!categoricalSelectionState) return null;
|
||||||
|
|
||||||
|
const category = categoricalSelectionState[metadataField];
|
||||||
|
const selected = category.categorySelected[categoryIndex];
|
||||||
|
const count = category.categoryCounts[categoryIndex];
|
||||||
|
const value = category.categoryValues[categoryIndex];
|
||||||
|
const displayString = String(
|
||||||
|
category.categoryValues[categoryIndex]
|
||||||
|
).valueOf();
|
||||||
|
|
||||||
const selected = categoricalAsBooleansMap[metadataField][value];
|
|
||||||
/* this is the color scale, so add swatches below */
|
/* this is the color scale, so add swatches below */
|
||||||
const c = metadataField === colorAccessor;
|
const c = metadataField === colorAccessor;
|
||||||
let categories = null;
|
let categories = null;
|
||||||
@@ -78,7 +84,7 @@ class CategoryValue extends React.Component {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
<span className="bp3-control-indicator" />
|
<span className="bp3-control-indicator" />
|
||||||
{value}
|
{displayString}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -4,14 +4,51 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
import fuzzysort from "fuzzysort";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Button, Tooltip } from "@blueprintjs/core";
|
import { MenuItem } from "@blueprintjs/core";
|
||||||
|
import { Suggest } from "@blueprintjs/select";
|
||||||
import HistogramBrush from "../brushableHistogram";
|
import HistogramBrush from "../brushableHistogram";
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
import { postUserErrorToast } from "../framework/toasters";
|
import { postUserErrorToast } from "../framework/toasters";
|
||||||
import ExpressionButtons from "./expressionButtons";
|
import ExpressionButtons from "./expressionButtons";
|
||||||
|
|
||||||
|
const renderGene = (fuzzySortResult, { handleClick, modifiers, query }) => {
|
||||||
|
if (!modifiers.matchesPredicate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/* the fuzzysort wraps the object with other properties, like a score */
|
||||||
|
const gene = fuzzySortResult.obj;
|
||||||
|
const text = gene.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
active={modifiers.active}
|
||||||
|
disabled={modifiers.disabled}
|
||||||
|
// Use of annotations in this way is incorrect and dataset specific.
|
||||||
|
// See https://github.com/chanzuckerberg/cellxgene/issues/483
|
||||||
|
// label={gene.n_counts}
|
||||||
|
key={gene.name}
|
||||||
|
onClick={g => {
|
||||||
|
/* this fires when user clicks a menu item */
|
||||||
|
handleClick(g);
|
||||||
|
}}
|
||||||
|
text={text}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterGenes = (query, genes) => {
|
||||||
|
/* fires on load, once, and then for each character typed into the input */
|
||||||
|
return fuzzysort.go(query, genes, {
|
||||||
|
key: "name",
|
||||||
|
limit: 5,
|
||||||
|
threshold: -10000 // don't return bad results
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@connect(state => {
|
@connect(state => {
|
||||||
const metadata = _.get(state.controls.world, "obsAnnotations", null);
|
const metadata = _.get(state.controls.world, "obsAnnotations", null);
|
||||||
const ranges = _.get(state.controls.world, "summary.obs", null);
|
const ranges = _.get(state.controls.world, "summary.obs", null);
|
||||||
@@ -29,23 +66,9 @@ import ExpressionButtons from "./expressionButtons";
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
class GeneExpression extends React.Component {
|
class GeneExpression extends React.Component {
|
||||||
constructor(props) {
|
handleClick(g) {
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
gene: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPress(e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
this.handleClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick() {
|
|
||||||
const { world, dispatch, userDefinedGenes } = this.props;
|
const { world, dispatch, userDefinedGenes } = this.props;
|
||||||
const { gene } = this.state;
|
const gene = g.target;
|
||||||
|
|
||||||
if (userDefinedGenes.indexOf(gene) !== -1) {
|
if (userDefinedGenes.indexOf(gene) !== -1) {
|
||||||
postUserErrorToast("That gene already exists");
|
postUserErrorToast("That gene already exists");
|
||||||
} else if (userDefinedGenes.length > 15) {
|
} else if (userDefinedGenes.length > 15) {
|
||||||
@@ -60,13 +83,11 @@ class GeneExpression extends React.Component {
|
|||||||
type: "user defined gene",
|
type: "user defined gene",
|
||||||
data: gene
|
data: gene
|
||||||
});
|
});
|
||||||
this.setState({ gene: "" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { world, userDefinedGenes, differential } = this.props;
|
const { world, userDefinedGenes, differential } = this.props;
|
||||||
const { gene } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -87,28 +108,28 @@ class GeneExpression extends React.Component {
|
|||||||
style={{ padding: globals.leftSidebarSectionPadding }}
|
style={{ padding: globals.leftSidebarSectionPadding }}
|
||||||
className="bp3-control-group"
|
className="bp3-control-group"
|
||||||
>
|
>
|
||||||
<div className="bp3-input-group bp3-fill">
|
<Suggest
|
||||||
<input
|
closeOnSelect
|
||||||
onKeyDown={this.keyPress.bind(this)}
|
openOnKeyDown
|
||||||
onChange={e => {
|
resetOnSelect
|
||||||
this.setState({ gene: e.target.value });
|
noResults={<MenuItem disabled text="No matching genes." />}
|
||||||
|
onItemSelect={g => {
|
||||||
|
/* this happens on 'enter' */
|
||||||
|
this.handleClick(g);
|
||||||
}}
|
}}
|
||||||
value={gene}
|
inputValueRenderer={g => {
|
||||||
type="text"
|
return "";
|
||||||
className="bp3-input"
|
}}
|
||||||
placeholder="Enter a gene name"
|
itemListPredicate={filterGenes}
|
||||||
style={{ paddingRight: 94 }}
|
itemRenderer={renderGene.bind(this)}
|
||||||
|
items={
|
||||||
|
world && world.varAnnotations
|
||||||
|
? world.varAnnotations
|
||||||
|
: [{ name: "No genes", n_counts: "" }]
|
||||||
|
}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
|
||||||
content="Add a gene to see its expression levels"
|
|
||||||
position="bottom"
|
|
||||||
>
|
|
||||||
<Button intent="primary" onClick={this.handleClick.bind(this)}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
{world && userDefinedGenes.length > 0
|
{world && userDefinedGenes.length > 0
|
||||||
? _.map(userDefinedGenes, (geneName, index) => {
|
? _.map(userDefinedGenes, (geneName, index) => {
|
||||||
const values = world.varDataCache[geneName];
|
const values = world.varDataCache[geneName];
|
||||||
|
|||||||
Vendored
+156
-56
@@ -1,7 +1,7 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { World, kvCache } from "../util/stateManager";
|
import { World, kvCache, WorldUtil } from "../util/stateManager";
|
||||||
import parseRGB from "../util/parseRGB";
|
import parseRGB from "../util/parseRGB";
|
||||||
import Crossfilter from "../util/typedCrossfilter";
|
import Crossfilter from "../util/typedCrossfilter";
|
||||||
import * as globals from "../globals";
|
import * as globals from "../globals";
|
||||||
@@ -12,34 +12,109 @@ import {
|
|||||||
diffexpDimensionName,
|
diffexpDimensionName,
|
||||||
makeContinuousDimensionName
|
makeContinuousDimensionName
|
||||||
} from "../util/nameCreators";
|
} from "../util/nameCreators";
|
||||||
|
import { fillRange } from "../util/typedCrossfilter/util";
|
||||||
|
|
||||||
function createCategoricalAsBooleansMap(world) {
|
/*
|
||||||
|
Selection state for categoricals are tracked in an Object that
|
||||||
|
has two main components for each category:
|
||||||
|
1. mapping of option value to an index
|
||||||
|
2. array of bool selection state by index
|
||||||
|
Remember that option values can be ANY js type, except undefined/null.
|
||||||
|
|
||||||
|
{
|
||||||
|
_category_name_1: {
|
||||||
|
// map of option value to index
|
||||||
|
categoryIndices: Map([
|
||||||
|
catval1: index,
|
||||||
|
...
|
||||||
|
])
|
||||||
|
|
||||||
|
// index->selection true/false state
|
||||||
|
categorySelected: [ true/false, true/false, ... ]
|
||||||
|
|
||||||
|
// number of options
|
||||||
|
numCategories: number,
|
||||||
|
|
||||||
|
// isTruncated - true if the options for selection has
|
||||||
|
// been truncated (ie, was too large to implement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
function topNCategories(summary) {
|
||||||
|
const counts = _.map(summary.categories, cat =>
|
||||||
|
summary.categoryCounts.get(cat)
|
||||||
|
);
|
||||||
|
const sortIndex = fillRange(new Array(summary.numCategories)).sort(
|
||||||
|
(a, b) => counts[b] - counts[a]
|
||||||
|
);
|
||||||
|
const sortedCategories = _.map(sortIndex, i => summary.categories[i]);
|
||||||
|
const sortedCounts = _.map(sortIndex, i => counts[i]);
|
||||||
|
const N = globals.maxCategoricalOptionsToDisplay;
|
||||||
|
|
||||||
|
if (sortedCategories.length < N) {
|
||||||
|
return [sortedCategories, sortedCounts];
|
||||||
|
}
|
||||||
|
return [sortedCategories.slice(0, N), sortedCounts.slice(0, N)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCategoricalSelectionState(state, world) {
|
||||||
const res = {};
|
const res = {};
|
||||||
_.each(world.summary.obs, (value, key) => {
|
_.forEach(world.summary.obs, (value, key) => {
|
||||||
if (value.options && key !== "name") {
|
if (value.categories) {
|
||||||
const optionsAsBooleans = {};
|
const isColorField = key.includes("color") || key.includes("Color");
|
||||||
_.each(value.options, (_value, _key) => {
|
const isSelectableCategory =
|
||||||
optionsAsBooleans[_key] = true;
|
!isColorField &&
|
||||||
});
|
key !== "name" &&
|
||||||
res[key] = optionsAsBooleans;
|
value.categories.length < state.maxCategoryItems;
|
||||||
|
if (isSelectableCategory) {
|
||||||
|
const [categoryValues, categoryCounts] = topNCategories(value);
|
||||||
|
const categoryIndices = new Map(categoryValues.map((v, i) => [v, i]));
|
||||||
|
const numCategories = categoryIndices.size;
|
||||||
|
const categorySelected = new Array(numCategories).fill(true);
|
||||||
|
const isTruncated = categoryValues.length < value.numCategories;
|
||||||
|
res[key] = {
|
||||||
|
categoryValues, // array: of natively typed category values
|
||||||
|
categoryIndices, // map: category value (native type) -> category index
|
||||||
|
categorySelected, // array: t/f selection state
|
||||||
|
numCategories, // number: of categories
|
||||||
|
isTruncated, // bool: true if list was truncated
|
||||||
|
categoryCounts // array: cardinality of each category
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
given a categoricalSelectionState, return the list of all category values
|
||||||
|
where selection state is true (ie, they are selected).
|
||||||
|
*/
|
||||||
|
function selectedValuesForCategory(categorySelectionState) {
|
||||||
|
const selectedValues = _([...categorySelectionState.categoryIndices])
|
||||||
|
.filter(tuple => categorySelectionState.categorySelected[tuple[1]])
|
||||||
|
.map(tuple => tuple[0])
|
||||||
|
.value();
|
||||||
|
return selectedValues;
|
||||||
|
}
|
||||||
|
|
||||||
const Controls = (
|
const Controls = (
|
||||||
state = {
|
state = {
|
||||||
// data loading flag
|
// data loading flag
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
|
// configuration
|
||||||
|
maxCategoryItems: globals.configDefaults.parameters["max-category-items"],
|
||||||
|
|
||||||
|
// the whole big bang
|
||||||
universe: null,
|
universe: null,
|
||||||
|
|
||||||
// all of the data + selection state
|
// all of the data + selection state
|
||||||
world: null,
|
world: null,
|
||||||
colorName: null,
|
colorName: null,
|
||||||
colorRGB: null,
|
colorRGB: null,
|
||||||
categoricalAsBooleansMap: null,
|
categoricalSelectionState: null,
|
||||||
crossfilter: null,
|
crossfilter: null,
|
||||||
dimensionMap: null,
|
dimensionMap: null,
|
||||||
userDefinedGenes: [],
|
userDefinedGenes: [],
|
||||||
@@ -72,6 +147,17 @@ const Controls = (
|
|||||||
Initialization, World/Universe management
|
Initialization, World/Universe management
|
||||||
and data loading.
|
and data loading.
|
||||||
******************************************************/
|
******************************************************/
|
||||||
|
case "configuration load complete": {
|
||||||
|
// there are a couple of configuration items we need to retain
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
maxCategoryItems: _.get(
|
||||||
|
state.config,
|
||||||
|
"parameters.max-category-items",
|
||||||
|
globals.configDefaults.parameters["max-category-items"]
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
case "initial data load start": {
|
case "initial data load start": {
|
||||||
return { ...state, loading: true };
|
return { ...state, loading: true };
|
||||||
}
|
}
|
||||||
@@ -83,9 +169,13 @@ const Controls = (
|
|||||||
const world = World.createWorldFromEntireUniverse(universe);
|
const world = World.createWorldFromEntireUniverse(universe);
|
||||||
const colorName = new Array(universe.nObs).fill(globals.defaultCellColor);
|
const colorName = new Array(universe.nObs).fill(globals.defaultCellColor);
|
||||||
const colorRGB = _.map(colorName, c => parseRGB(c));
|
const colorRGB = _.map(colorName, c => parseRGB(c));
|
||||||
const categoricalAsBooleansMap = createCategoricalAsBooleansMap(world);
|
const categoricalSelectionState = createCategoricalSelectionState(
|
||||||
|
state,
|
||||||
|
world
|
||||||
|
);
|
||||||
const crossfilter = Crossfilter(world.obsAnnotations);
|
const crossfilter = Crossfilter(world.obsAnnotations);
|
||||||
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
||||||
|
WorldUtil.clearCaches();
|
||||||
|
|
||||||
const worldVarDataCache = world.varDataCache;
|
const worldVarDataCache = world.varDataCache;
|
||||||
|
|
||||||
@@ -135,7 +225,7 @@ const Controls = (
|
|||||||
world,
|
world,
|
||||||
colorName,
|
colorName,
|
||||||
colorRGB,
|
colorRGB,
|
||||||
categoricalAsBooleansMap,
|
categoricalSelectionState,
|
||||||
crossfilter,
|
crossfilter,
|
||||||
dimensionMap,
|
dimensionMap,
|
||||||
colorAccessor: null
|
colorAccessor: null
|
||||||
@@ -152,9 +242,13 @@ const Controls = (
|
|||||||
);
|
);
|
||||||
const colorName = new Array(world.nObs).fill(globals.defaultCellColor);
|
const colorName = new Array(world.nObs).fill(globals.defaultCellColor);
|
||||||
const colorRGB = _.map(colorName, c => parseRGB(c));
|
const colorRGB = _.map(colorName, c => parseRGB(c));
|
||||||
const categoricalAsBooleansMap = createCategoricalAsBooleansMap(world);
|
const categoricalSelectionState = createCategoricalSelectionState(
|
||||||
|
state,
|
||||||
|
world
|
||||||
|
);
|
||||||
const crossfilter = Crossfilter(world.obsAnnotations);
|
const crossfilter = Crossfilter(world.obsAnnotations);
|
||||||
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
||||||
|
WorldUtil.clearCaches();
|
||||||
|
|
||||||
const worldVarDataCache = world.varDataCache;
|
const worldVarDataCache = world.varDataCache;
|
||||||
/* var dimensions */
|
/* var dimensions */
|
||||||
@@ -197,7 +291,7 @@ const Controls = (
|
|||||||
world,
|
world,
|
||||||
colorName,
|
colorName,
|
||||||
colorRGB,
|
colorRGB,
|
||||||
categoricalAsBooleansMap,
|
categoricalSelectionState,
|
||||||
crossfilter,
|
crossfilter,
|
||||||
dimensionMap,
|
dimensionMap,
|
||||||
colorAccessor: null
|
colorAccessor: null
|
||||||
@@ -422,83 +516,89 @@ const Controls = (
|
|||||||
Categorical metadata
|
Categorical metadata
|
||||||
*******************************/
|
*******************************/
|
||||||
case "categorical metadata filter select": {
|
case "categorical metadata filter select": {
|
||||||
const newCategoricalAsBooleansMap = {
|
const newCategorySelected = Array.from(
|
||||||
...state.categoricalAsBooleansMap,
|
state.categoricalSelectionState[action.metadataField].categorySelected
|
||||||
|
);
|
||||||
|
newCategorySelected[action.categoryIndex] = true;
|
||||||
|
const newCategoricalSelectionState = {
|
||||||
|
...state.categoricalSelectionState,
|
||||||
[action.metadataField]: {
|
[action.metadataField]: {
|
||||||
...state.categoricalAsBooleansMap[action.metadataField],
|
...state.categoricalSelectionState[action.metadataField],
|
||||||
[action.value]: true
|
categorySelected: newCategorySelected
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// update the filter for the one category that changed state
|
|
||||||
|
// update the filter to match all selected options
|
||||||
|
const cat = newCategoricalSelectionState[action.metadataField];
|
||||||
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
||||||
_.filter(
|
selectedValuesForCategory(cat)
|
||||||
_.map(
|
|
||||||
newCategoricalAsBooleansMap[action.metadataField],
|
|
||||||
(val, key) => (val ? key : false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
categoricalSelectionState: newCategoricalSelectionState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "categorical metadata filter deselect": {
|
case "categorical metadata filter deselect": {
|
||||||
const newCategoricalAsBooleansMap = {
|
const newCategorySelected = Array.from(
|
||||||
...state.categoricalAsBooleansMap,
|
state.categoricalSelectionState[action.metadataField].categorySelected
|
||||||
|
);
|
||||||
|
newCategorySelected[action.categoryIndex] = false;
|
||||||
|
const newCategoricalSelectionState = {
|
||||||
|
...state.categoricalSelectionState,
|
||||||
[action.metadataField]: {
|
[action.metadataField]: {
|
||||||
...state.categoricalAsBooleansMap[action.metadataField],
|
...state.categoricalSelectionState[action.metadataField],
|
||||||
[action.value]: false
|
categorySelected: newCategorySelected
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// update the filter for the one category that changed state
|
|
||||||
|
// update the filter to match all selected options
|
||||||
|
const cat = newCategoricalSelectionState[action.metadataField];
|
||||||
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
state.dimensionMap[obsAnnoDimensionName(action.metadataField)].filterEnum(
|
||||||
_.filter(
|
selectedValuesForCategory(cat)
|
||||||
_.map(
|
|
||||||
newCategoricalAsBooleansMap[action.metadataField],
|
|
||||||
(val, key) => (val ? key : false)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
categoricalSelectionState: newCategoricalSelectionState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "categorical metadata filter none of these": {
|
case "categorical metadata filter none of these": {
|
||||||
const newCategoricalAsBooleansMap = {
|
const newCategoricalSelectionState = {
|
||||||
...state.categoricalAsBooleansMap
|
...state.categoricalSelectionState,
|
||||||
};
|
[action.metadataField]: {
|
||||||
_.forEach(
|
...state.categoricalSelectionState[action.metadataField],
|
||||||
newCategoricalAsBooleansMap[action.metadataField],
|
categorySelected: Array.from(
|
||||||
(v, k, c) => {
|
state.categoricalSelectionState[action.metadataField]
|
||||||
c[k] = false;
|
.categorySelected
|
||||||
|
).fill(false)
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
state.dimensionMap[
|
state.dimensionMap[
|
||||||
obsAnnoDimensionName(action.metadataField)
|
obsAnnoDimensionName(action.metadataField)
|
||||||
].filterNone();
|
].filterNone();
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
categoricalSelectionState: newCategoricalSelectionState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "categorical metadata filter all of these": {
|
case "categorical metadata filter all of these": {
|
||||||
const newCategoricalAsBooleansMap = {
|
const newCategoricalSelectionState = {
|
||||||
...state.categoricalAsBooleansMap
|
...state.categoricalSelectionState,
|
||||||
};
|
[action.metadataField]: {
|
||||||
_.forEach(
|
...state.categoricalSelectionState[action.metadataField],
|
||||||
newCategoricalAsBooleansMap[action.metadataField],
|
categorySelected: Array.from(
|
||||||
(v, k, c) => {
|
state.categoricalSelectionState[action.metadataField]
|
||||||
c[k] = true;
|
.categorySelected
|
||||||
|
).fill(true)
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
state.dimensionMap[
|
state.dimensionMap[
|
||||||
obsAnnoDimensionName(action.metadataField)
|
obsAnnoDimensionName(action.metadataField)
|
||||||
].filterAll();
|
].filterAll();
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
categoricalSelectionState: newCategoricalSelectionState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ export const doJsonRequest = async url => {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
// else an error
|
// else an error
|
||||||
const msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`;
|
let msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`;
|
||||||
|
const body = await res.text();
|
||||||
|
if (body && body.length > 0) {
|
||||||
|
msg = `${msg} -- ${body}`;
|
||||||
|
}
|
||||||
dispatchNetworkErrorMessageToUser(msg);
|
dispatchNetworkErrorMessageToUser(msg);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ exists to support those concepts.
|
|||||||
export * as Universe from "./universe";
|
export * as Universe from "./universe";
|
||||||
export * as World from "./world";
|
export * as World from "./world";
|
||||||
export * as kvCache from "./keyvalcache";
|
export * as kvCache from "./keyvalcache";
|
||||||
|
export * as WorldUtil from "./worldUtil";
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Value will be an object, containing summary information.
|
|||||||
|
|
||||||
For continuous annotations (int, float, etc):
|
For continuous annotations (int, float, etc):
|
||||||
<annotation_name>: {
|
<annotation_name>: {
|
||||||
|
categorical: false,
|
||||||
range {
|
range {
|
||||||
min: <number>,
|
min: <number>,
|
||||||
max: <number>
|
max: <number>
|
||||||
@@ -15,12 +16,14 @@ For continuous annotations (int, float, etc):
|
|||||||
}
|
}
|
||||||
|
|
||||||
For categorical annotations (boolean, string, category):
|
For categorical annotations (boolean, string, category):
|
||||||
<annotatoin_name>: {
|
<annotation_name>: {
|
||||||
options: {
|
categorical: true,
|
||||||
<option1>: <number>,
|
categories: [ <category1>, <category2>, ... ]
|
||||||
|
categoryCounts: Map {
|
||||||
|
<category1>: <number>,
|
||||||
...
|
...
|
||||||
},
|
},
|
||||||
numOptions: <number>
|
numCategories: <number>
|
||||||
}
|
}
|
||||||
|
|
||||||
Summarize will be returned for BOTH obs and var annotations.
|
Summarize will be returned for BOTH obs and var annotations.
|
||||||
@@ -28,19 +31,19 @@ Summarize will be returned for BOTH obs and var annotations.
|
|||||||
Example:
|
Example:
|
||||||
{
|
{
|
||||||
"Splice_sites_Annotated": {
|
"Splice_sites_Annotated": {
|
||||||
"range": {
|
categorical: false,
|
||||||
|
range: {
|
||||||
"min": 26,
|
"min": 26,
|
||||||
"max": 1075869
|
"max": 1075869
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Selection": {
|
"Selection": {
|
||||||
numOptions, 6,
|
categorical: true,
|
||||||
"options": {
|
numCategories, 3,
|
||||||
|
categories: [ "Astrocytes(HEPACAM)", "Endothelial(BSC)", "Unpanned" ],
|
||||||
|
categoryCounts: Map {
|
||||||
"Astrocytes(HEPACAM)": 714,
|
"Astrocytes(HEPACAM)": 714,
|
||||||
"Endothelial(BSC)": 123,
|
"Endothelial(BSC)": 123,
|
||||||
"Oligodendrocytes(GC)": 294,
|
|
||||||
"Neurons(Thy1)": 685,
|
|
||||||
"Microglia(CD45)": 1108,
|
|
||||||
"Unpanned": 665
|
"Unpanned": 665
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,37 +52,45 @@ Example:
|
|||||||
NOTE: will not summarize the required 'name' annotation, as that is
|
NOTE: will not summarize the required 'name' annotation, as that is
|
||||||
specified as unique per element.
|
specified as unique per element.
|
||||||
*/
|
*/
|
||||||
function summarizeDimension(schema, annotations) {
|
function _summarizeAnnotations(_schema, annotations) {
|
||||||
return _(schema)
|
const summary = _(_schema) // lodash wrapping: https://lodash.com/docs/4.17.11#lodash
|
||||||
.filter(v => v.name !== "name")
|
.filter(v => v.name !== "name")
|
||||||
.keyBy("name")
|
.keyBy("name")
|
||||||
.mapValues(anno => {
|
.mapValues(anno => {
|
||||||
const { name, type } = anno;
|
const { name, type } = anno;
|
||||||
const continuous = type === "int32" || type === "float32";
|
const continuous = type === "int32" || type === "float32";
|
||||||
|
|
||||||
if (!continuous) {
|
|
||||||
const options = _.countBy(annotations, name);
|
|
||||||
const numOptions = _.size(options);
|
|
||||||
return {
|
|
||||||
numOptions,
|
|
||||||
options
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (continuous) {
|
if (continuous) {
|
||||||
let min = Number.POSITIVE_INFINITY;
|
let min = Number.POSITIVE_INFINITY;
|
||||||
let max = Number.NEGATIVE_INFINITY;
|
let max = Number.NEGATIVE_INFINITY;
|
||||||
_.forEach(annotations, obs => {
|
for (let r = 0; r < annotations.length; r += 1) {
|
||||||
const val = Number(obs[name]);
|
const val = Number(annotations[r][name]);
|
||||||
min = val < min ? val : min;
|
min = val < min ? val : min;
|
||||||
max = val > max ? val : max;
|
max = val > max ? val : max;
|
||||||
});
|
}
|
||||||
return { range: { min, max } };
|
return {
|
||||||
|
categorical: false,
|
||||||
|
range: { min, max }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("incomprehensible schema");
|
/* else categorical */
|
||||||
|
const categoryCounts = new Map();
|
||||||
|
for (let r = 0; r < annotations.length; r += 1) {
|
||||||
|
const val = annotations[r][name];
|
||||||
|
let curCount = categoryCounts.get(val);
|
||||||
|
if (curCount === undefined) curCount = 0;
|
||||||
|
categoryCounts.set(val, curCount + 1);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
categorical: true,
|
||||||
|
categories: [...categoryCounts.keys()],
|
||||||
|
categoryCounts,
|
||||||
|
numCategories: categoryCounts.size
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.value();
|
.value();
|
||||||
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function summarizeAnnotations(
|
export default function summarizeAnnotations(
|
||||||
@@ -88,7 +99,7 @@ export default function summarizeAnnotations(
|
|||||||
varAnnotations
|
varAnnotations
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
obs: summarizeDimension(schema.annotations.obs, obsAnnotations),
|
obs: _summarizeAnnotations(schema.annotations.obs, obsAnnotations),
|
||||||
var: summarizeDimension(schema.annotations.var, varAnnotations)
|
var: _summarizeAnnotations(schema.annotations.var, varAnnotations)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,32 @@ function RESTv02LayoutResponseToInternal(response) {
|
|||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reconcileSchemaCategoriesWithSummary(universe) {
|
||||||
|
/*
|
||||||
|
where we treat types as (essentially) categorical metadata, update
|
||||||
|
the schema with data-derived categories (in addition to those in
|
||||||
|
the server declared schema).
|
||||||
|
|
||||||
|
For example, boolean defined fields in the schema do not contain
|
||||||
|
explicit declaration of categories (nor do string fields). In these
|
||||||
|
cases, add a 'categories' field to the schema so it is accessible.
|
||||||
|
*/
|
||||||
|
|
||||||
|
_.forEach(universe.schema.annotations.obs, s => {
|
||||||
|
if (
|
||||||
|
s.type === "string" ||
|
||||||
|
s.type === "boolean" ||
|
||||||
|
s.type === "categorical"
|
||||||
|
) {
|
||||||
|
const categories = _.union(
|
||||||
|
_.get(s, "categories", []),
|
||||||
|
_.get(universe.summary.obs[s.name], "categories", [])
|
||||||
|
);
|
||||||
|
s.categories = categories;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createUniverseFromRestV02Response(
|
export function createUniverseFromRestV02Response(
|
||||||
configResponse,
|
configResponse,
|
||||||
schemaResponse,
|
schemaResponse,
|
||||||
@@ -199,6 +225,7 @@ export function createUniverseFromRestV02Response(
|
|||||||
universe.varAnnotations
|
universe.varAnnotations
|
||||||
);
|
);
|
||||||
|
|
||||||
|
reconcileSchemaCategoriesWithSummary(universe);
|
||||||
return finalize(universe);
|
return finalize(universe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Various utility functions operating on World/Universe
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Count unique category values, binning first by dim1 then by dim2
|
||||||
|
Return:
|
||||||
|
|
||||||
|
Map {
|
||||||
|
dim1_val1: Map {
|
||||||
|
dim2_val1: number,
|
||||||
|
dim2_val2: number,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
function _countCategoryValues2D(dim1, dim2, rows) {
|
||||||
|
const dimMap = new Map();
|
||||||
|
for (let r = 0; r < rows.length; r += 1) {
|
||||||
|
const row = rows[r];
|
||||||
|
const val1 = row[dim1];
|
||||||
|
const val2 = row[dim2];
|
||||||
|
let d2Map = dimMap.get(val1);
|
||||||
|
if (d2Map === undefined) {
|
||||||
|
d2Map = new Map();
|
||||||
|
dimMap.set(val1, d2Map);
|
||||||
|
}
|
||||||
|
let curCount = d2Map.get(val2);
|
||||||
|
if (curCount === undefined) {
|
||||||
|
curCount = 0;
|
||||||
|
}
|
||||||
|
d2Map.set(val2, curCount + 1);
|
||||||
|
}
|
||||||
|
return dimMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
let __worldUtilMemoId__ = 0;
|
||||||
|
function _memoizedId(x) {
|
||||||
|
if (!x.__worldUtilMemoId__) {
|
||||||
|
__worldUtilMemoId__ += 1;
|
||||||
|
x.__worldUtilMemoId__ = __worldUtilMemoId__;
|
||||||
|
}
|
||||||
|
return x.__worldUtilMemoId__;
|
||||||
|
}
|
||||||
|
function _countCategoryValues2DResolver(...args) {
|
||||||
|
const id = args[0] + args[1] + _memoizedId(args[2]);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const countCategoryValues2D = _.memoize(
|
||||||
|
_countCategoryValues2D,
|
||||||
|
_countCategoryValues2DResolver
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Clear any cached data within WorldUtil caches, eg, memoized functions
|
||||||
|
*/
|
||||||
|
export function clearCaches() {
|
||||||
|
countCategoryValues2D.cache.clear();
|
||||||
|
}
|
||||||
+15
-14
@@ -20,35 +20,36 @@ Follow these steps to create a release.
|
|||||||
|
|
||||||
1. Preparation:
|
1. Preparation:
|
||||||
- Define the release version number, using [semantic versioning](https://semver.org/)
|
- Define the release version number, using [semantic versioning](https://semver.org/)
|
||||||
- Write the release title and release notes
|
- Write the release title and release notes and add to
|
||||||
|
[release notes document](https://docs.google.com/document/d/1KnHwkYfhyWO5H8BDcMu7y3ogjvq5Yi4OwpmZ8DB6w0Y/edit)
|
||||||
2. Create a release branch, eg, `release-version`
|
2. Create a release branch, eg, `release-version`
|
||||||
3. In the release branch:
|
3. In the release branch:
|
||||||
- run `bumpversion --config-file .bumpversion.cfg [major | minor | patch]`
|
- Run `bumpversion --config-file .bumpversion.cfg [major | minor | patch]`
|
||||||
- clean up existing environment using `bin/clean`
|
- Clean up existing environment using `bin/clean`
|
||||||
- build the JS asserts using `bin/build-client`
|
- Build the JS asserts using `bin/build-client`
|
||||||
4. Commit and push the new branch
|
4. Commit and push the new branch
|
||||||
5. Create a PR for the release.
|
5. Create a PR for the release.
|
||||||
- [optional] As needed, conduct PR review.
|
- [optional] As needed, conduct PR review.
|
||||||
6. Merge to master
|
6. Merge to master
|
||||||
7. Create Github release using the version number and release notes ([instructions](https://help.github.com/articles/creating-releases/)).
|
7. Create Github release using the version number and release notes
|
||||||
|
([instructions](https://help.github.com/articles/creating-releases/)).
|
||||||
- Draft new release
|
- Draft new release
|
||||||
- Type version name matching release version number from (1)
|
- Type version name matching release version number from (1)
|
||||||
- Select `master` as release branch (ensure you merged the release PR)
|
- Select `master` as release branch (ensure you merged the release PR)
|
||||||
- Type title `Release {version num}`
|
- Type title `Release {version num}`
|
||||||
- [optional] check pre-release if this release is not ready for production
|
- [optional] Check pre-release if this release is not ready for production
|
||||||
- Publish Release
|
- Publish Release
|
||||||
8. Publish to pypi by performing the following steps
|
8. Publish to pypi by performing the following steps (assumes you have `setuptools` and `twine` installed and that you
|
||||||
(assumes you have `setuptools` and `twine` installed and that you have
|
have registered for pypi and have write access to the cellxgene pypi package)
|
||||||
registered for pypi and have write access to the cellxgene pypi package)
|
- Build the distribution by calling
|
||||||
- build the distribution by calling
|
|
||||||
`python setup.py sdist`
|
`python setup.py sdist`
|
||||||
inside the top-level directory
|
inside the top-level directory
|
||||||
- [optional] upload the package to test pypi
|
- [optional] Upload the package to test pypi
|
||||||
`twine upload --repository-url https://test.pypi.org/legacy/ dist/*`
|
`twine upload --repository-url https://test.pypi.org/legacy/ dist/*`
|
||||||
- [optional] test the test installation in a fresh virtual environment using
|
- [optional] Test the test installation in a fresh virtual environment using
|
||||||
`pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cellxgene`
|
`pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cellxgene`
|
||||||
- upload the package to real pypi using `twine upload dist/*`
|
- Upload the package to real pypi using `twine upload dist/*`
|
||||||
- [optional] test the installation in a fresh virtual environment using
|
- [optional] Test the installation in a fresh virtual environment using
|
||||||
`pip install cellxgene`
|
`pip install cellxgene`
|
||||||
|
|
||||||
The optional steps are for testing purposes, and are recommended
|
The optional steps are for testing purposes, and are recommended
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import warnings
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, current_app, jsonify, make_response, request
|
Blueprint, current_app, jsonify, make_response, request
|
||||||
@@ -7,7 +8,7 @@ from flask import (
|
|||||||
from flask_restful_swagger_2 import Api, swagger, Resource
|
from flask_restful_swagger_2 import Api, swagger, Resource
|
||||||
from werkzeug.datastructures import ImmutableMultiDict
|
from werkzeug.datastructures import ImmutableMultiDict
|
||||||
|
|
||||||
from server.app.util.constants import Axis, DiffExpMode
|
from server.app.util.constants import Axis, DiffExpMode, JSON_NaN_to_num_warning_msg
|
||||||
from server.app.util.filter import parse_filter, QueryStringError
|
from server.app.util.filter import parse_filter, QueryStringError
|
||||||
from server.app.util.models import FilterModel
|
from server.app.util.models import FilterModel
|
||||||
from server.app.util.utils import get_mime_type
|
from server.app.util.utils import get_mime_type
|
||||||
@@ -160,7 +161,12 @@ class AnnotationsObsAPI(Resource):
|
|||||||
annotation_response = current_app.data.annotation({}, "obs", fields)
|
annotation_response = current_app.data.annotation({}, "obs", fields)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
||||||
|
try:
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
@swagger.doc({
|
@swagger.doc({
|
||||||
"summary": "Fetch annotations (metadata) for filtered subset of observations.",
|
"summary": "Fetch annotations (metadata) for filtered subset of observations.",
|
||||||
@@ -211,7 +217,12 @@ class AnnotationsObsAPI(Resource):
|
|||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
||||||
except FilterError as e:
|
except FilterError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
|
try:
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class AnnotationsVarAPI(Resource):
|
class AnnotationsVarAPI(Resource):
|
||||||
@@ -253,7 +264,12 @@ class AnnotationsVarAPI(Resource):
|
|||||||
annotation_response = current_app.data.annotation({}, "var", fields)
|
annotation_response = current_app.data.annotation({}, "var", fields)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
||||||
|
try:
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
@swagger.doc({
|
@swagger.doc({
|
||||||
"summary": "Fetch annotations (metadata) for filtered subset of variables.",
|
"summary": "Fetch annotations (metadata) for filtered subset of variables.",
|
||||||
@@ -302,7 +318,12 @@ class AnnotationsVarAPI(Resource):
|
|||||||
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
return make_response(f"Error bad key in {fields}", HTTPStatus.BAD_REQUEST)
|
||||||
except FilterError:
|
except FilterError:
|
||||||
return make_response("Malformed filter", HTTPStatus.BAD_REQUEST)
|
return make_response("Malformed filter", HTTPStatus.BAD_REQUEST)
|
||||||
|
try:
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class DataObsAPI(Resource):
|
class DataObsAPI(Resource):
|
||||||
@@ -363,6 +384,10 @@ class DataObsAPI(Resource):
|
|||||||
return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.OBS))), HTTPStatus.OK)
|
return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.OBS))), HTTPStatus.OK)
|
||||||
except FilterError as e:
|
except FilterError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
@swagger.doc({
|
@swagger.doc({
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
"summary": "Get data (expression values) from the dataframe.",
|
||||||
@@ -407,6 +432,10 @@ class DataObsAPI(Resource):
|
|||||||
HTTPStatus.OK)
|
HTTPStatus.OK)
|
||||||
except FilterError as e:
|
except FilterError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class DataVarAPI(Resource):
|
class DataVarAPI(Resource):
|
||||||
@@ -465,6 +494,10 @@ class DataVarAPI(Resource):
|
|||||||
return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.VAR))), HTTPStatus.OK)
|
return make_response((jsonify(current_app.data.data_frame(filter_, axis=Axis.VAR))), HTTPStatus.OK)
|
||||||
except FilterError as e:
|
except FilterError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
@swagger.doc({
|
@swagger.doc({
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
"summary": "Get data (expression values) from the dataframe.",
|
||||||
@@ -510,6 +543,10 @@ class DataVarAPI(Resource):
|
|||||||
HTTPStatus.OK)
|
HTTPStatus.OK)
|
||||||
except FilterError as e:
|
except FilterError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class DiffExpObsAPI(Resource):
|
class DiffExpObsAPI(Resource):
|
||||||
@@ -614,7 +651,12 @@ class DiffExpObsAPI(Resource):
|
|||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
except InteractiveError:
|
except InteractiveError:
|
||||||
return make_response("Non-interactive request", HTTPStatus.FORBIDDEN)
|
return make_response("Non-interactive request", HTTPStatus.FORBIDDEN)
|
||||||
|
try:
|
||||||
return make_response(jsonify(diffexp), HTTPStatus.OK)
|
return make_response(jsonify(diffexp), HTTPStatus.OK)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
class LayoutObsAPI(Resource):
|
class LayoutObsAPI(Resource):
|
||||||
@@ -647,7 +689,12 @@ class LayoutObsAPI(Resource):
|
|||||||
layout = current_app.data.layout({})
|
layout = current_app.data.layout({})
|
||||||
except PrepareError as e:
|
except PrepareError as e:
|
||||||
return make_response(e.message, HTTPStatus.INTERNAL_SERVER_ERROR)
|
return make_response(e.message, HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
try:
|
||||||
return make_response((jsonify({"layout": layout})), HTTPStatus.OK)
|
return make_response((jsonify({"layout": layout})), HTTPStatus.OK)
|
||||||
|
except ValueError as e:
|
||||||
|
# JSON encoding failure, usually due to bad data
|
||||||
|
warnings.warn(JSON_NaN_to_num_warning_msg)
|
||||||
|
return make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
# @swagger.doc({
|
# @swagger.doc({
|
||||||
# "summary": "Observation layout for filtered subset.",
|
# "summary": "Observation layout for filtered subset.",
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class ScanpyEngine(CXGDriver):
|
|||||||
self.diffexp_options = ["ttest"]
|
self.diffexp_options = ["ttest"]
|
||||||
self._create_schema()
|
self._create_schema()
|
||||||
|
|
||||||
|
# TODO: temporary work-arounds
|
||||||
|
if args['nan_to_num']:
|
||||||
|
self._IEEE754_special_values_workaround()
|
||||||
|
|
||||||
def _alias_annotation_names(self, axis, name):
|
def _alias_annotation_names(self, axis, name):
|
||||||
"""
|
"""
|
||||||
Do all user-specified annotation aliasing.
|
Do all user-specified annotation aliasing.
|
||||||
@@ -65,6 +69,24 @@ class ScanpyEngine(CXGDriver):
|
|||||||
else:
|
else:
|
||||||
raise KeyError(f"Annotation name {name}, specified in --{ax_name}_name does not exist.")
|
raise KeyError(f"Annotation name {name}, specified in --{ax_name}_name does not exist.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _can_cast_to_float32(ann):
|
||||||
|
if ann.dtype.kind == "f":
|
||||||
|
if not np.can_cast(ann.dtype, np.float32):
|
||||||
|
warnings.warn(f"Annotation {ann.name} will be converted to 32 bit float and may lose precision.")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _can_cast_to_int32(ann):
|
||||||
|
if ann.dtype.kind in ["i", "u"]:
|
||||||
|
if np.can_cast(ann.dtype, np.int32):
|
||||||
|
return True
|
||||||
|
ii32 = np.iinfo(np.int32)
|
||||||
|
if ann.min() >= ii32.min and ann.max() <= ii32.max:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def _create_schema(self):
|
def _create_schema(self):
|
||||||
self.schema = {
|
self.schema = {
|
||||||
"dataframe": {
|
"dataframe": {
|
||||||
@@ -81,16 +103,18 @@ class ScanpyEngine(CXGDriver):
|
|||||||
curr_axis = getattr(self.data, str(ax))
|
curr_axis = getattr(self.data, str(ax))
|
||||||
for ann in curr_axis:
|
for ann in curr_axis:
|
||||||
ann_schema = {"name": ann}
|
ann_schema = {"name": ann}
|
||||||
data_kind = curr_axis[ann].dtype.kind
|
dtype = curr_axis[ann].dtype
|
||||||
if data_kind == "f":
|
data_kind = dtype.kind
|
||||||
|
|
||||||
|
if self._can_cast_to_float32(curr_axis[ann]):
|
||||||
ann_schema["type"] = "float32"
|
ann_schema["type"] = "float32"
|
||||||
elif data_kind in ["i", "u"]:
|
elif self._can_cast_to_int32(curr_axis[ann]):
|
||||||
ann_schema["type"] = "int32"
|
ann_schema["type"] = "int32"
|
||||||
elif data_kind == "?":
|
elif dtype == np.bool_:
|
||||||
ann_schema["type"] = "boolean"
|
ann_schema["type"] = "boolean"
|
||||||
elif data_kind == "O" and curr_axis[ann].dtype == "object":
|
elif data_kind == "O" and dtype == "object":
|
||||||
ann_schema["type"] = "string"
|
ann_schema["type"] = "string"
|
||||||
elif data_kind == "O" and curr_axis[ann].dtype == "category":
|
elif data_kind == "O" and dtype == "category":
|
||||||
ann_schema["type"] = "categorical"
|
ann_schema["type"] = "categorical"
|
||||||
ann_schema["categories"] = curr_axis[ann].dtype.categories.tolist()
|
ann_schema["categories"] = curr_axis[ann].dtype.categories.tolist()
|
||||||
else:
|
else:
|
||||||
@@ -152,6 +176,67 @@ class ScanpyEngine(CXGDriver):
|
|||||||
f"`cellxgene prepare --layout {self.layout_method} <datafile>` "
|
f"`cellxgene prepare --layout {self.layout_method} <datafile>` "
|
||||||
f"to solve this problem. ")
|
f"to solve this problem. ")
|
||||||
|
|
||||||
|
def _IEEE754_special_values_workaround(self):
|
||||||
|
"""
|
||||||
|
TODO: temporary workaround
|
||||||
|
|
||||||
|
Because all floating point data is serialized to JSON, and JSON has no means of representing
|
||||||
|
non-finite, floating point special values (NaN, +/-Infinity, etc), we include this temporary
|
||||||
|
work-around.
|
||||||
|
|
||||||
|
This will likely be removed in the future, contingent upon improved marshalling.
|
||||||
|
|
||||||
|
Where non-finite floating point is present in obs, var or X:
|
||||||
|
* issue a warning to the user that these values will be convert to finite numbers.
|
||||||
|
* set NaN to zero, and Infinities to min/max of the element.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# annotations
|
||||||
|
for ax in Axis:
|
||||||
|
curr_axis = getattr(self.data, str(ax))
|
||||||
|
for ann in curr_axis:
|
||||||
|
dtype = curr_axis[ann].dtype
|
||||||
|
if dtype.kind == 'f':
|
||||||
|
finite_idx = np.isfinite(curr_axis[ann])
|
||||||
|
if not finite_idx.all():
|
||||||
|
curr_axis.loc[np.isnan(curr_axis[ann]), ann] = 0
|
||||||
|
curr_axis.loc[np.isneginf(curr_axis[ann]), ann] = curr_axis[ann][finite_idx].min()
|
||||||
|
curr_axis.loc[np.isposinf(curr_axis[ann]), ann] = curr_axis[ann][finite_idx].max()
|
||||||
|
warnings.warn(
|
||||||
|
f"{str(ax).title()} annotation '{ann}' contains floating point NaN or Infinities. "
|
||||||
|
f"These will be converted to finite values."
|
||||||
|
)
|
||||||
|
|
||||||
|
# X
|
||||||
|
non_finite_X_found = False
|
||||||
|
if sparse.issparse(self.data._X):
|
||||||
|
coo = self.data._X.tocoo()
|
||||||
|
finite_idx = np.isfinite(coo.data)
|
||||||
|
if not finite_idx.all():
|
||||||
|
non_finite_X_found = True
|
||||||
|
coo.data[np.isnan(coo.data)] = 0
|
||||||
|
coo.data[np.isneginf(coo.data)] = np.min(coo.data[finite_idx])
|
||||||
|
coo.data[np.isposinf(coo.data)] = np.max(coo.data[finite_idx])
|
||||||
|
coo.eliminate_zeros()
|
||||||
|
_X = coo.asformat(self.data._X.getformat())
|
||||||
|
self.data._X = _X
|
||||||
|
else:
|
||||||
|
_X = self.data._X
|
||||||
|
finite_idx = np.isfinite(_X.flat)
|
||||||
|
if not finite_idx.all():
|
||||||
|
non_finite_X_found = True
|
||||||
|
min_X = _X.flat[finite_idx].min()
|
||||||
|
max_X = _X.flat[finite_idx].max()
|
||||||
|
_X[np.isnan(_X)] = 0
|
||||||
|
_X[np.isneginf(_X)] = min_X
|
||||||
|
_X[np.isposinf(_X)] = max_X
|
||||||
|
|
||||||
|
if non_finite_X_found:
|
||||||
|
warnings.warn(
|
||||||
|
"Dataframe X contains floating point NaN or Infinities. "
|
||||||
|
"These will be converted to finite values."
|
||||||
|
)
|
||||||
|
|
||||||
def filter_dataframe(self, filter):
|
def filter_dataframe(self, filter):
|
||||||
"""
|
"""
|
||||||
Filter cells from data and return a subset of the data. They can operate on both obs and var dimension with
|
Filter cells from data and return a subset of the data. They can operate on both obs and var dimension with
|
||||||
|
|||||||
@@ -25,3 +25,6 @@ class Axis(AugmentedEnum):
|
|||||||
class DiffExpMode(AugmentedEnum):
|
class DiffExpMode(AugmentedEnum):
|
||||||
TOP_N = "topN"
|
TOP_N = "topN"
|
||||||
VAR_FILTER = "varFilter"
|
VAR_FILTER = "varFilter"
|
||||||
|
|
||||||
|
|
||||||
|
JSON_NaN_to_num_warning_msg = "JSON encoding failure - suggest trying --nan-to-num command line option"
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ from server.app.util.errors import MimeTypeError
|
|||||||
|
|
||||||
|
|
||||||
class Float32JSONEncoder(json.JSONEncoder):
|
class Float32JSONEncoder(json.JSONEncoder):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
NaN/Infinities are illegal in standard JSON. Python extends JSON with
|
||||||
|
non-standard symbols that most JavaScript JSON parsers do not understand.
|
||||||
|
The `allow_nan` parameter will force Python simplejson to throw an ValueError
|
||||||
|
if it runs into non-finite floating point values which are unsupported by
|
||||||
|
standard JSON.
|
||||||
|
"""
|
||||||
|
kwargs['allow_nan'] = False
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, float32):
|
if isinstance(obj, float32):
|
||||||
return float(obj)
|
return float(obj)
|
||||||
@@ -15,6 +26,10 @@ class Float32JSONEncoder(json.JSONEncoder):
|
|||||||
return json.JSONEncoder.default(self, obj)
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|
||||||
|
def custom_format_warning(msg, *args, **kwargs):
|
||||||
|
return f"[cellxgene] Warning: {msg} \n"
|
||||||
|
|
||||||
|
|
||||||
def get_mime_type(default="application/json", acceptable_types=["application/json", "text/csv"], query_param=None,
|
def get_mime_type(default="application/json", acceptable_types=["application/json", "text/csv"], query_param=None,
|
||||||
header=None):
|
header=None):
|
||||||
mime_type = default
|
mime_type = default
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ from .prepare import prepare
|
|||||||
|
|
||||||
|
|
||||||
@click.group(name="cellxgene", context_settings=dict(max_content_width=85))
|
@click.group(name="cellxgene", context_settings=dict(max_content_width=85))
|
||||||
@click.version_option(version="0.2.0", prog_name="cellxgene", message="[%(prog)s] Version %(version)s")
|
@click.version_option(version="0.3.0", prog_name="cellxgene", message="[%(prog)s] Version %(version)s")
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
+17
-14
@@ -1,11 +1,14 @@
|
|||||||
import sys
|
|
||||||
import click
|
|
||||||
import logging
|
import logging
|
||||||
from os import devnull
|
from os import devnull
|
||||||
from os.path import splitext, basename
|
from os.path import splitext, basename
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
from server.app.util.errors import ScanpyFileError
|
from server.app.util.errors import ScanpyFileError
|
||||||
|
from server.app.util.utils import custom_format_warning
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@@ -24,14 +27,16 @@ from server.app.util.errors import ScanpyFileError
|
|||||||
@click.option("--port", "-p", help="Port to run server on.", metavar="", default=5005, show_default=True)
|
@click.option("--port", "-p", help="Port to run server on.", metavar="", default=5005, show_default=True)
|
||||||
@click.option("--obs-names", default=None, metavar="", help="Name of annotation field to use for observations.")
|
@click.option("--obs-names", default=None, metavar="", help="Name of annotation field to use for observations.")
|
||||||
@click.option("--var-names", default=None, metavar="", help="Name of annotation to use for variables.")
|
@click.option("--var-names", default=None, metavar="", help="Name of annotation to use for variables.")
|
||||||
@click.option("--listen-all", is_flag=True, default=False, show_default=True,
|
@click.option("--host", default="127.0.0.1", help="Host IP address")
|
||||||
help="Bind to all interfaces (this makes the server accessible beyond this computer).")
|
|
||||||
@click.option("--max-category-items", default=100, metavar="", show_default=True,
|
@click.option("--max-category-items", default=100, metavar="", show_default=True,
|
||||||
help="Limits the number of categorical annotation items displayed.")
|
help="Limits the number of categorical annotation items displayed.")
|
||||||
@click.option("--diffexp-lfc-cutoff", default=0.01, show_default=True,
|
@click.option("--diffexp-lfc-cutoff", default=0.01, show_default=True,
|
||||||
help="Relative expression cutoff used when selecting top N differentially expressed genes")
|
help="Relative expression cutoff used when selecting top N differentially expressed genes")
|
||||||
|
@click.option("--nan-to-num", is_flag=True, default=False, show_default=True,
|
||||||
|
help="Replace all floating point NaN with zero, and infinities with finite numbers")
|
||||||
def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
||||||
open_browser, port, listen_all, max_category_items, diffexp_lfc_cutoff):
|
open_browser, port, host, max_category_items, diffexp_lfc_cutoff,
|
||||||
|
nan_to_num):
|
||||||
"""Launch the cellxgene data viewer.
|
"""Launch the cellxgene data viewer.
|
||||||
This web app lets you explore single-cell expression data.
|
This web app lets you explore single-cell expression data.
|
||||||
Data must be in a format that cellxgene expects, read the
|
Data must be in a format that cellxgene expects, read the
|
||||||
@@ -46,9 +51,6 @@ def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
|||||||
# Startup message
|
# Startup message
|
||||||
click.echo("[cellxgene] Starting the CLI...")
|
click.echo("[cellxgene] Starting the CLI...")
|
||||||
|
|
||||||
# Import Flask app
|
|
||||||
from server.app.app import app
|
|
||||||
|
|
||||||
# Argument checking
|
# Argument checking
|
||||||
name, extension = splitext(data)
|
name, extension = splitext(data)
|
||||||
if extension != ".h5ad":
|
if extension != ".h5ad":
|
||||||
@@ -57,6 +59,8 @@ def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
|||||||
if debug:
|
if debug:
|
||||||
verbose = True
|
verbose = True
|
||||||
open_browser = False
|
open_browser = False
|
||||||
|
else:
|
||||||
|
warnings.formatwarning = custom_format_warning
|
||||||
|
|
||||||
if not verbose:
|
if not verbose:
|
||||||
sys.tracebacklimit = 0
|
sys.tracebacklimit = 0
|
||||||
@@ -65,15 +69,13 @@ def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
|||||||
file_parts = splitext(basename(data))
|
file_parts = splitext(basename(data))
|
||||||
title = file_parts[0]
|
title = file_parts[0]
|
||||||
|
|
||||||
if listen_all:
|
|
||||||
host = "0.0.0.0"
|
|
||||||
else:
|
|
||||||
host = "127.0.0.1"
|
|
||||||
|
|
||||||
# Setup app
|
# Setup app
|
||||||
cellxgene_url = f"http://{host}:{port}"
|
cellxgene_url = f"http://{host}:{port}"
|
||||||
api_base = f"{cellxgene_url}/api/"
|
api_base = f"{cellxgene_url}/api/"
|
||||||
|
|
||||||
|
# Import Flask app
|
||||||
|
from server.app.app import app
|
||||||
|
|
||||||
app.config.update(
|
app.config.update(
|
||||||
DATASET_TITLE=title,
|
DATASET_TITLE=title,
|
||||||
CXG_API_BASE=api_base
|
CXG_API_BASE=api_base
|
||||||
@@ -97,7 +99,8 @@ def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
|||||||
"max_category_items": max_category_items,
|
"max_category_items": max_category_items,
|
||||||
"diffexp_lfc_cutoff": diffexp_lfc_cutoff,
|
"diffexp_lfc_cutoff": diffexp_lfc_cutoff,
|
||||||
"obs_names": obs_names,
|
"obs_names": obs_names,
|
||||||
"var_names": var_names
|
"var_names": var_names,
|
||||||
|
"nan_to_num": nan_to_num
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ numpy>=1.14.5
|
|||||||
pandas>=0.23.1
|
pandas>=0.23.1
|
||||||
scanpy>=1.3.2
|
scanpy>=1.3.2
|
||||||
scipy>=1.1.0
|
scipy>=1.1.0
|
||||||
scikit-learn==0.19.1
|
scikit-learn>=0.20.1
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
|||||||
class UtilTest(unittest.TestCase):
|
class UtilTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
args = {'layout': 'umap', 'diffexp': 'ttest', 'max_category_items': 100,
|
args = {'layout': 'umap', 'diffexp': 'ttest', 'max_category_items': 100,
|
||||||
'obs_names': None, 'var_names': None, 'diffexp_lfc_cutoff': 0.01}
|
'obs_names': None, 'var_names': None, 'diffexp_lfc_cutoff': 0.01,
|
||||||
|
'nan_to_num': True}
|
||||||
|
|
||||||
self.data = ScanpyEngine("example-dataset/pbmc3k.h5ad", args)
|
self.data = ScanpyEngine("example-dataset/pbmc3k.h5ad", args)
|
||||||
self.data._create_schema()
|
self.data._create_schema()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ with open("server/requirements.txt") as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="cellxgene",
|
name="cellxgene",
|
||||||
version="0.2.0",
|
version="0.3.0",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
url="https://github.com/chanzuckerberg/cellxgene",
|
url="https://github.com/chanzuckerberg/cellxgene",
|
||||||
license="MIT",
|
license="MIT",
|
||||||
|
|||||||
Reference in New Issue
Block a user