mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-26 11:08:12 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a297ea30e2 | ||
|
|
0ef6c36f4c | ||
|
|
aaa60bc303 | ||
|
|
7d40d89fd8 | ||
|
|
46ad346df1 | ||
|
|
83154577e4 | ||
|
|
a847951658 | ||
|
|
24af6efbcb | ||
|
|
126cac833a | ||
|
|
fc272dc42e | ||
|
|
d3a0d66139 | ||
|
|
9604231a2a | ||
|
|
6ea3b7f3cf | ||
|
|
0d0a32f272 | ||
|
|
48e0ea542b | ||
|
|
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.4.0
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
[bumpversion:file:setup.py]
|
||||||
search = version="{current_version}"
|
search = version="{current_version}"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
bin
|
||||||
|
client
|
||||||
|
dist
|
||||||
|
docs
|
||||||
|
server
|
||||||
+3
-2
@@ -11,10 +11,11 @@ install:
|
|||||||
- ./bin/build-client
|
- ./bin/build-client
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
- pip install -r server/requirements-dev.txt
|
- pip install -r server/requirements-dev.txt
|
||||||
|
- docker build .
|
||||||
script:
|
script:
|
||||||
- set -eo pipefail
|
- set -eo pipefail
|
||||||
- flake8 server/app/
|
- flake8 server
|
||||||
- flake8 server/cli/
|
- black --check
|
||||||
- npm run --prefix client/ build
|
- npm run --prefix client/ build
|
||||||
- npm run --prefix client/ test
|
- npm run --prefix client/ test
|
||||||
- pytest -s server/test
|
- pytest -s server/test
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
FROM ubuntu:bionic
|
||||||
|
|
||||||
|
ENV LC_ALL=C.UTF-8
|
||||||
|
ENV LANG=C.UTF-8
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev && \
|
||||||
|
pip3 install cellxgene
|
||||||
|
|
||||||
|
ENTRYPOINT ["cellxgene"]
|
||||||
@@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
`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
|
||||||
|
|
||||||
You'll need **python 3.6** and **Google Chrome**. The web UI is tested on OSX and Windows using Chrome, and the python CLI is tested on OSX and Ubuntu (via WSL/Windows). It should work on other platforms, but if you run into trouble let us know (see [help](#help-and-contact) below).
|
You'll need **python 3.6** and **Google Chrome**. (*Warning*: Python 3.7 is **not** supported at this time)
|
||||||
|
The web UI is tested on OSX and Windows using Chrome, and the python CLI is tested on OSX and Ubuntu (via WSL/Windows). It should work on other platforms, but if you run into trouble let us know (see [help](#help-and-contact) below).
|
||||||
|
|
||||||
|
|
||||||
To install run
|
To install run
|
||||||
|
|
||||||
@@ -27,15 +29,16 @@ 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.
|
||||||
|
|
||||||
There are several options available, such as:
|
There are several options available, such as:
|
||||||
|
|
||||||
- `--layout` to specify the layout as `tsne` or `umap`
|
- `--layout` to specify the layout as `tsne` or `umap`
|
||||||
- `--title` to show a title on the explorer
|
- `--title` to show a title on the explorer
|
||||||
- `--open` to automatically open the web browser after launching (OS X only)
|
- `--open` to automatically open the web browser after launching (OS X only)
|
||||||
|
|
||||||
@@ -56,11 +59,11 @@ The `launch` command assumes that the data is stored in the `.h5ad` format from
|
|||||||
- an `obs` field has a unique identifier for every cell (you can specify which field to use with the `--obs-names` option, by default it will use the value of `data.obs_names`)
|
- an `obs` field has a unique identifier for every cell (you can specify which field to use with the `--obs-names` option, by default it will use the value of `data.obs_names`)
|
||||||
- a `var` field has a unique identifier for every gene (you can specify which field to use with the `--var-names` option, by default it will use the value of `data.var_names`)
|
- a `var` field has a unique identifier for every gene (you can specify which field to use with the `--var-names` option, by default it will use the value of `data.var_names`)
|
||||||
- an `obsm` field contains the two-dimensional coordinates for the layout that you want to render (e.g. `X_tsne` for the `tsne` layout or `X_umap` for the `umap` layout)
|
- an `obsm` field contains the two-dimensional coordinates for the layout that you want to render (e.g. `X_tsne` for the `tsne` layout or `X_umap` for the `umap` layout)
|
||||||
- any additional `obs` fields will be rendered as per-cell continuous or categorical metadata by the app (e.g. `louvain` cluster assignments)
|
- any additional `obs` fields will be rendered as per-cell continuous or categorical metadata by the app (e.g. `louvain` cluster assignments)
|
||||||
|
|
||||||
### prepare
|
### prepare
|
||||||
|
|
||||||
The `prepare` command is included to help you format your data. It uses `scanpy` under the hood. This is especially useful if you are starting with raw unanalyzed data and are unfamiliar with `scanpy`.
|
The `prepare` command is included to help you format your data. It uses `scanpy` under the hood. This is especially useful if you are starting with raw unanalyzed data and are unfamiliar with `scanpy`.
|
||||||
|
|
||||||
To prepare from an existing `.h5ad` file use
|
To prepare from an existing `.h5ad` file use
|
||||||
|
|
||||||
@@ -68,7 +71,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
|
||||||
|
|
||||||
@@ -88,6 +91,12 @@ cellxgene prepare --help
|
|||||||
pip install cellxgene[louvain]
|
pip install cellxgene[louvain]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the aforementioned optional package installation fails, you can also install these packages directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install python-igraph louvain>=0.6
|
||||||
|
```
|
||||||
|
|
||||||
## conda and virtual environments
|
## conda and virtual environments
|
||||||
|
|
||||||
If you use conda and want to create a conda environment for `cellxgene` you can use the following commands
|
If you use conda and want to create a conda environment for `cellxgene` you can use the following commands
|
||||||
@@ -102,13 +111,28 @@ Or you can create a virtual environment by using
|
|||||||
|
|
||||||
```
|
```
|
||||||
ENV_NAME=cellxgene
|
ENV_NAME=cellxgene
|
||||||
python3 -m venv ${ENV_NAME}
|
python3.6 -m venv ${ENV_NAME}
|
||||||
source ${ENV_NAME}/bin/activate
|
source ${ENV_NAME}/bin/activate
|
||||||
pip install cellxgene
|
pip install cellxgene
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## docker
|
||||||
|
|
||||||
|
We have included a dockerfile to conveniently run cellxgene from docker.
|
||||||
|
|
||||||
|
1. Build the image `docker build . -t cellxgene`
|
||||||
|
2. Run the container and mount data `docker run -v "$PWD/example-dataset/:/data/" -p 5005:5005 cellxgene launch --host 0.0.0.0 data/pbmc3k.h5ad`
|
||||||
|
* You will need to use --host 0.0.0.0 to have the container listen to incoming requests from the browser
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary> questions about data formatting </summary>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
> Someone sent me a directory of `10X-Genomics` data with a `mtx` file and I've never used `scanpy`, can I use `cellxgene`?
|
> Someone sent me a directory of `10X-Genomics` data with a `mtx` file and I've never used `scanpy`, can I use `cellxgene`?
|
||||||
|
|
||||||
Yep! This should only take a couple steps. We'll assume your data is in a folder called `data/` and you've successfully installed `cellxgene` with the `louvain` packages as described above. Just run
|
Yep! This should only take a couple steps. We'll assume your data is in a folder called `data/` and you've successfully installed `cellxgene` with the `louvain` packages as described above. Just run
|
||||||
@@ -125,14 +149,18 @@ cellxgene launch data-processed.h5ad --layout=umap --open
|
|||||||
|
|
||||||
And your web browser should open with an interactive view of your data.
|
And your web browser should open with an interactive view of your data.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
> In my `prepare` command I received the following error `Warning: louvain module is not installed, no clusters will be calculated. To fix this please install cellxgene with the optional feature louvain enabled`
|
> In my `prepare` command I received the following error `Warning: louvain module is not installed, no clusters will be calculated. To fix this please install cellxgene with the optional feature louvain enabled`
|
||||||
|
|
||||||
Louvain clustering requires additional dependencies that are somewhat complex, so we don't include them by default. For now, you need to specify that you want these packages by using
|
Louvain clustering requires additional dependencies that are somewhat complex, so we don't include them by default. For now, you need to specify that you want these packages by using
|
||||||
|
|
||||||
```
|
```
|
||||||
pip install cellxgene[louvain]
|
pip install cellxgene[louvain]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
> I ran `prepare` and I'm getting results that look unexpected
|
> I ran `prepare` and I'm getting results that look unexpected
|
||||||
|
|
||||||
You might want to try running one of the preprocessing recipes included with `scanpy` (read more about them [here](https://scanpy.readthedocs.io/en/latest/api/index.html#recipes)). You can specify this with the `--recipe` option, such as
|
You might want to try running one of the preprocessing recipes included with `scanpy` (read more about them [here](https://scanpy.readthedocs.io/en/latest/api/index.html#recipes)). You can specify this with the `--recipe` option, such as
|
||||||
@@ -143,22 +171,60 @@ cellxgene prepare data/ --output=data-processed.h5ad --recipe=zheng17
|
|||||||
|
|
||||||
It should be easy to run `prepare` then call `cellxgene launch` a few times with different settings to explore different behaviors. We may explore adding other preprocessing options in the future.
|
It should be easy to run `prepare` then call `cellxgene launch` a few times with different settings to explore different behaviors. We may explore adding other preprocessing options in the future.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
> I have extra metadata that I want to add to my dataset
|
> I have extra metadata that I want to add to my dataset
|
||||||
|
|
||||||
Currently this is not supported directly, but you should be able to do this manually using `scanpy`. For example, this [notebook](https://github.com/falexwolf/fun-analyses/blob/master/tabula_muris/tabula_muris.ipynb) shows adding the contents of a `csv` file with metadata to an `anndata` object. For now, you could do this manually on your data in the same way and then save out the result before loading into `cellxgene`.
|
Currently this is not supported directly, but you should be able to do this manually using `scanpy`. For example, this [notebook](https://github.com/falexwolf/fun-analyses/blob/master/tabula_muris/tabula_muris.ipynb) shows adding the contents of a `csv` file with metadata to an `anndata` object. For now, you could do this manually on your data in the same way and then save out the result before loading into `cellxgene`.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
> 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
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary> questions about installing and building </summary>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
> I tried to `pip install cellxgene` and got a weird error I don't understand
|
> I tried to `pip install cellxgene` and got a weird error I don't understand
|
||||||
|
|
||||||
This may happen, especially as we work out bugs in our installation process! Please create a new [Github issue](https://github.com/chanzuckerberg/cellxgene/issues), explain what you did, and include all the error messages you saw. It'd also be super helpful if you call `pip freeze` and include the full output alongside your issue.
|
This may happen, especially as we work out bugs in our installation process! Please create a new [Github issue](https://github.com/chanzuckerberg/cellxgene/issues), explain what you did, and include all the error messages you saw. It'd also be super helpful if you call `pip freeze` and include the full output alongside your issue.
|
||||||
|
|
||||||
> How are you computing and sorting differential expression results?
|
<hr>
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary> questions about algorithms </summary>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## developer guide
|
## developer guide
|
||||||
|
|
||||||
This project has made a few key design choices
|
This project has made a few key design choices
|
||||||
@@ -194,15 +260,15 @@ pip install -e .
|
|||||||
|
|
||||||
You can start the app while developing either by calling `cellxgene` or by calling `python -m server`. We recommend using the `--debug` flag to see more output, which you can include when reporting bugs.
|
You can start the app while developing either by calling `cellxgene` or by calling `python -m server`. We recommend using the `--debug` flag to see more output, which you can include when reporting bugs.
|
||||||
|
|
||||||
If you have any questions about developing or contributing, come hang out with us by joining the [CZI Science Slack](https://cziscience.slack.com/messages/CCTA8DF1T) and posting in the `#cellxgene-dev` channel.
|
If you have any questions about developing or contributing, come hang out with us by joining the [CZI Science Slack](https://join-cellxgene-users.herokuapp.com/) and posting in the `#cellxgene-dev` channel.
|
||||||
|
|
||||||
## development roadmap
|
## development roadmap
|
||||||
|
|
||||||
`cellxgene` is still very much in development, and we've love to include the community as we plan new features to work on. We are thinking about working on the following features over the next 3-12 months. If you are interested in updates, want to give feedback, want to contribute, or have ideas about other features we should work on, please [contact us](#help-and-contact)
|
`cellxgene` is still very much in development, and we've love to include the community as we plan new features to work on. We are thinking about working on the following features over the next 3-12 months. If you are interested in updates, want to give feedback, want to contribute, or have ideas about other features we should work on, please [contact us](#help-and-contact)
|
||||||
|
|
||||||
- **Visualizaling spatial metadata** Image-based transcriptomics methods also generate large cell by gene matrices, alongside rich metadata about spatial location; we would like to render this information in `cellxgene`
|
- **Visualizaling spatial metadata** Image-based transcriptomics methods also generate large cell by gene matrices, alongside rich metadata about spatial location; we would like to render this information in `cellxgene`
|
||||||
- **Visualizing trajectories** Trajectory analyses infer progression along some ordering or pseudotime; we would like `cellxgene ` to render the results of these analyses when they have been performed
|
- **Visualizing trajectories** Trajectory analyses infer progression along some ordering or pseudotime; we would like `cellxgene` to render the results of these analyses when they have been performed
|
||||||
- **Deploy to web** Many projects release public data browser websites alongside their publicatons; we would like to make it easy for anyone to deploy `cellxgene` to a custom URL with their own dataset that they own and operate
|
- **Deploy to web** Many projects release public data browser websites alongside their publicatons; we would like to make it easy for anyone to deploy `cellxgene` to a custom URL with their own dataset that they own and operate
|
||||||
- **HCA Integration** The [Human Cell Atlas](https://humancellatlas.org) is generating a large corpus of single-cell expression data and will make it available through the Data Coordination Platform; we would like `cellxgene` to be one of several different portals for browsing these data
|
- **HCA Integration** The [Human Cell Atlas](https://humancellatlas.org) is generating a large corpus of single-cell expression data and will make it available through the Data Coordination Platform; we would like `cellxgene` to be one of several different portals for browsing these data
|
||||||
|
|
||||||
## contributing
|
## contributing
|
||||||
@@ -215,13 +281,13 @@ We've been heavily inspired by several other related single-cell visualization p
|
|||||||
|
|
||||||
We were inspired by Mike Bostock and the [crossfilter](https://github.com/crossfilter) team for the design of our filtering implementation.
|
We were inspired by Mike Bostock and the [crossfilter](https://github.com/crossfilter) team for the design of our filtering implementation.
|
||||||
|
|
||||||
We have been working closely with the [`scanpy`](https://github.com/theislab/scanpy) team to integrate with their awesome analysis tools. Special thanks to Alex Wolf, Fabian Theis, and the rest of the team for their help during development and for providing an example dataset.
|
We have been working closely with the [`scanpy`](https://github.com/theislab/scanpy) team to integrate with their awesome analysis tools. Special thanks to Alex Wolf, Fabian Theis, and the rest of the team for their help during development and for providing an example dataset.
|
||||||
|
|
||||||
We are eager to explore integrations with other computational backends such as [`Seurat`](https://github.com/satijalab/seurat) or [`Bioconductor`](https://github.com/Bioconductor)
|
We are eager to explore integrations with other computational backends such as [`Seurat`](https://github.com/satijalab/seurat) or [`Bioconductor`](https://github.com/Bioconductor)
|
||||||
|
|
||||||
## help and contact
|
## help and contact
|
||||||
|
|
||||||
Have questions, suggestions, or comments? You can come hang out with us by joining the [CZI Science Slack](https://cziscience.slack.com/messages/CCTA8DF1T) and posting in the `#cellxgene-users` channel. As mentioned above, please submit any feature requests or bugs as [Github issues](https://github.com/chanzuckerberg/cellxgene/issues). We'd love to hear from you!
|
Have questions, suggestions, or comments? You can come hang out with us by joining the [CZI Science Slack](https://join-cellxgene-users.herokuapp.com/) and posting in the `#cellxgene-users` channel. As mentioned above, please submit any feature requests or bugs as [Github issues](https://github.com/chanzuckerberg/cellxgene/issues). We'd love to hear from you!
|
||||||
|
|
||||||
## reuse
|
## reuse
|
||||||
|
|
||||||
|
|||||||
@@ -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
+48
-36
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cellxgene",
|
"name": "cellxgene",
|
||||||
"version": "0.1.0",
|
"version": "0.4.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": {
|
||||||
@@ -5202,11 +5202,22 @@
|
|||||||
"randomatic": "^3.0.0",
|
"randomatic": "^3.0.0",
|
||||||
"repeat-element": "^1.1.2",
|
"repeat-element": "^1.1.2",
|
||||||
"repeat-string": "^1.5.2"
|
"repeat-string": "^1.5.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"is-number": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"kind-of": "^3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
||||||
@@ -5922,6 +5933,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 +5952,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 +6339,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 +6415,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 +6638,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": {
|
||||||
@@ -6752,13 +6768,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"is-number": {
|
"is-number": {
|
||||||
"version": "2.1.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"kind-of": "^3.0.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"is-obj": {
|
"is-obj": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -7629,7 +7641,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 +7731,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 +8055,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 +8180,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 +8225,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 +9951,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 +10659,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 +11681,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 +11868,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 +12322,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 +12558,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 +13214,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 +13877,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 +13985,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": {
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cellxgene",
|
"name": "cellxgene",
|
||||||
"version": "0.2.0",
|
"version": "0.4.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,8 +34,10 @@
|
|||||||
"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",
|
||||||
|
"is-number": "^7.0.0",
|
||||||
"key-pressed": "0.0.1",
|
"key-pressed": "0.0.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"memoize-one": "^4.0.0",
|
"memoize-one": "^4.0.0",
|
||||||
|
|||||||
@@ -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 sortedCategoryValues 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,16 @@ 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 = sortedCategoryValues([...cat.categoryIndices]);
|
||||||
|
return _.map(optTuples, (tuple, i) => (
|
||||||
<Value
|
<Value
|
||||||
key={v}
|
optTuples={optTuples}
|
||||||
|
key={tuple[1]}
|
||||||
metadataField={metadataField}
|
metadataField={metadataField}
|
||||||
count={values[v]}
|
categoryIndex={tuple[1]}
|
||||||
value={v}
|
|
||||||
i={i}
|
i={i}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
@@ -108,12 +100,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
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// jshint esversion: 6
|
||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
class Occupancy extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
occupancy,
|
||||||
|
colorScale,
|
||||||
|
categoricalSelectionState,
|
||||||
|
colorAccessor,
|
||||||
|
schema
|
||||||
|
} = this.props;
|
||||||
|
const width = 100;
|
||||||
|
const height = 11;
|
||||||
|
|
||||||
|
const categories = _.filter(schema.annotations.obs, {
|
||||||
|
name: colorAccessor
|
||||||
|
})[0].categories;
|
||||||
|
|
||||||
|
const x = d3
|
||||||
|
.scaleLinear()
|
||||||
|
/* get all the keys d[1] as an array, then find the sum */
|
||||||
|
.domain([0, d3.sum(Array.from(occupancy, d => d[1]))])
|
||||||
|
.range([0, width]);
|
||||||
|
|
||||||
|
let currentOffset = 0;
|
||||||
|
|
||||||
|
const stacks = categoricalSelectionState[colorAccessor].categoryValues.map(
|
||||||
|
d => {
|
||||||
|
const o = occupancy.get(d);
|
||||||
|
|
||||||
|
const scaledValue = x(o);
|
||||||
|
|
||||||
|
const stackItem = {
|
||||||
|
key: d,
|
||||||
|
value: o || 0,
|
||||||
|
rectWidth: o ? scaledValue : 0,
|
||||||
|
offset: currentOffset,
|
||||||
|
fill: o ? colorScale(categories.indexOf(d)) : "rgb(255,255,255)"
|
||||||
|
};
|
||||||
|
currentOffset += o ? scaledValue : 0;
|
||||||
|
return stackItem;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
marginRight: 5,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stacks.map(d => (
|
||||||
|
<rect
|
||||||
|
key={d.key}
|
||||||
|
width={d.rectWidth}
|
||||||
|
height={height}
|
||||||
|
x={d.offset}
|
||||||
|
title={d.metadataField}
|
||||||
|
fill={d.fill}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Occupancy;
|
||||||
@@ -1,7 +1,35 @@
|
|||||||
// jshint esversion: 6
|
// jshint esversion: 6
|
||||||
export default values =>
|
|
||||||
Object.keys(values).sort((a, b) => {
|
// values is [ [optVal, optIdx], ...]
|
||||||
const textA = a.toUpperCase();
|
// index is range array
|
||||||
const textB = b.toUpperCase();
|
// return sorted index
|
||||||
|
|
||||||
|
import isNumber from "is-number";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
const sortedCategoryValues = values => {
|
||||||
|
/* this sort could be memoized for perf */
|
||||||
|
|
||||||
|
const strings = [];
|
||||||
|
const ints = [];
|
||||||
|
|
||||||
|
_.forEach(values, v => {
|
||||||
|
if (isNumber(v[0])) {
|
||||||
|
ints.push(v);
|
||||||
|
} else {
|
||||||
|
strings.push(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
strings.sort((a, b) => {
|
||||||
|
const textA = String(a[0]).toUpperCase();
|
||||||
|
const textB = String(b[0]).toUpperCase();
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ints.sort((a, b) => +a[0] - +b[0]);
|
||||||
|
|
||||||
|
return ints.concat(strings);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sortedCategoryValues;
|
||||||
|
|||||||
@@ -2,57 +2,77 @@
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import Occupancy from "./occupancy";
|
||||||
|
import { countCategoryValues2D } from "../../util/stateManager/worldUtil";
|
||||||
|
import * as globals from "../../globals";
|
||||||
|
|
||||||
@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),
|
||||||
|
world: state.controls.world
|
||||||
}))
|
}))
|
||||||
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,
|
||||||
|
world
|
||||||
} = 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 isColorBy = metadataField === colorAccessor;
|
||||||
let categories = null;
|
let categories = null;
|
||||||
|
let occupancy = null;
|
||||||
|
|
||||||
if (c && schema) {
|
if (isColorBy && schema) {
|
||||||
categories = _.filter(schema.annotations.obs, {
|
categories = _.filter(schema.annotations.obs, {
|
||||||
name: colorAccessor
|
name: colorAccessor
|
||||||
})[0].categories;
|
})[0].categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (colorAccessor && !isColorBy) {
|
||||||
|
occupancy = countCategoryValues2D(
|
||||||
|
metadataField,
|
||||||
|
colorAccessor,
|
||||||
|
world.obsAnnotations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@@ -66,7 +86,10 @@ class CategoryValue extends React.Component {
|
|||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
userSelect: "none"
|
userSelect: "none",
|
||||||
|
width: globals.leftSidebarWidth - 130,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="bp3-control bp3-checkbox">
|
<label className="bp3-control bp3-checkbox">
|
||||||
@@ -78,8 +101,20 @@ class CategoryValue extends React.Component {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
<span className="bp3-control-indicator" />
|
<span className="bp3-control-indicator" />
|
||||||
{value}
|
{displayString}
|
||||||
</label>
|
</label>
|
||||||
|
<span style={{ flexShrink: 0 }}>
|
||||||
|
{colorAccessor &&
|
||||||
|
!isColorBy &&
|
||||||
|
categoricalSelectionState[colorAccessor] ? (
|
||||||
|
<Occupancy
|
||||||
|
occupancy={occupancy.get(
|
||||||
|
category.categoryValues[categoryIndex]
|
||||||
|
)}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
<span>{count}</span>
|
<span>{count}</span>
|
||||||
@@ -89,7 +124,7 @@ class CategoryValue extends React.Component {
|
|||||||
width: 11,
|
width: 11,
|
||||||
height: 11,
|
height: 11,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
c && categories
|
isColorBy && categories
|
||||||
? colorScale(categories.indexOf(value))
|
? colorScale(categories.indexOf(value))
|
||||||
: "inherit"
|
: "inherit"
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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,27 +108,27 @@ 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 => {
|
||||||
value={gene}
|
/* this happens on 'enter' */
|
||||||
type="text"
|
this.handleClick(g);
|
||||||
className="bp3-input"
|
}}
|
||||||
placeholder="Enter a gene name"
|
inputValueRenderer={g => {
|
||||||
style={{ paddingRight: 94 }}
|
return "";
|
||||||
/>
|
}}
|
||||||
</div>
|
itemListPredicate={filterGenes}
|
||||||
<Tooltip
|
itemRenderer={renderGene.bind(this)}
|
||||||
content="Add a gene to see its expression levels"
|
items={
|
||||||
position="bottom"
|
world && world.varAnnotations
|
||||||
>
|
? world.varAnnotations
|
||||||
<Button intent="primary" onClick={this.handleClick.bind(this)}>
|
: [{ name: "No genes", n_counts: "" }]
|
||||||
Add
|
}
|
||||||
</Button>
|
popoverProps={{ minimal: true }}
|
||||||
</Tooltip>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{world && userDefinedGenes.length > 0
|
{world && userDefinedGenes.length > 0
|
||||||
? _.map(userDefinedGenes, (geneName, index) => {
|
? _.map(userDefinedGenes, (geneName, index) => {
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
theme: jekyll-theme-architect
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# cellxgene
|
||||||
|
|
||||||
|
cellxgene is an interactive data explorer for single-cell transcriptomics data designed to handle large datasets (1 million cells or more) and integrate with your favorite analysis tools
|
||||||
|
|
||||||
|
## getting started
|
||||||
|
|
||||||
|
install the package
|
||||||
|
> `> pip install cellxgene`
|
||||||
|
|
||||||
|
preprocess the data for use with cellxgene (optional)
|
||||||
|
> `> cellxgene --prepare dataset.h5ad -o processed.h5ad`
|
||||||
|
|
||||||
|
launch the web app
|
||||||
|
> `> cellxgene --launch processed.h5ad`
|
||||||
|
|
||||||
|
## features
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### inspiration and collaboration
|
||||||
|
|
||||||
|
We've been heavily inspired by several other related single-cell visualization projects:
|
||||||
|
* [UCSC Cell Browswer](http://cells.ucsc.edu/)
|
||||||
|
* [Cytoscape](http://www.cytoscape.org/)
|
||||||
|
* [Xena](https://xena.ucsc.edu/)
|
||||||
|
* [ASAP](https://asap.epfl.ch/)
|
||||||
|
* [Gene Pattern](http://genepattern-notebook.org/)
|
||||||
|
|
||||||
|
We were inspired by Mike Bostock and the [crossfilter](https://github.com/crossfilter) team for the design of our filtering implementation.
|
||||||
|
|
||||||
|
We have been working closely with the [`scanpy`](https://github.com/theislab/scanpy) team to integrate with their awesome analysis tools. Special thanks to Alex Wolf, Fabian Theis, and the rest of the team for their help during development and for providing an example dataset.
|
||||||
|
|
||||||
|
We are eager to explore integrations with other computational backends such as [`Seurat`](https://github.com/satijalab/seurat) or [`Bioconductor`](https://github.com/Bioconductor)
|
||||||
|
|
||||||
|
### help and contact
|
||||||
|
|
||||||
|
Have questions, suggestions, or comments? You can come hang out with us by joining the [CZI Science Slack](https://join-cziscience-slack.herokuapp.com/) and posting in the `#cellxgene-users` channel. As mentioned above, please submit any feature requests or bugs as [Github issues](https://github.com/chanzuckerberg/cellxgene/issues). We'd love to hear from you!
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
## Creating PR
|
||||||
|
1. Name [username]/branchname
|
||||||
|
1. Branch name should be all lowercase
|
||||||
|
2. Words separated by “-”
|
||||||
|
2. Code should address only one issue ideally, make a separate PR for each task
|
||||||
|
3. Description
|
||||||
|
1. Clear explanation of issues solved
|
||||||
|
2. Describe why and how, when appropriate
|
||||||
|
3. Call out specific areas you want extra attention in review (optional)
|
||||||
|
4. If your PR requires more than one reviewer tag those people in the description or comments and let them know that you specifically require them
|
||||||
|
4. Ensure that the PR updates tests and documentation and adds tests where appropriate
|
||||||
|
5. Use github’s issue keywords when PR is addressing an issue https://help.github.com/articles/closing-issues-using-keywords/
|
||||||
|
6. Tags (add at beginning of title)
|
||||||
|
1. [EASY] - small non-controversial change, easy to review
|
||||||
|
2. [DO NOT MERGE] - PR is in progress, do not merge changes
|
||||||
|
|
||||||
|
## Review
|
||||||
|
1. Assign at least one reviewer to submitted PRs. Reviewers should be selected based on expertise in areas affected by the PR (eg, web UI: Colin), and should include Comp Bio and PM as needed.
|
||||||
|
2. Reviewers should approve or request changes (not just comment) and put general and line level comments where appropriate
|
||||||
|
3. As a PR submitter respond to all comments (eg, comment, commit a change, etc)
|
||||||
|
4. External PRs
|
||||||
|
1. For external PRs or PRs not from our core team, core team should assign a reviewer and make initial contact within 1 business day
|
||||||
|
2. Build code on local environment and run smoke tests
|
||||||
|
|
||||||
|
## Required to Merge
|
||||||
|
1. Travis CI Build passing
|
||||||
|
2. At least one reviewer approved
|
||||||
|
1. Exceptions:
|
||||||
|
1. Release PRs where version is just bumped should not need review
|
||||||
|
2. Complex PRs which touch multiple parts of the codebase should have reviews from all relevant parties
|
||||||
|
3. License and Security checks (SNYK) passing. If their server is down and you didn’t add any new external npm or python packages, merge is OK
|
||||||
|
|
||||||
|
## Merging
|
||||||
|
1. Use "squash and merge" option when merging
|
||||||
|
2. If you resolved conflicts, wait until the build passes to merge
|
||||||
+23
-17
@@ -14,41 +14,47 @@ The release process should result in the following side-effects:
|
|||||||
- Tagged github release
|
- Tagged github release
|
||||||
- Publication to PyPi
|
- Publication to PyPi
|
||||||
|
|
||||||
## Process
|
## Recipe
|
||||||
|
|
||||||
Follow these steps to create a release.
|
Follow these steps to create a release.
|
||||||
|
|
||||||
1. Preparation:
|
1. Preparation:
|
||||||
- Define the release version number, using [semantic versioning](https://semver.org/)
|
- python3.6 environment, and a cellxgene clone
|
||||||
- Write the release title and release notes
|
- install required tools: `pip install -r requirements-dev.txt`
|
||||||
|
- Define the release version number, using [semantic versioning](https://semver.org/),
|
||||||
|
and specifying all three digits (eg, 0.3.0)
|
||||||
|
- 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`
|
where you choose major/minor/patch depending on which part of the version
|
||||||
- build the JS asserts using `bin/build-client`
|
is being bumped (eg, 0.2.9->0.3 is minor).
|
||||||
|
- Clean up existing environment using `bin/clean`
|
||||||
|
- 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`
|
||||||
(assumes you have `setuptools` and `twine` installed and that you have
|
and `twine` installed, that you have registered for pypi, and that you have
|
||||||
registered for pypi and have write access to the cellxgene pypi package)
|
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
|
||||||
|
|||||||
+5
-2
@@ -2,11 +2,14 @@
|
|||||||
if __package__ is None:
|
if __package__ is None:
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
PKG_PATH = Path(__file__).parent
|
PKG_PATH = Path(__file__).parent
|
||||||
sys.path.insert(0, str(PKG_PATH.parent))
|
sys.path.insert(0, str(PKG_PATH.parent))
|
||||||
import server
|
import server # noqa F401
|
||||||
|
|
||||||
__package__ = PKG_PATH.name
|
__package__ = PKG_PATH.name
|
||||||
|
|
||||||
# Main thing
|
# Main thing
|
||||||
from .cli.cli import cli
|
from .cli.cli import cli # noqa F402
|
||||||
|
|
||||||
cli()
|
cli()
|
||||||
|
|||||||
+10
-6
@@ -14,16 +14,14 @@ REACTIVE_LIMIT = 1_000_000
|
|||||||
|
|
||||||
app = Flask(__name__, static_folder="web/static")
|
app = Flask(__name__, static_folder="web/static")
|
||||||
app.json_encoder = Float32JSONEncoder
|
app.json_encoder = Float32JSONEncoder
|
||||||
cache = Cache(app, config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 860000})
|
cache = Cache(app, config={"CACHE_TYPE": "simple", "CACHE_DEFAULT_TIMEOUT": 860_000})
|
||||||
Compress(app)
|
Compress(app)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
SECRET_KEY = os.environ.get("CXG_SECRET_KEY", default="SparkleAndShine")
|
SECRET_KEY = os.environ.get("CXG_SECRET_KEY", default="SparkleAndShine")
|
||||||
|
|
||||||
app.config.update(
|
app.config.update(SECRET_KEY=SECRET_KEY)
|
||||||
SECRET_KEY=SECRET_KEY,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Application Data
|
# Application Data
|
||||||
data = None
|
data = None
|
||||||
@@ -36,7 +34,13 @@ docs.append(resources.get_swagger_doc())
|
|||||||
app.register_blueprint(webapp.bp)
|
app.register_blueprint(webapp.bp)
|
||||||
app.register_blueprint(resources.blueprint)
|
app.register_blueprint(resources.blueprint)
|
||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
get_swagger_blueprint(docs, "/api/swagger", produces=["application/json"], title="cellxgene rest api",
|
get_swagger_blueprint(
|
||||||
description="An API connecting ExpressionMatrix2 clustering algorithm to cellxgene"))
|
docs,
|
||||||
|
"/api/swagger",
|
||||||
|
produces=["application/json"],
|
||||||
|
title="cellxgene rest api",
|
||||||
|
description="An API connecting ExpressionMatrix2 clustering algorithm to cellxgene",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
app.add_url_rule("/", endpoint="index")
|
app.add_url_rule("/", endpoint="index")
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Sort order for methods
|
|||||||
|
|
||||||
|
|
||||||
class CXGDriver(metaclass=ABCMeta):
|
class CXGDriver(metaclass=ABCMeta):
|
||||||
|
|
||||||
def __init__(self, data, args):
|
def __init__(self, data, args):
|
||||||
self.data = self._load_data(data)
|
self.data = self._load_data(data)
|
||||||
self.layout_method = args["layout"]
|
self.layout_method = args["layout"]
|
||||||
@@ -24,11 +23,8 @@ class CXGDriver(metaclass=ABCMeta):
|
|||||||
def features(self):
|
def features(self):
|
||||||
features = {
|
features = {
|
||||||
"cluster": {"available": False},
|
"cluster": {"available": False},
|
||||||
"layout": {
|
"layout": {"obs": {"available": False}, "var": {"available": False}},
|
||||||
"obs": {"available": False},
|
"diffexp": {"available": False},
|
||||||
"var": {"available": False},
|
|
||||||
},
|
|
||||||
"diffexp": {"available": False}
|
|
||||||
}
|
}
|
||||||
# TODO - Interactive limit should be generated from the actual available methods see GH issue #94
|
# TODO - Interactive limit should be generated from the actual available methods see GH issue #94
|
||||||
if self.layout_method:
|
if self.layout_method:
|
||||||
|
|||||||
+381
-433
@@ -1,13 +1,12 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
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
|
||||||
@@ -22,83 +21,78 @@ Sort order for routes
|
|||||||
|
|
||||||
|
|
||||||
class SchemaAPI(Resource):
|
class SchemaAPI(Resource):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "get schema for dataframe and annotations",
|
{
|
||||||
"tags": ["initialize"],
|
"summary": "get schema for dataframe and annotations",
|
||||||
"parameters": [],
|
"tags": ["initialize"],
|
||||||
"responses": {
|
"parameters": [],
|
||||||
"200": {
|
"responses": {
|
||||||
"description": "schema",
|
"200": {
|
||||||
"examples": {
|
"description": "schema",
|
||||||
"application/json": {
|
"examples": {
|
||||||
"schema": {
|
"application/json": {
|
||||||
"dataframe": {
|
"schema": {
|
||||||
"nObs": 383,
|
"dataframe": {"nObs": 383, "nVar": 19944, "type": "float32"},
|
||||||
"nVar": 19944,
|
"annotations": {
|
||||||
"type": "float32"
|
"obs": [
|
||||||
},
|
{"name": "name", "type": "string"},
|
||||||
"annotations": {
|
{"name": "tissue_type", "type": "string"},
|
||||||
"obs": [
|
{"name": "num_reads", "type": "int32"},
|
||||||
{"name": "name", "type": "string"},
|
{"name": "sample_name", "type": "string"},
|
||||||
{"name": "tissue_type", "type": "string"},
|
{
|
||||||
{"name": "num_reads", "type": "int32"},
|
"name": "clusters",
|
||||||
{"name": "sample_name", "type": "string"},
|
"type": "categorical",
|
||||||
{
|
"categories": [99, 1, "unknown cluster"],
|
||||||
"name": "clusters",
|
},
|
||||||
"type": "categorical",
|
{"name": "QScore", "type": "float32"},
|
||||||
"categories": [99, 1, "unknown cluster"]
|
],
|
||||||
},
|
"var": [{"name": "name", "type": "string"}, {"name": "gene", "type": "string"}],
|
||||||
{"name": "QScore", "type": "float32"}
|
},
|
||||||
],
|
|
||||||
"var": [
|
|
||||||
{"name": "name", "type": "string"},
|
|
||||||
{"name": "gene", "type": "string"}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
})
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return make_response(jsonify({"schema": current_app.data.schema}), HTTPStatus.OK)
|
return make_response(jsonify({"schema": current_app.data.schema}), HTTPStatus.OK)
|
||||||
|
|
||||||
|
|
||||||
class ConfigAPI(Resource):
|
class ConfigAPI(Resource):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "Configuration information to assist in front-end adaptation"
|
{
|
||||||
" to underlying engine, available functionality, interactive time limits, etc",
|
"summary": "Configuration information to assist in front-end adaptation"
|
||||||
"tags": ["initialize"],
|
" to underlying engine, available functionality, interactive time limits, etc",
|
||||||
"parameters": [],
|
"tags": ["initialize"],
|
||||||
"responses": {
|
"parameters": [],
|
||||||
"200": {
|
"responses": {
|
||||||
"description": "schema",
|
"200": {
|
||||||
"examples": {
|
"description": "schema",
|
||||||
"application/json": {
|
"examples": {
|
||||||
"config": {
|
"application/json": {
|
||||||
"features": [
|
"config": {
|
||||||
{"method": "POST", "path": "/cluster/", "available": False},
|
"features": [
|
||||||
{
|
{"method": "POST", "path": "/cluster/", "available": False},
|
||||||
"method": "POST",
|
{
|
||||||
"path": "/layout/obs",
|
"method": "POST",
|
||||||
"available": True,
|
"path": "/layout/obs",
|
||||||
"interactiveLimit": 10000
|
"available": True,
|
||||||
|
"interactiveLimit": 10000,
|
||||||
|
},
|
||||||
|
{"method": "POST", "path": "/layout/var", "available": False},
|
||||||
|
],
|
||||||
|
"displayNames": {
|
||||||
|
"engine": "ScanPy version 1.33",
|
||||||
|
"dataset": "/home/joe/mouse/blorth.csv",
|
||||||
},
|
},
|
||||||
{"method": "POST", "path": "/layout/var", "available": False}
|
}
|
||||||
|
|
||||||
],
|
|
||||||
"displayNames": {
|
|
||||||
"engine": "ScanPy version 1.33",
|
|
||||||
"dataset": "/home/joe/mouse/blorth.csv"
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
config = {
|
config = {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -110,99 +104,95 @@ class ConfigAPI(Resource):
|
|||||||
],
|
],
|
||||||
"displayNames": {
|
"displayNames": {
|
||||||
"engine": f"cellxgene Scanpy engine version {pkg_resources.get_distribution('cellxgene').version}",
|
"engine": f"cellxgene Scanpy engine version {pkg_resources.get_distribution('cellxgene').version}",
|
||||||
"dataset": current_app.config["DATASET_TITLE"]
|
"dataset": current_app.config["DATASET_TITLE"],
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {"max_category_items": current_app.data.max_category_items},
|
||||||
"max_category_items": current_app.data.max_category_items
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return make_response(jsonify(config), HTTPStatus.OK)
|
return make_response(jsonify(config), HTTPStatus.OK)
|
||||||
|
|
||||||
|
|
||||||
class AnnotationsObsAPI(Resource):
|
class AnnotationsObsAPI(Resource):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "Fetch annotations (metadata) for all observations.",
|
{
|
||||||
"tags": ["annotations"],
|
"summary": "Fetch annotations (metadata) for all observations.",
|
||||||
"parameters": [{
|
"tags": ["annotations"],
|
||||||
"in": "query",
|
"parameters": [
|
||||||
"name": "annotation-name",
|
{
|
||||||
"type": "string",
|
"in": "query",
|
||||||
"description": "list of 1 or more annotation names"
|
"name": "annotation-name",
|
||||||
}],
|
"type": "string",
|
||||||
"responses": {
|
"description": "list of 1 or more annotation names",
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": [
|
|
||||||
"tissue_type", "sex", "num_reads", "clusters"
|
|
||||||
],
|
|
||||||
"data": [
|
|
||||||
[0, "lung", "F", 39844, 99],
|
|
||||||
[1, "heart", "M", 83, 1],
|
|
||||||
[49, "spleen", None, 2, "unknown cluster"],
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "annotations",
|
||||||
|
"examples": {
|
||||||
|
"application/json": {
|
||||||
|
"names": ["tissue_type", "sex", "num_reads", "clusters"],
|
||||||
|
"data": [
|
||||||
|
[0, "lung", "F", 39844, 99],
|
||||||
|
[1, "heart", "M", 83, 1],
|
||||||
|
[49, "spleen", None, 2, "unknown cluster"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "one or more of the annotation-name identifiers were not associated with an "
|
||||||
|
"annotation name"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"400": {
|
|
||||||
"description": "one or more of the annotation-name identifiers were not associated with an "
|
|
||||||
"annotation name"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
fields = request.args.getlist("annotation-name", None)
|
fields = request.args.getlist("annotation-name", None)
|
||||||
try:
|
try:
|
||||||
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)
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
try:
|
||||||
|
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.",
|
{
|
||||||
"tags": ["annotations"],
|
"summary": "Fetch annotations (metadata) for filtered subset of observations.",
|
||||||
"parameters": [
|
"tags": ["annotations"],
|
||||||
{
|
"parameters": [
|
||||||
"in": "query",
|
{
|
||||||
"name": "annotation-name",
|
"in": "query",
|
||||||
"type": "string",
|
"name": "annotation-name",
|
||||||
"description": "list of 1 or more annotation names"
|
"type": "string",
|
||||||
|
"description": "list of 1 or more annotation names",
|
||||||
|
},
|
||||||
|
{"name": "filter", "description": "Complex Filter", "in": "body", "schema": FilterModel},
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "annotations",
|
||||||
|
"examples": {
|
||||||
|
"application/json": {
|
||||||
|
"names": ["tissue_type", "sex", "num_reads", "clusters"],
|
||||||
|
"data": [
|
||||||
|
[0, "lung", "F", 39844, 99],
|
||||||
|
[1, "heart", "M", 83, 1],
|
||||||
|
[49, "spleen", None, 2, "unknown cluster"],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "malformed filter or one or more of the annotation-name identifiers were"
|
||||||
|
"not associated with an annotation name"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "filter",
|
|
||||||
"description": "Complex Filter",
|
|
||||||
"in": "body",
|
|
||||||
"schema": FilterModel
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": [
|
|
||||||
"tissue_type", "sex", "num_reads", "clusters"
|
|
||||||
],
|
|
||||||
"data": [
|
|
||||||
[0, "lung", "F", 39844, 99],
|
|
||||||
[1, "heart", "M", 83, 1],
|
|
||||||
[49, "spleen", None, 2, "unknown cluster"],
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "malformed filter or one or more of the annotation-name identifiers were"
|
|
||||||
"not associated with an annotation name"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def put(self):
|
def put(self):
|
||||||
fields = request.args.getlist("annotation-name", None)
|
fields = request.args.getlist("annotation-name", None)
|
||||||
try:
|
try:
|
||||||
@@ -211,89 +201,87 @@ 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)
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
try:
|
||||||
|
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):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "Fetch annotations (metadata) for all variables.",
|
{
|
||||||
"tags": ["annotations"],
|
"summary": "Fetch annotations (metadata) for all variables.",
|
||||||
"parameters": [{
|
"tags": ["annotations"],
|
||||||
"in": "query",
|
"parameters": [
|
||||||
"name": "annotation-name",
|
{
|
||||||
"type": "string",
|
"in": "query",
|
||||||
"description": "list of 1 or more annotation names"
|
"name": "annotation-name",
|
||||||
}],
|
"type": "string",
|
||||||
"responses": {
|
"description": "list of 1 or more annotation names",
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": [
|
|
||||||
"name", "category"
|
|
||||||
],
|
|
||||||
"data": [
|
|
||||||
[0, "ATAD3C", 1],
|
|
||||||
[1, "RER1", None],
|
|
||||||
[49, "S100B", 6]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "annotations",
|
||||||
|
"examples": {
|
||||||
|
"application/json": {
|
||||||
|
"names": ["name", "category"],
|
||||||
|
"data": [[0, "ATAD3C", 1], [1, "RER1", None], [49, "S100B", 6]],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "one or more of the annotation-name identifiers were not associated with an"
|
||||||
|
" annotation name"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"400": {
|
|
||||||
"description": "one or more of the annotation-name identifiers were not associated with an"
|
|
||||||
" annotation name"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
fields = request.args.getlist("annotation-name", None)
|
fields = request.args.getlist("annotation-name", None)
|
||||||
try:
|
try:
|
||||||
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)
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
try:
|
||||||
|
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.",
|
{
|
||||||
"tags": ["annotations"],
|
"summary": "Fetch annotations (metadata) for filtered subset of variables.",
|
||||||
"parameters": [
|
"tags": ["annotations"],
|
||||||
{
|
"parameters": [
|
||||||
"in": "query",
|
{
|
||||||
"name": "annotation-name",
|
"in": "query",
|
||||||
"type": "string",
|
"name": "annotation-name",
|
||||||
"description": "list of 1 or more annotation names"
|
"type": "string",
|
||||||
|
"description": "list of 1 or more annotation names",
|
||||||
|
},
|
||||||
|
{"name": "filter", "description": "Complex Filter", "in": "body", "schema": FilterModel},
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "annotations",
|
||||||
|
"examples": {
|
||||||
|
"application/json": {
|
||||||
|
"names": ["name", "category"],
|
||||||
|
"data": [[0, "ATAD3C", 1], [1, "RER1", None], [49, "S100B", 6]],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "malformed filter or one or more of the annotation-name identifiers were"
|
||||||
|
"not associated with an annotation name"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "filter",
|
|
||||||
"description": "Complex Filter",
|
|
||||||
"in": "body",
|
|
||||||
"schema": FilterModel
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "annotations",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"names": [
|
|
||||||
"name", "category"
|
|
||||||
],
|
|
||||||
"data": [
|
|
||||||
[0, "ATAD3C", 1],
|
|
||||||
[1, "RER1", None],
|
|
||||||
[49, "S100B", 6]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "malformed filter or one or more of the annotation-name identifiers were"
|
|
||||||
"not associated with an annotation name"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def put(self):
|
def put(self):
|
||||||
fields = request.args.getlist("annotation-name", None)
|
fields = request.args.getlist("annotation-name", None)
|
||||||
try:
|
try:
|
||||||
@@ -302,99 +290,74 @@ 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)
|
||||||
return make_response(jsonify(annotation_response), HTTPStatus.OK)
|
try:
|
||||||
|
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):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
{
|
||||||
"tags": ["data"],
|
"summary": "Get data (expression values) from the dataframe.",
|
||||||
"parameters": [
|
"tags": ["data"],
|
||||||
{
|
"parameters": [
|
||||||
"in": "query",
|
{"in": "query", "name": "filter", "type": "string", "description": "axis:key:value"},
|
||||||
"name": "filter",
|
{"in": "query", "name": "accept-type", "type": "string", "description": "MIME type"},
|
||||||
"type": "string",
|
],
|
||||||
"description": "axis:key:value"
|
"responses": {
|
||||||
},
|
"200": {
|
||||||
{
|
"description": "expression",
|
||||||
"in": "query",
|
"examples": {"application/json": {"var": [0, 20000], "obs": [[1, 39483, 3902, 203, 0, 0, 28]]}},
|
||||||
"name": "accept-type",
|
},
|
||||||
"type": "string",
|
"400": {"description": "Malformed filter"},
|
||||||
"description": "MIME type"
|
"406": {"description": "Unacceptable MIME type"},
|
||||||
},
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"var": [0, 20000],
|
|
||||||
"obs": [
|
|
||||||
[1, 39483, 3902, 203, 0, 0, 28]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Malformed filter"
|
|
||||||
},
|
|
||||||
"406": {
|
|
||||||
"description": "Unacceptable MIME type"
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
accept_type = request.args.get("accept-type", None)
|
accept_type = request.args.get("accept-type", None)
|
||||||
# request.args is immutable
|
# request.args is immutable
|
||||||
args = request.args.copy()
|
args = request.args.copy()
|
||||||
args.pop("accept-type", None)
|
args.pop("accept-type", None)
|
||||||
try:
|
try:
|
||||||
filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations'])
|
filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema["annotations"])
|
||||||
except QueryStringError as e:
|
except QueryStringError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
# TODO support CSV
|
# TODO support CSV
|
||||||
try:
|
try:
|
||||||
# TODO store mime_type when more than one is supported
|
# TODO store mime_type when more than one is supported
|
||||||
get_mime_type(acceptable_types=["application/json"], query_param=accept_type,
|
get_mime_type(
|
||||||
header=request.accept_mimetypes)
|
acceptable_types=["application/json"], query_param=accept_type, header=request.accept_mimetypes
|
||||||
|
)
|
||||||
except MimeTypeError as e:
|
except MimeTypeError as e:
|
||||||
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
||||||
try:
|
try:
|
||||||
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.",
|
{
|
||||||
"tags": ["data"],
|
"summary": "Get data (expression values) from the dataframe.",
|
||||||
"parameters": [
|
"tags": ["data"],
|
||||||
{
|
"parameters": [{"name": "filter", "description": "Complex Filter", "in": "body", "schema": FilterModel}],
|
||||||
'name': 'filter',
|
"responses": {
|
||||||
'description': 'Complex Filter',
|
"200": {
|
||||||
'in': 'body',
|
"description": "expression",
|
||||||
'schema': FilterModel
|
"examples": {"application/json": {"var": [0, 20000], "obs": [[1, 39483, 3902, 203, 0, 0, 28]]}},
|
||||||
}
|
},
|
||||||
],
|
"400": {"description": "Malformed filter"},
|
||||||
"responses": {
|
"406": {"description": "Unacceptable MIME type"},
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"var": [0, 20000],
|
|
||||||
"obs": [
|
|
||||||
[1, 39483, 3902, 203, 0, 0, 28]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Malformed filter"
|
|
||||||
},
|
|
||||||
"406": {
|
|
||||||
"description": "Unacceptable MIME type"
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def put(self):
|
def put(self):
|
||||||
if not request.accept_mimetypes.best_match(["application/json", "text/csv"]):
|
if not request.accept_mimetypes.best_match(["application/json", "text/csv"]):
|
||||||
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
||||||
@@ -403,100 +366,75 @@ class DataObsAPI(Resource):
|
|||||||
except MimeTypeError as e:
|
except MimeTypeError as e:
|
||||||
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
||||||
try:
|
try:
|
||||||
return make_response((jsonify(current_app.data.data_frame(request.get_json()["filter"], axis=Axis.OBS))),
|
return make_response(
|
||||||
HTTPStatus.OK)
|
(jsonify(current_app.data.data_frame(request.get_json()["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)
|
||||||
|
|
||||||
|
|
||||||
class DataVarAPI(Resource):
|
class DataVarAPI(Resource):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "Get data (expression values) from the dataframe.",
|
{
|
||||||
"tags": ["data"],
|
"summary": "Get data (expression values) from the dataframe.",
|
||||||
"parameters": [
|
"tags": ["data"],
|
||||||
{
|
"parameters": [
|
||||||
"in": "query",
|
{"in": "query", "name": "filter", "type": "string", "description": "axis:key:value"},
|
||||||
"name": "filter",
|
{"in": "query", "name": "accept-type", "type": "string", "description": "MIME type"},
|
||||||
"type": "string",
|
],
|
||||||
"description": "axis:key:value"
|
"responses": {
|
||||||
},
|
"200": {
|
||||||
{
|
"description": "expression",
|
||||||
"in": "query",
|
"examples": {"application/json": {"obs": [0, 20000], "var": [[1, 39483, 3902, 203, 0, 0, 28]]}},
|
||||||
"name": "accept-type",
|
},
|
||||||
"type": "string",
|
"400": {"description": "Malformed filter"},
|
||||||
"description": "MIME type"
|
"406": {"description": "Unacceptable MIME type"},
|
||||||
},
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"obs": [0, 20000],
|
|
||||||
"var": [
|
|
||||||
[1, 39483, 3902, 203, 0, 0, 28]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Malformed filter"
|
|
||||||
},
|
|
||||||
"406": {
|
|
||||||
"description": "Unacceptable MIME type"
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
accept_type = request.args.get("accept-type", None)
|
accept_type = request.args.get("accept-type", None)
|
||||||
# request.args is immutable
|
# request.args is immutable
|
||||||
args = request.args.copy()
|
args = request.args.copy()
|
||||||
args.pop("accept-type", None)
|
args.pop("accept-type", None)
|
||||||
try:
|
try:
|
||||||
filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema['annotations'])
|
filter_ = parse_filter(ImmutableMultiDict(args), current_app.data.schema["annotations"])
|
||||||
except QueryStringError as e:
|
except QueryStringError as e:
|
||||||
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
return make_response(e.message, HTTPStatus.BAD_REQUEST)
|
||||||
try:
|
try:
|
||||||
get_mime_type(acceptable_types=["application/json"], query_param=accept_type,
|
get_mime_type(
|
||||||
header=request.accept_mimetypes)
|
acceptable_types=["application/json"], query_param=accept_type, header=request.accept_mimetypes
|
||||||
|
)
|
||||||
except MimeTypeError as e:
|
except MimeTypeError as e:
|
||||||
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
||||||
try:
|
try:
|
||||||
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.",
|
{
|
||||||
"tags": ["data"],
|
"summary": "Get data (expression values) from the dataframe.",
|
||||||
"parameters": [
|
"tags": ["data"],
|
||||||
{
|
"parameters": [{"name": "filter", "description": "Complex Filter", "in": "body", "schema": FilterModel}],
|
||||||
'name': 'filter',
|
"responses": {
|
||||||
'description': 'Complex Filter',
|
"200": {
|
||||||
'in': 'body',
|
"description": "expression",
|
||||||
'schema': FilterModel
|
"examples": {"application/json": {"obs": [0, 20000], "var": [[1, 39483, 3902, 203, 0, 0, 28]]}},
|
||||||
}
|
},
|
||||||
],
|
"400": {"description": "Malformed filter"},
|
||||||
"responses": {
|
"406": {"description": "Unacceptable MIME type"},
|
||||||
"200": {
|
|
||||||
"description": "expression",
|
|
||||||
"examples": {
|
|
||||||
"application/json": {
|
|
||||||
"obs": [0, 20000],
|
|
||||||
"var": [
|
|
||||||
[1, 39483, 3902, 203, 0, 0, 28]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Malformed filter"
|
|
||||||
},
|
|
||||||
"406": {
|
|
||||||
"description": "Unacceptable MIME type"
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def put(self):
|
def put(self):
|
||||||
if not request.accept_mimetypes.best_match(["application/json", "text/csv"]):
|
if not request.accept_mimetypes.best_match(["application/json", "text/csv"]):
|
||||||
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(f"Unsupported MIME type '{request.accept_mimetypes}'", HTTPStatus.NOT_ACCEPTABLE)
|
||||||
@@ -506,74 +444,76 @@ class DataVarAPI(Resource):
|
|||||||
except MimeTypeError as e:
|
except MimeTypeError as e:
|
||||||
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
return make_response(e.message, HTTPStatus.NOT_ACCEPTABLE)
|
||||||
try:
|
try:
|
||||||
return make_response((jsonify(current_app.data.data_frame(request.get_json()["filter"], axis=Axis.VAR))),
|
return make_response(
|
||||||
HTTPStatus.OK)
|
(jsonify(current_app.data.data_frame(request.get_json()["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)
|
||||||
|
|
||||||
|
|
||||||
class DiffExpObsAPI(Resource):
|
class DiffExpObsAPI(Resource):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "Generate differential expression (DE) statistics for two specified subsets of data, "
|
{
|
||||||
"as indicated by the two provided observation complex filters",
|
"summary": "Generate differential expression (DE) statistics for two specified subsets of data, "
|
||||||
"tags": ["diffexp"],
|
"as indicated by the two provided observation complex filters",
|
||||||
# TODO sort out params
|
"tags": ["diffexp"],
|
||||||
# "parameters": [
|
# TODO sort out params
|
||||||
# # {
|
# "parameters": [
|
||||||
# # "in": "body",
|
# # {
|
||||||
# # "name": "mode",
|
# # "in": "body",
|
||||||
# # "type": "string",
|
# # "name": "mode",
|
||||||
# # "required": True,
|
# # "type": "string",
|
||||||
# # "description": "topN or varFilter"
|
# # "required": True,
|
||||||
# # },
|
# # "description": "topN or varFilter"
|
||||||
# {
|
# # },
|
||||||
# "in": "query",
|
# {
|
||||||
# "name": "count",
|
# "in": "query",
|
||||||
# "type": "int32",
|
# "name": "count",
|
||||||
# "description": "TopN mode: how many vars to return"
|
# "type": "int32",
|
||||||
# },
|
# "description": "TopN mode: how many vars to return"
|
||||||
# {
|
# },
|
||||||
# "in": "body",
|
# {
|
||||||
# "name": "varFilter",
|
# "in": "body",
|
||||||
# "schema": FilterModel,
|
# "name": "varFilter",
|
||||||
# "description": "varFilter: Complex filter, only var for which vars to return"
|
# "schema": FilterModel,
|
||||||
# },
|
# "description": "varFilter: Complex filter, only var for which vars to return"
|
||||||
# {
|
# },
|
||||||
# "in": "body",
|
# {
|
||||||
# "name": "set1",
|
# "in": "body",
|
||||||
# "schema": FilterModel,
|
# "name": "set1",
|
||||||
# "required": True,
|
# "schema": FilterModel,
|
||||||
# "description": "Complex filter, only obs - observations in set1"
|
# "required": True,
|
||||||
# },
|
# "description": "Complex filter, only obs - observations in set1"
|
||||||
# {
|
# },
|
||||||
# "in": "body",
|
# {
|
||||||
# "name": "set2",
|
# "in": "body",
|
||||||
# "schema": FilterModel,
|
# "name": "set2",
|
||||||
# "description": "Complex filter, only obs - observations in set2. If not included, inverse of set1."
|
# "schema": FilterModel,
|
||||||
# },
|
# "description": "Complex filter, only obs - observations in set2.
|
||||||
# ],
|
# If not included, inverse of set1."
|
||||||
"responses": {
|
# },
|
||||||
"200": {
|
# ],
|
||||||
"description": "Statistics are encoded as an array of arrays, with fields ordered as: "
|
"responses": {
|
||||||
"varIndex, logfoldchange, pVal, pValAdj",
|
"200": {
|
||||||
"examples": {
|
"description": "Statistics are encoded as an array of arrays, with fields ordered as: "
|
||||||
"application/json": [
|
"varIndex, logfoldchange, pVal, pValAdj",
|
||||||
[328, -2.569489, 2.655706e-63, 3.642036e-57],
|
"examples": {
|
||||||
[1250, -2.569489, 2.655706e-63, 3.642036e-57],
|
"application/json": [
|
||||||
]
|
[328, -2.569_489, 2.655_706e-63, 3.642_036e-57],
|
||||||
}
|
[1250, -2.569_489, 2.655_706e-63, 3.642_036e-57],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"400": {"description": "malformed filter"},
|
||||||
|
"403": {"description": "non-interactive request"},
|
||||||
|
"501": {"description": "diffexp is not implemented"},
|
||||||
},
|
},
|
||||||
"400": {
|
|
||||||
"description": "malformed filter"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "non-interactive request"
|
|
||||||
},
|
|
||||||
"501": {
|
|
||||||
"description": "diffexp is not implemented"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def post(self):
|
def post(self):
|
||||||
args = request.get_json()
|
args = request.get_json()
|
||||||
# confirm mode is present and legal
|
# confirm mode is present and legal
|
||||||
@@ -608,46 +548,54 @@ class DiffExpObsAPI(Resource):
|
|||||||
# mode=topN
|
# mode=topN
|
||||||
count = args.get("count", None)
|
count = args.get("count", None)
|
||||||
try:
|
try:
|
||||||
diffexp = current_app.data.diffexp_topN(set1_filter, set2_filter, count,
|
diffexp = current_app.data.diffexp_topN(
|
||||||
current_app.data.features["diffexp"]["interactiveLimit"])
|
set1_filter, set2_filter, count, current_app.data.features["diffexp"]["interactiveLimit"]
|
||||||
|
)
|
||||||
except (ValueError, FilterError) as e:
|
except (ValueError, FilterError) as e:
|
||||||
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)
|
||||||
return make_response(jsonify(diffexp), HTTPStatus.OK)
|
try:
|
||||||
|
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):
|
||||||
@swagger.doc({
|
@swagger.doc(
|
||||||
"summary": "Get the default layout for all observations.",
|
{
|
||||||
"tags": ["layout"],
|
"summary": "Get the default layout for all observations.",
|
||||||
"parameters": [],
|
"tags": ["layout"],
|
||||||
"responses": {
|
"parameters": [],
|
||||||
"200": {
|
"responses": {
|
||||||
"description": "layout",
|
"200": {
|
||||||
"examples": {
|
"description": "layout",
|
||||||
"application/json": {
|
"examples": {
|
||||||
"layout": {
|
"application/json": {
|
||||||
"ndims": 2,
|
"layout": {
|
||||||
"coordinates": [
|
"ndims": 2,
|
||||||
[0, 0.284483, 0.983744],
|
"coordinates": [[0, 0.284_483, 0.983_744], [1, 0.038_844, 0.739_444]],
|
||||||
[1, 0.038844, 0.739444]
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
"400": {"description": "Data preparation error"},
|
||||||
},
|
},
|
||||||
"400": {
|
|
||||||
"description": "Data preparation error"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
try:
|
try:
|
||||||
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)
|
||||||
return make_response((jsonify({"layout": layout})), HTTPStatus.OK)
|
try:
|
||||||
|
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.",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy import sparse, stats
|
from scipy import sparse, stats
|
||||||
|
|
||||||
@@ -64,19 +63,19 @@ def diffexp_ttest(adata, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
|
|||||||
sum_vn = vnA + vnB
|
sum_vn = vnA + vnB
|
||||||
|
|
||||||
# degrees of freedom for Welch's t-test
|
# degrees of freedom for Welch's t-test
|
||||||
with np.errstate(divide='ignore', invalid='ignore'):
|
with np.errstate(divide="ignore", invalid="ignore"):
|
||||||
dof = sum_vn**2 / (vnA**2 / (nA - 1) + vnB**2 / (nB - 1))
|
dof = sum_vn ** 2 / (vnA ** 2 / (nA - 1) + vnB ** 2 / (nB - 1))
|
||||||
dof[np.isnan(dof)] = 1
|
dof[np.isnan(dof)] = 1
|
||||||
|
|
||||||
# Welch's t-test score calculation
|
# Welch's t-test score calculation
|
||||||
with np.errstate(divide='ignore', invalid='ignore'):
|
with np.errstate(divide="ignore", invalid="ignore"):
|
||||||
tscores = (meanA - meanB) / np.sqrt(sum_vn)
|
tscores = (meanA - meanB) / np.sqrt(sum_vn)
|
||||||
tscores[np.isnan(tscores)] = 0
|
tscores[np.isnan(tscores)] = 0
|
||||||
|
|
||||||
# p-value
|
# p-value
|
||||||
pvals = stats.t.sf(np.abs(tscores), dof) * 2
|
pvals = stats.t.sf(np.abs(tscores), dof) * 2
|
||||||
pvals_adj = pvals * adata._X.shape[1]
|
pvals_adj = pvals * adata._X.shape[1]
|
||||||
pvals_adj[pvals_adj > 1] = 1 # cap adjusted p-value at 1
|
pvals_adj[pvals_adj > 1] = 1 # cap adjusted p-value at 1
|
||||||
|
|
||||||
# logfoldchanges: log2(meanA / meanB)
|
# logfoldchanges: log2(meanA / meanB)
|
||||||
logfoldchanges = np.log2(np.abs((meanA + 1e-9) / (meanB + 1e-9)))
|
logfoldchanges = np.log2(np.abs((meanA + 1e-9) / (meanB + 1e-9)))
|
||||||
@@ -106,8 +105,5 @@ def diffexp_ttest(adata, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
|
|||||||
pvals_adj_top_n = pvals_adj[sort_order]
|
pvals_adj_top_n = pvals_adj[sort_order]
|
||||||
|
|
||||||
# varIndex, logfoldchange, pval, pval_adj
|
# varIndex, logfoldchange, pval, pval_adj
|
||||||
result = [[sort_order[i],
|
result = [[sort_order[i], logfoldchanges_top_n[i], pvals_top_n[i], pvals_adj_top_n[i]] for i in range(top_n)]
|
||||||
logfoldchanges_top_n[i],
|
|
||||||
pvals_top_n[i],
|
|
||||||
pvals_adj_top_n[i]] for i in range(top_n)]
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ Sort order for methods
|
|||||||
|
|
||||||
|
|
||||||
class ScanpyEngine(CXGDriver):
|
class ScanpyEngine(CXGDriver):
|
||||||
|
|
||||||
def __init__(self, data, args):
|
def __init__(self, data, args):
|
||||||
super().__init__(data, args)
|
super().__init__(data, args)
|
||||||
self._alias_annotation_names(Axis.OBS, args["obs_names"])
|
self._alias_annotation_names(Axis.OBS, args["obs_names"])
|
||||||
@@ -35,6 +34,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.
|
||||||
@@ -57,40 +60,54 @@ class ScanpyEngine(CXGDriver):
|
|||||||
if name not in df_axis.columns:
|
if name not in df_axis.columns:
|
||||||
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.")
|
||||||
if not df_axis[name].is_unique:
|
if not df_axis[name].is_unique:
|
||||||
raise KeyError(f"Values in -{ax_name}-name must be unique. "
|
raise KeyError(
|
||||||
"Please prepare data to contain unique values.")
|
f"Values in -{ax_name}-name must be unique. " "Please prepare data to contain unique values."
|
||||||
|
)
|
||||||
# reset index to simple range; alias user-specified annotation to "name"
|
# reset index to simple range; alias user-specified annotation to "name"
|
||||||
df_axis.reset_index(drop=True, inplace=True)
|
df_axis.reset_index(drop=True, inplace=True)
|
||||||
df_axis.rename(inplace=True, columns={name: "name"})
|
df_axis.rename(inplace=True, columns={name: "name"})
|
||||||
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": {"nObs": self.cell_count, "nVar": self.gene_count, "type": str(self.data.X.dtype)},
|
||||||
"nObs": self.cell_count,
|
"annotations": {"obs": [], "var": []},
|
||||||
"nVar": self.gene_count,
|
|
||||||
"type": str(self.data.X.dtype)
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"obs": [],
|
|
||||||
"var": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for ax in Axis:
|
for ax in Axis:
|
||||||
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:
|
||||||
@@ -105,32 +122,35 @@ class ScanpyEngine(CXGDriver):
|
|||||||
try:
|
try:
|
||||||
result = sc.read(data, cache=True)
|
result = sc.read(data, cache=True)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ScanpyFileError("File must be in the .h5ad format. Please read "
|
raise ScanpyFileError(
|
||||||
"https://github.com/theislab/scanpy_usage/blob/master/170505_seurat/info_h5ad.md to "
|
"File must be in the .h5ad format. Please read "
|
||||||
"learn more about this format. You may be able to convert your file into this format "
|
"https://github.com/theislab/scanpy_usage/blob/master/170505_seurat/info_h5ad.md to "
|
||||||
"using `cellxgene prepare`, please run `cellxgene prepare --help` for more "
|
"learn more about this format. You may be able to convert your file into this format "
|
||||||
"information.")
|
"using `cellxgene prepare`, please run `cellxgene prepare --help` for more "
|
||||||
|
"information."
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ScanpyFileError(f"Error while loading file: {e}, File must be in the .h5ad format, please check "
|
raise ScanpyFileError(
|
||||||
f"that your input and try again.")
|
f"Error while loading file: {e}, File must be in the .h5ad format, please check "
|
||||||
|
f"that your input and try again."
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _validate_data_types(self):
|
def _validate_data_types(self):
|
||||||
if self.data.X.dtype != "float32":
|
if self.data.X.dtype != "float32":
|
||||||
warnings.warn(f"Scanpy data matrix is in {self.data.X.dtype} format not float32. "
|
warnings.warn(
|
||||||
f"Precision may be truncated.")
|
f"Scanpy data matrix is in {self.data.X.dtype} format not float32. " f"Precision may be truncated."
|
||||||
|
)
|
||||||
for ax in Axis:
|
for ax in Axis:
|
||||||
curr_axis = getattr(self.data, str(ax))
|
curr_axis = getattr(self.data, str(ax))
|
||||||
for ann in curr_axis:
|
for ann in curr_axis:
|
||||||
datatype = curr_axis[ann].dtype
|
datatype = curr_axis[ann].dtype
|
||||||
downcast_map = {"int64": "int32",
|
downcast_map = {"int64": "int32", "uint32": "int32", "uint64": "int32", "float64": "float32"}
|
||||||
"uint32": "int32",
|
|
||||||
"uint64": "int32",
|
|
||||||
"float64": "float32",
|
|
||||||
}
|
|
||||||
if datatype in downcast_map:
|
if datatype in downcast_map:
|
||||||
warnings.warn(f"Scanpy annotation {ax}:{ann} is in unsupported format: {datatype}. "
|
warnings.warn(
|
||||||
f"Data will be downcast to {downcast_map[datatype]}.")
|
f"Scanpy annotation {ax}:{ann} is in unsupported format: {datatype}. "
|
||||||
|
f"Data will be downcast to {downcast_map[datatype]}."
|
||||||
|
)
|
||||||
if isinstance(datatype, CategoricalDtype):
|
if isinstance(datatype, CategoricalDtype):
|
||||||
category_num = len(curr_axis[ann].dtype.categories)
|
category_num = len(curr_axis[ann].dtype.categories)
|
||||||
if category_num > 500 and category_num > self.max_category_items:
|
if category_num > 500 and category_num > self.max_category_items:
|
||||||
@@ -138,7 +158,8 @@ class ScanpyEngine(CXGDriver):
|
|||||||
f"{str(ax).title()} annotation '{ann}' has {category_num} categories, this may be "
|
f"{str(ax).title()} annotation '{ann}' has {category_num} categories, this may be "
|
||||||
f"cumbersome or slow to display. We recommend setting the "
|
f"cumbersome or slow to display. We recommend setting the "
|
||||||
f"--max-category-items option to 500, this will hide categorical "
|
f"--max-category-items option to 500, this will hide categorical "
|
||||||
f"annotations with more than 500 categories in the UI")
|
f"annotations with more than 500 categories in the UI"
|
||||||
|
)
|
||||||
|
|
||||||
def _validate_data_calculations(self):
|
def _validate_data_calculations(self):
|
||||||
layout_key = f"X_{self.layout_method}"
|
layout_key = f"X_{self.layout_method}"
|
||||||
@@ -150,7 +171,68 @@ class ScanpyEngine(CXGDriver):
|
|||||||
f" layout may have been computed. The requested layout must be pre-calculated and saved "
|
f" layout may have been computed. The requested layout must be pre-calculated and saved "
|
||||||
f"back in the h5ad file. You can run "
|
f"back in the h5ad file. You can run "
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
@@ -171,7 +253,7 @@ class ScanpyEngine(CXGDriver):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _annotation_filter_to_mask(filter, d_axis, count):
|
def _annotation_filter_to_mask(filter, d_axis, count):
|
||||||
mask = np.ones((count, ), dtype=bool)
|
mask = np.ones((count,), dtype=bool)
|
||||||
for v in filter:
|
for v in filter:
|
||||||
if d_axis[v["name"]].dtype.name in ["boolean", "category", "object"]:
|
if d_axis[v["name"]].dtype.name in ["boolean", "category", "object"]:
|
||||||
key_idx = np.in1d(getattr(d_axis, v["name"]), v["values"])
|
key_idx = np.in1d(getattr(d_axis, v["name"]), v["values"])
|
||||||
@@ -189,24 +271,23 @@ class ScanpyEngine(CXGDriver):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _index_filter_to_mask(filter, count):
|
def _index_filter_to_mask(filter, count):
|
||||||
mask = np.zeros((count, ), dtype=bool)
|
mask = np.zeros((count,), dtype=bool)
|
||||||
for i in filter:
|
for i in filter:
|
||||||
if type(i) == list:
|
if type(i) == list:
|
||||||
mask[i[0]:i[1]] = True
|
mask[i[0] : i[1]] = True
|
||||||
else:
|
else:
|
||||||
mask[i] = True
|
mask[i] = True
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _axis_filter_to_mask(filter, d_axis, count):
|
def _axis_filter_to_mask(filter, d_axis, count):
|
||||||
mask = np.ones((count, ), dtype=bool)
|
mask = np.ones((count,), dtype=bool)
|
||||||
if "index" in filter:
|
if "index" in filter:
|
||||||
mask = np.logical_and(mask, ScanpyEngine._index_filter_to_mask(filter["index"], count))
|
mask = np.logical_and(mask, ScanpyEngine._index_filter_to_mask(filter["index"], count))
|
||||||
if "annotation_value" in filter:
|
if "annotation_value" in filter:
|
||||||
mask = np.logical_and(mask,
|
mask = np.logical_and(
|
||||||
ScanpyEngine._annotation_filter_to_mask(filter["annotation_value"],
|
mask, ScanpyEngine._annotation_filter_to_mask(filter["annotation_value"], d_axis, count)
|
||||||
d_axis,
|
)
|
||||||
count))
|
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
def _filter_to_mask(self, filter, use_slices=True):
|
def _filter_to_mask(self, filter, use_slices=True):
|
||||||
@@ -236,8 +317,9 @@ class ScanpyEngine(CXGDriver):
|
|||||||
|
|
||||||
https://docs.scipy.org/doc/scipy/reference/sparse.html
|
https://docs.scipy.org/doc/scipy/reference/sparse.html
|
||||||
"""
|
"""
|
||||||
prefer_row_access = sparse.isspmatrix_csr(data._X) or sparse.isspmatrix_lil(data._X) \
|
prefer_row_access = (
|
||||||
or sparse.isspmatrix_bsr(data._X)
|
sparse.isspmatrix_csr(data._X) or sparse.isspmatrix_lil(data._X) or sparse.isspmatrix_bsr(data._X)
|
||||||
|
)
|
||||||
if prefer_row_access:
|
if prefer_row_access:
|
||||||
# Row-major slicing
|
# Row-major slicing
|
||||||
if obs_selector is not None:
|
if obs_selector is not None:
|
||||||
@@ -270,18 +352,12 @@ class ScanpyEngine(CXGDriver):
|
|||||||
obs = self.data.obs[obs_selector]
|
obs = self.data.obs[obs_selector]
|
||||||
if not fields:
|
if not fields:
|
||||||
fields = obs.columns.tolist()
|
fields = obs.columns.tolist()
|
||||||
result = {
|
result = {"names": fields, "data": DataFrame(obs[fields]).to_records(index=True).tolist()}
|
||||||
"names": fields,
|
|
||||||
"data": DataFrame(obs[fields]).to_records(index=True).tolist()
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
var = self.data.var[var_selector]
|
var = self.data.var[var_selector]
|
||||||
if not fields:
|
if not fields:
|
||||||
fields = var.columns.tolist()
|
fields = var.columns.tolist()
|
||||||
result = {
|
result = {"names": fields, "data": DataFrame(var[fields]).to_records(index=True).tolist()}
|
||||||
"names": fields,
|
|
||||||
"data": DataFrame(var[fields]).to_records(index=True).tolist()
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def data_frame(self, filter, axis):
|
def data_frame(self, filter, axis):
|
||||||
@@ -306,12 +382,12 @@ class ScanpyEngine(CXGDriver):
|
|||||||
if axis == Axis.OBS:
|
if axis == Axis.OBS:
|
||||||
result = {
|
result = {
|
||||||
"var": var_index_sliced.tolist(),
|
"var": var_index_sliced.tolist(),
|
||||||
"obs": DataFrame(_X, index=obs_index_sliced).to_records(index=True).tolist()
|
"obs": DataFrame(_X, index=obs_index_sliced).to_records(index=True).tolist(),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
result = {
|
result = {
|
||||||
"obs": obs_index_sliced.tolist(),
|
"obs": obs_index_sliced.tolist(),
|
||||||
"var": DataFrame(_X.T, index=var_index_sliced).to_records(index=True).tolist()
|
"var": DataFrame(_X.T, index=var_index_sliced).to_records(index=True).tolist(),
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -350,11 +426,11 @@ class ScanpyEngine(CXGDriver):
|
|||||||
try:
|
try:
|
||||||
df_layout = df.obsm[f"X_{self.layout_method}"]
|
df_layout = df.obsm[f"X_{self.layout_method}"]
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise PrepareError(f"Layout has not been calculated using {self.layout_method}, "
|
raise PrepareError(
|
||||||
f"please prepare your datafile and relaunch cellxgene") from e
|
f"Layout has not been calculated using {self.layout_method}, "
|
||||||
normalized_layout = DataFrame((df_layout - df_layout.min()) / (df_layout.max() - df_layout.min()),
|
f"please prepare your datafile and relaunch cellxgene"
|
||||||
index=df.obs.index)
|
) from e
|
||||||
return {
|
normalized_layout = DataFrame(
|
||||||
"ndims": normalized_layout.shape[1],
|
(df_layout - df_layout.min()) / (df_layout.max() - df_layout.min()), index=df.obs.index
|
||||||
"coordinates": normalized_layout.to_records(index=True).tolist()
|
)
|
||||||
}
|
return {"ndims": normalized_layout.shape[1], "coordinates": normalized_layout.to_records(index=True).tolist()}
|
||||||
|
|||||||
@@ -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,7 +7,6 @@ from server.app.util.constants import Axis
|
|||||||
|
|
||||||
|
|
||||||
class QueryStringError(Exception):
|
class QueryStringError(Exception):
|
||||||
|
|
||||||
def __init__(self, key, message):
|
def __init__(self, key, message):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|||||||
@@ -5,24 +5,13 @@ class AnnotationModel(Schema):
|
|||||||
type = "object"
|
type = "object"
|
||||||
description = "Filter by annotation key: value"
|
description = "Filter by annotation key: value"
|
||||||
properties = {
|
properties = {
|
||||||
"name": {
|
"name": {"type": "string"},
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
# TODO update to OpenAPI v3.0 when a library is available that supports it
|
# TODO update to OpenAPI v3.0 when a library is available that supports it
|
||||||
# Unfortunately 2.0 doesn't have a way to have a schema that accepts multiple types
|
# Unfortunately 2.0 doesn't have a way to have a schema that accepts multiple types
|
||||||
# Overloading the type key with a list seems to work ok and makes it to the page
|
# Overloading the type key with a list seems to work ok and makes it to the page
|
||||||
"values": {
|
"values": {"type": "array", "items": {"type": ["float32", "string", "int32", "bool"]}},
|
||||||
"type": "array",
|
"min": {"type": ["int32", "float32"]},
|
||||||
"items": {
|
"max": {"type": ["int32", "float32"]},
|
||||||
"type": ["float32", "string", "int32", "bool"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"min": {
|
|
||||||
"type": ["int32", "float32"],
|
|
||||||
},
|
|
||||||
"max": {
|
|
||||||
"type": ["int32", "float32"],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
required = ["name"]
|
required = ["name"]
|
||||||
|
|
||||||
@@ -30,36 +19,16 @@ class AnnotationModel(Schema):
|
|||||||
class IndexModel(Schema):
|
class IndexModel(Schema):
|
||||||
type = "object"
|
type = "object"
|
||||||
description = "Filter by index of observation/variable ex. [0, 5, 15]"
|
description = "Filter by index of observation/variable ex. [0, 5, 15]"
|
||||||
properties = {
|
properties = {"index": {"type": "array", "items": {"format": "int32", "type": "integer"}}}
|
||||||
"index": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"format": "int32",
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AxisModel(Schema):
|
class AxisModel(Schema):
|
||||||
type = "object"
|
type = "object"
|
||||||
description = "Axis of data -- obs or var"
|
description = "Axis of data -- obs or var"
|
||||||
properties = {
|
properties = {"index": IndexModel, "annotation_value": AnnotationModel.array()}
|
||||||
"index": IndexModel,
|
|
||||||
"annotation_value": AnnotationModel.array()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class FilterModel(Schema):
|
class FilterModel(Schema):
|
||||||
type = "object"
|
type = "object"
|
||||||
description = "Complex filter"
|
description = "Complex filter"
|
||||||
properties = {
|
properties = {"filter": {"type": "object", "properties": {"obs": AxisModel, "var": AxisModel}}}
|
||||||
"filter": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"obs": AxisModel,
|
|
||||||
"var": AxisModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,8 +26,13 @@ class Float32JSONEncoder(json.JSONEncoder):
|
|||||||
return json.JSONEncoder.default(self, obj)
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|
||||||
def get_mime_type(default="application/json", acceptable_types=["application/json", "text/csv"], query_param=None,
|
def custom_format_warning(msg, *args, **kwargs):
|
||||||
header=None):
|
return f"[cellxgene] Warning: {msg} \n"
|
||||||
|
|
||||||
|
|
||||||
|
def get_mime_type(
|
||||||
|
default="application/json", acceptable_types=["application/json", "text/csv"], query_param=None, header=None
|
||||||
|
):
|
||||||
mime_type = default
|
mime_type = default
|
||||||
if query_param:
|
if query_param:
|
||||||
if query_param in acceptable_types:
|
if query_param in acceptable_types:
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from flask import (
|
from flask import Blueprint, render_template, send_from_directory, current_app
|
||||||
Blueprint, render_template, send_from_directory, current_app
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
bp = Blueprint("webapp", __name__, template_folder="templates")
|
bp = Blueprint("webapp", __name__, template_folder="templates")
|
||||||
|
|||||||
+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.4.0", prog_name="cellxgene", message="[%(prog)s] Version %(version)s")
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
+82
-35
@@ -1,37 +1,88 @@
|
|||||||
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()
|
||||||
@click.argument("data", metavar="<data file>", type=click.Path(exists=True, file_okay=True, dir_okay=False))
|
@click.argument("data", metavar="<data file>", type=click.Path(exists=True, file_okay=True, dir_okay=False))
|
||||||
@click.option("--layout", "-l", type=click.Choice(["umap", "tsne"]), default="umap", show_default=True,
|
@click.option(
|
||||||
help="Method for layout.")
|
"--layout", "-l", type=click.Choice(["umap", "tsne"]), default="umap", show_default=True, help="Method for layout."
|
||||||
@click.option("--diffexp", "-d", type=click.Choice(["ttest"]), default="ttest", show_default=True,
|
)
|
||||||
help="Method for differential expression.")
|
@click.option(
|
||||||
|
"--diffexp",
|
||||||
|
"-d",
|
||||||
|
type=click.Choice(["ttest"]),
|
||||||
|
default="ttest",
|
||||||
|
show_default=True,
|
||||||
|
help="Method for differential expression.",
|
||||||
|
)
|
||||||
@click.option("--title", "-t", help="Title to display (if omitted will use file name).", metavar="")
|
@click.option("--title", "-t", help="Title to display (if omitted will use file name).", metavar="")
|
||||||
@click.option("--verbose", "-v", is_flag=True, default=False, show_default=True,
|
@click.option(
|
||||||
help="Provide verbose output, including warnings and all server requests.")
|
"--verbose",
|
||||||
@click.option("--debug", "-d", is_flag=True, default=False, show_default=True,
|
"-v",
|
||||||
help="Run in debug mode.")
|
is_flag=True,
|
||||||
@click.option("--open", "-o", "open_browser", is_flag=True, default=False, show_default=True,
|
default=False,
|
||||||
help="Open the web browser after launch.")
|
show_default=True,
|
||||||
|
help="Provide verbose output, including warnings and all server requests.",
|
||||||
|
)
|
||||||
|
@click.option("--debug", "-d", is_flag=True, default=False, show_default=True, help="Run in debug mode.")
|
||||||
|
@click.option(
|
||||||
|
"--open",
|
||||||
|
"-o",
|
||||||
|
"open_browser",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
show_default=True,
|
||||||
|
help="Open the web browser after launch.",
|
||||||
|
)
|
||||||
@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(
|
||||||
@click.option("--max-category-items", default=100, metavar="", show_default=True,
|
"--max-category-items",
|
||||||
help="Limits the number of categorical annotation items displayed.")
|
default=100,
|
||||||
@click.option("--diffexp-lfc-cutoff", default=0.01, show_default=True,
|
metavar="",
|
||||||
help="Relative expression cutoff used when selecting top N differentially expressed genes")
|
show_default=True,
|
||||||
def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
help="Limits the number of categorical annotation items displayed.",
|
||||||
open_browser, port, listen_all, max_category_items, diffexp_lfc_cutoff):
|
)
|
||||||
|
@click.option(
|
||||||
|
"--diffexp-lfc-cutoff",
|
||||||
|
default=0.01,
|
||||||
|
show_default=True,
|
||||||
|
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,
|
||||||
|
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 +97,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 +105,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,19 +115,14 @@ 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/"
|
||||||
|
|
||||||
app.config.update(
|
# Import Flask app
|
||||||
DATASET_TITLE=title,
|
from server.app.app import app
|
||||||
CXG_API_BASE=api_base
|
|
||||||
)
|
app.config.update(DATASET_TITLE=title, CXG_API_BASE=api_base)
|
||||||
|
|
||||||
if not verbose:
|
if not verbose:
|
||||||
log = logging.getLogger("werkzeug")
|
log = logging.getLogger("werkzeug")
|
||||||
@@ -88,7 +133,8 @@ def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
|||||||
# Fix for anaconda python. matplotlib typically expects python to be installed as a framework TKAgg is usually
|
# Fix for anaconda python. matplotlib typically expects python to be installed as a framework TKAgg is usually
|
||||||
# available and fixes this issue. See https://matplotlib.org/faq/virtualenv_faq.html
|
# available and fixes this issue. See https://matplotlib.org/faq/virtualenv_faq.html
|
||||||
import matplotlib as mpl
|
import matplotlib as mpl
|
||||||
mpl.use('TkAgg')
|
|
||||||
|
mpl.use("TkAgg")
|
||||||
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
from server.app.scanpy_engine.scanpy_engine import ScanpyEngine
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
@@ -97,7 +143,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:
|
||||||
@@ -114,7 +161,7 @@ def launch(data, layout, diffexp, title, verbose, debug, obs_names, var_names,
|
|||||||
click.echo("[cellxgene] Type CTRL-C at any time to exit.")
|
click.echo("[cellxgene] Type CTRL-C at any time to exit.")
|
||||||
|
|
||||||
if not verbose:
|
if not verbose:
|
||||||
f = open(devnull, 'w')
|
f = open(devnull, "w")
|
||||||
sys.stdout = f
|
sys.stdout = f
|
||||||
|
|
||||||
app.run(host=host, debug=debug, port=port, threaded=True)
|
app.run(host=host, debug=debug, port=port, threaded=True)
|
||||||
|
|||||||
+48
-17
@@ -7,22 +7,48 @@ from scipy.sparse.csc import csc_matrix
|
|||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("data", nargs=1, metavar="<dataset: file or path to data>", required=True)
|
@click.argument("data", nargs=1, metavar="<dataset: file or path to data>", required=True)
|
||||||
@click.option("--layout", "-l", default=["umap", "tsne"], multiple=True, type=click.Choice(["umap", "tsne"]),
|
@click.option(
|
||||||
help="Layout algorithm", show_default=True)
|
"--layout",
|
||||||
@click.option("--recipe", "-r", default="none", type=click.Choice(["none", "seurat", "zheng17"]),
|
"-l",
|
||||||
help="Preprocessing to run.", show_default=True)
|
default=["umap", "tsne"],
|
||||||
|
multiple=True,
|
||||||
|
type=click.Choice(["umap", "tsne"]),
|
||||||
|
help="Layout algorithm",
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--recipe",
|
||||||
|
"-r",
|
||||||
|
default="none",
|
||||||
|
type=click.Choice(["none", "seurat", "zheng17"]),
|
||||||
|
help="Preprocessing to run.",
|
||||||
|
show_default=True,
|
||||||
|
)
|
||||||
@click.option("--output", "-o", default="", help="Save a new file to filename.", metavar="<filename>")
|
@click.option("--output", "-o", default="", help="Save a new file to filename.", metavar="<filename>")
|
||||||
@click.option("--plotting", "-p", default=False, is_flag=True, help="Whether to generate plots.", show_default=True)
|
@click.option("--plotting", "-p", default=False, is_flag=True, help="Whether to generate plots.", show_default=True)
|
||||||
@click.option("--sparse", default=False, is_flag=True, help="Whether to force sparsity.", show_default=True)
|
@click.option("--sparse", default=False, is_flag=True, help="Whether to force sparsity.", show_default=True)
|
||||||
@click.option("--overwrite", default=False, is_flag=True, help="Allow file overwriting.", show_default=True)
|
@click.option("--overwrite", default=False, is_flag=True, help="Allow file overwriting.", show_default=True)
|
||||||
@click.option("--set-obs-names", default="", help="Named field to set as index for obs.", metavar="<name>")
|
@click.option("--set-obs-names", default="", help="Named field to set as index for obs.", metavar="<name>")
|
||||||
@click.option("--set-var-names", default="", help="Named field to set as index for var.", metavar="<name>")
|
@click.option("--set-var-names", default="", help="Named field to set as index for var.", metavar="<name>")
|
||||||
@click.option("--make-obs-names-unique", default=True, is_flag=True,
|
@click.option(
|
||||||
help="Ensure obs index is unique.", show_default=True)
|
"--make-obs-names-unique", default=True, is_flag=True, help="Ensure obs index is unique.", show_default=True
|
||||||
@click.option("--make-var-names-unique", default=True, is_flag=True,
|
)
|
||||||
help="Ensure var index is unique.", show_default=True)
|
@click.option(
|
||||||
def prepare(data, layout, recipe, output, plotting, sparse, overwrite,
|
"--make-var-names-unique", default=True, is_flag=True, help="Ensure var index is unique.", show_default=True
|
||||||
set_obs_names, set_var_names, make_obs_names_unique, make_var_names_unique):
|
)
|
||||||
|
def prepare(
|
||||||
|
data,
|
||||||
|
layout,
|
||||||
|
recipe,
|
||||||
|
output,
|
||||||
|
plotting,
|
||||||
|
sparse,
|
||||||
|
overwrite,
|
||||||
|
set_obs_names,
|
||||||
|
set_var_names,
|
||||||
|
make_obs_names_unique,
|
||||||
|
make_var_names_unique,
|
||||||
|
):
|
||||||
"""Preprocesses data for use with cellxgene.
|
"""Preprocesses data for use with cellxgene.
|
||||||
|
|
||||||
This tool runs a series of scanpy routines for preparing a dataset
|
This tool runs a series of scanpy routines for preparing a dataset
|
||||||
@@ -35,6 +61,7 @@ def prepare(data, layout, recipe, output, plotting, sparse, overwrite,
|
|||||||
# collect slow imports here to make CLI startup more responsive
|
# collect slow imports here to make CLI startup more responsive
|
||||||
click.echo("[cellxgene] Starting CLI...")
|
click.echo("[cellxgene] Starting CLI...")
|
||||||
import matplotlib
|
import matplotlib
|
||||||
|
|
||||||
matplotlib.use("Agg")
|
matplotlib.use("Agg")
|
||||||
import scanpy.api as sc
|
import scanpy.api as sc
|
||||||
|
|
||||||
@@ -49,8 +76,10 @@ def prepare(data, layout, recipe, output, plotting, sparse, overwrite,
|
|||||||
output = expanduser(output)
|
output = expanduser(output)
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
click.echo("Warning: No file will be saved, to save the results of cellxgene prepare include "
|
click.echo(
|
||||||
"--output <filename> to save output to a new file")
|
"Warning: No file will be saved, to save the results of cellxgene prepare include "
|
||||||
|
"--output <filename> to save output to a new file"
|
||||||
|
)
|
||||||
if isfile(output) and not overwrite:
|
if isfile(output) and not overwrite:
|
||||||
raise click.UsageError(f"Cannot overwrite existing file {output}, try using the flag --overwrite")
|
raise click.UsageError(f"Cannot overwrite existing file {output}, try using the flag --overwrite")
|
||||||
|
|
||||||
@@ -119,9 +148,11 @@ def prepare(data, layout, recipe, output, plotting, sparse, overwrite,
|
|||||||
try:
|
try:
|
||||||
sc.tl.louvain(adata)
|
sc.tl.louvain(adata)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
click.echo("\nWarning: louvain module is not installed, no clusters will be calculated. "
|
click.echo(
|
||||||
"To fix this please install cellxgene with the optional feature louvain enabled: "
|
"\nWarning: louvain module is not installed, no clusters will be calculated. "
|
||||||
"`pip install cellxgene[louvain]`")
|
"To fix this please install cellxgene with the optional feature louvain enabled: "
|
||||||
|
"`pip install cellxgene[louvain]`"
|
||||||
|
)
|
||||||
|
|
||||||
def run_layout(adata):
|
def run_layout(adata):
|
||||||
if len(unique(adata.obs["louvain"].values)) < 10:
|
if len(unique(adata.obs["louvain"].values)) < 10:
|
||||||
@@ -142,11 +173,11 @@ def prepare(data, layout, recipe, output, plotting, sparse, overwrite,
|
|||||||
def show_step(item):
|
def show_step(item):
|
||||||
names = {
|
names = {
|
||||||
"make_sparse": "Ensuring sparsity",
|
"make_sparse": "Ensuring sparsity",
|
||||||
"run_recipe": f"Running preprocessing recipe \"{recipe}\"",
|
"run_recipe": f'Running preprocessing recipe "{recipe}"',
|
||||||
"run_pca": "Running PCA",
|
"run_pca": "Running PCA",
|
||||||
"run_neighbors": "Calculating neighbors",
|
"run_neighbors": "Calculating neighbors",
|
||||||
"run_louvain": "Calculating clusters",
|
"run_louvain": "Calculating clusters",
|
||||||
"run_layout": "Computing layout"
|
"run_layout": "Computing layout",
|
||||||
}
|
}
|
||||||
if item is not None:
|
if item is not None:
|
||||||
return names[item.__name__]
|
return names[item.__name__]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
black
|
||||||
|
bumpversion>=0.5
|
||||||
pytest>=3.6.3
|
pytest>=3.6.3
|
||||||
requests>=2.18.4
|
requests>=2.18.4
|
||||||
twine>=1.12.1
|
twine>=1.12.1
|
||||||
bumpversion>=0.5
|
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|||||||
@@ -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.19.1,!=0.20.0
|
||||||
|
|||||||
+59
-80
@@ -9,15 +9,7 @@ LOCAL_URL = "http://127.0.0.1:5005/"
|
|||||||
VERSION = "v0.2"
|
VERSION = "v0.2"
|
||||||
URL_BASE = f"{LOCAL_URL}api/{VERSION}/"
|
URL_BASE = f"{LOCAL_URL}api/{VERSION}/"
|
||||||
|
|
||||||
BAD_FILTER = {
|
BAD_FILTER = {"filter": {"obs": {"annotation_value": [{"name": "xyz"}]}}}
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "xyz"},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class EndPoints(unittest.TestCase):
|
class EndPoints(unittest.TestCase):
|
||||||
@@ -133,7 +125,7 @@ class EndPoints(unittest.TestCase):
|
|||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||||
{"name": "n_counts", "min": 3000},
|
{"name": "n_counts", "min": 3000},
|
||||||
],
|
],
|
||||||
"index": [1, 99, [1000, 2000]]
|
"index": [1, 99, [1000, 2000]],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +146,7 @@ class EndPoints(unittest.TestCase):
|
|||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||||
{"name": "n_counts", "min": 3000},
|
{"name": "n_counts", "min": 3000},
|
||||||
],
|
],
|
||||||
"index": [1, 99, [1000, 2000]]
|
"index": [1, 99, [1000, 2000]],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,23 +162,9 @@ class EndPoints(unittest.TestCase):
|
|||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
params = {
|
params = {
|
||||||
"mode": "topN",
|
"mode": "topN",
|
||||||
"set1": {
|
"set1": {"filter": {"obs": {"annotation_value": [{"name": "louvain", "values": ["NK cells"]}]}}},
|
||||||
"filter": {
|
"set2": {"filter": {"obs": {"annotation_value": [{"name": "louvain", "values": ["CD8 T cells"]}]}}},
|
||||||
"obs": {"annotation_value": [
|
"count": 7,
|
||||||
{"name": "louvain", "values": ["NK cells"]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"set2": {
|
|
||||||
"filter": {
|
|
||||||
"obs": {"annotation_value": [
|
|
||||||
{"name": "louvain", "values": ["CD8 T cells"]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"count": 7
|
|
||||||
}
|
}
|
||||||
result = self.session.post(url, json=params)
|
result = self.session.post(url, json=params)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
@@ -199,20 +177,8 @@ class EndPoints(unittest.TestCase):
|
|||||||
params = {
|
params = {
|
||||||
"mode": "topN",
|
"mode": "topN",
|
||||||
"count": 10,
|
"count": 10,
|
||||||
"set1": {
|
"set1": {"filter": {"obs": {"index": [[0, 500]]}}},
|
||||||
"filter": {
|
"set2": {"filter": {"obs": {"index": [[500, 1000]]}}},
|
||||||
"obs": {
|
|
||||||
"index": [[0, 500]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"set2": {
|
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"index": [[500, 1000]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result = self.session.post(url, json=params)
|
result = self.session.post(url, json=params)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
@@ -249,15 +215,7 @@ class EndPoints(unittest.TestCase):
|
|||||||
def test_put_annotations_var(self):
|
def test_put_annotations_var(self):
|
||||||
endpoint = "annotations/var"
|
endpoint = "annotations/var"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
var_filter = {
|
var_filter = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["ATAD3C", "RER1"]}]}}}
|
||||||
"filter": {
|
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "name", "values": ["ATAD3C", "RER1"]},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, json=var_filter)
|
result = self.session.put(url, json=var_filter)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
@@ -268,15 +226,7 @@ class EndPoints(unittest.TestCase):
|
|||||||
endpoint = "annotations/var"
|
endpoint = "annotations/var"
|
||||||
query = "annotation-name=n_cells"
|
query = "annotation-name=n_cells"
|
||||||
url = f"{URL_BASE}{endpoint}?{query}"
|
url = f"{URL_BASE}{endpoint}?{query}"
|
||||||
var_filter = {
|
var_filter = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["ATAD3C", "RER1"]}]}}}
|
||||||
"filter": {
|
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "name", "values": ["ATAD3C", "RER1"]},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, json=var_filter)
|
result = self.session.put(url, json=var_filter)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
@@ -335,7 +285,7 @@ class EndPoints(unittest.TestCase):
|
|||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||||
{"name": "n_counts", "min": 3000},
|
{"name": "n_counts", "min": 3000},
|
||||||
],
|
],
|
||||||
"index": [1, 99, [1000, 2000]]
|
"index": [1, 99, [1000, 2000]],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,15 +299,7 @@ class EndPoints(unittest.TestCase):
|
|||||||
endpoint = f"data/{axis}"
|
endpoint = f"data/{axis}"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
header = {"Accept": "application/json"}
|
header = {"Accept": "application/json"}
|
||||||
var_filter = {
|
var_filter = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}}}
|
||||||
"filter": {
|
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "name", "values": ["RER1"]},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.session.put(url, headers=header, json=var_filter)
|
result = self.session.put(url, headers=header, json=var_filter)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
@@ -371,16 +313,44 @@ class EndPoints(unittest.TestCase):
|
|||||||
def test_cache(self):
|
def test_cache(self):
|
||||||
endpoint = "annotations/var"
|
endpoint = "annotations/var"
|
||||||
url = f"{URL_BASE}{endpoint}"
|
url = f"{URL_BASE}{endpoint}"
|
||||||
f1 = {"filter": {"var": {"annotation_value": [{"name": "name",
|
f1 = {
|
||||||
"values": ["HLA-DRB1", "HLA-DQA1", "HLA-DQB1", "HLA-DPA1",
|
"filter": {
|
||||||
"HLA-DPB1", "MS4A1", "IL32", "CCL5", "CD79B",
|
"var": {
|
||||||
"CD79A"]}]}}}
|
"annotation_value": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"values": [
|
||||||
|
"HLA-DRB1",
|
||||||
|
"HLA-DQA1",
|
||||||
|
"HLA-DQB1",
|
||||||
|
"HLA-DPA1",
|
||||||
|
"HLA-DPB1",
|
||||||
|
"MS4A1",
|
||||||
|
"IL32",
|
||||||
|
"CCL5",
|
||||||
|
"CD79B",
|
||||||
|
"CD79A",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
result = self.session.put(url, json=f1)
|
result = self.session.put(url, json=f1)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
result_data1 = result.json()
|
result_data1 = result.json()
|
||||||
f2 = {"filter": {"var": {"annotation_value": [{"name": "name",
|
f2 = {
|
||||||
"values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH",
|
"filter": {
|
||||||
"CCL5", "CCL4", "CST7", "NKG7"]}]}}}
|
"var": {
|
||||||
|
"annotation_value": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH", "CCL5", "CCL4", "CST7", "NKG7"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
result = self.session.put(url, json=f2)
|
result = self.session.put(url, json=f2)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
result_data2 = result.json()
|
result_data2 = result.json()
|
||||||
@@ -393,9 +363,18 @@ class EndPoints(unittest.TestCase):
|
|||||||
result = self.session.put(url, json=f1)
|
result = self.session.put(url, json=f1)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
result_data1 = result.json()
|
result_data1 = result.json()
|
||||||
f2 = {"filter": {"var": {"annotation_value": [{"name": "name",
|
f2 = {
|
||||||
"values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH",
|
"filter": {
|
||||||
"CCL5", "CCL4", "CST7", "NKG7"]}]}}}
|
"var": {
|
||||||
|
"annotation_value": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"values": ["FGFBP2", "GZMA", "LTB", "PRF1", "CTSW", "GZMH", "CCL5", "CCL4", "CST7", "NKG7"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
result = self.session.put(url, json=f2)
|
result = self.session.put(url, json=f2)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
result_data2 = result.json()
|
result_data2 = result.json()
|
||||||
|
|||||||
@@ -54,13 +54,17 @@ class UtilTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_complex_filter(self):
|
def test_complex_filter(self):
|
||||||
filter_dict = ImmutableMultiDict(
|
filter_dict = ImmutableMultiDict(
|
||||||
[("obs:louvain", "NK cells"), ("obs:louvain", "CD8 T cells"), ("obs:n_counts", "3000,*")])
|
[("obs:louvain", "NK cells"), ("obs:louvain", "CD8 T cells"), ("obs:n_counts", "3000,*")]
|
||||||
|
)
|
||||||
filter_ = parse_filter(filter_dict, self.schema)
|
filter_ = parse_filter(filter_dict, self.schema)
|
||||||
self.assertIn("obs", filter_)
|
self.assertIn("obs", filter_)
|
||||||
self.assertEqual(filter_["obs"]["annotation_value"], [{"name": "louvain",
|
self.assertEqual(
|
||||||
"values": ["NK cells", "CD8 T cells"]},
|
filter_["obs"]["annotation_value"],
|
||||||
{"name": "n_counts",
|
[
|
||||||
"max": None, "min": 3000.0}])
|
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||||
|
{"name": "n_counts", "max": None, "min": 3000.0},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_bad_filter(self):
|
def test_bad_filter(self):
|
||||||
bad_annotation_type = ImmutableMultiDict([("obs:tissue", "lung")])
|
bad_annotation_type = ImmutableMultiDict([("obs:tissue", "lung")])
|
||||||
@@ -71,9 +75,7 @@ class UtilTest(unittest.TestCase):
|
|||||||
parse_filter(bad_axis, self.schema)
|
parse_filter(bad_axis, self.schema)
|
||||||
|
|
||||||
def test_boolean_filter(self):
|
def test_boolean_filter(self):
|
||||||
schema = {
|
schema = {"obs": [{"name": "bool_filter", "type": "boolean"}]}
|
||||||
"obs": [{"name": "bool_filter", "type": "boolean"}]
|
|
||||||
}
|
|
||||||
filter_dict = ImmutableMultiDict([("obs:bool_filter", "false")])
|
filter_dict = ImmutableMultiDict([("obs:bool_filter", "false")])
|
||||||
filter_ = parse_filter(filter_dict, schema)
|
filter_ = parse_filter(filter_dict, schema)
|
||||||
self.assertIn("obs", filter_)
|
self.assertIn("obs", filter_)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from os import path
|
|||||||
import pytest
|
import pytest
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
import argparse
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pandas import Series
|
from pandas import Series
|
||||||
@@ -13,8 +12,15 @@ 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 = {
|
||||||
'obs_names': None, 'var_names': None, 'diffexp_lfc_cutoff': 0.01}
|
"layout": "umap",
|
||||||
|
"diffexp": "ttest",
|
||||||
|
"max_category_items": 100,
|
||||||
|
"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()
|
||||||
@@ -22,8 +28,8 @@ class UtilTest(unittest.TestCase):
|
|||||||
def test_init(self):
|
def test_init(self):
|
||||||
self.assertEqual(self.data.cell_count, 2638)
|
self.assertEqual(self.data.cell_count, 2638)
|
||||||
self.assertEqual(self.data.gene_count, 1838)
|
self.assertEqual(self.data.gene_count, 1838)
|
||||||
epsilon = 0.000005
|
epsilon = 0.000_005
|
||||||
self.assertTrue(self.data.data.X[0, 0] - -0.17146951 < epsilon)
|
self.assertTrue(self.data.data.X[0, 0] - -0.171_469_51 < epsilon)
|
||||||
|
|
||||||
def test_mandatory_annotations(self):
|
def test_mandatory_annotations(self):
|
||||||
self.assertIn("name", self.data.data.obs)
|
self.assertIn("name", self.data.data.obs)
|
||||||
@@ -38,69 +44,36 @@ class UtilTest(unittest.TestCase):
|
|||||||
self.data._validate_data_types()
|
self.data._validate_data_types()
|
||||||
|
|
||||||
def test_filter_idx(self):
|
def test_filter_idx(self):
|
||||||
filter_ = {
|
filter_ = {"filter": {"var": {"index": [1, 99, [200, 300]]}, "obs": {"index": [1, 99, [1000, 2000]]}}}
|
||||||
"filter": {
|
|
||||||
"var": {
|
|
||||||
"index": [1, 99, [200, 300]]
|
|
||||||
},
|
|
||||||
"obs": {
|
|
||||||
"index": [1, 99, [1000, 2000]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
data = self.data.filter_dataframe(filter_["filter"])
|
||||||
self.assertEqual(data.shape, (1002, 102))
|
self.assertEqual(data.shape, (1002, 102))
|
||||||
|
|
||||||
def test_filter_annotation(self):
|
def test_filter_annotation(self):
|
||||||
filter_ = {
|
filter_ = {
|
||||||
"filter": {
|
"filter": {"obs": {"annotation_value": [{"name": "louvain", "values": ["NK cells", "CD8 T cells"]}]}}
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
data = self.data.filter_dataframe(filter_["filter"])
|
||||||
self.assertEqual(data.shape, (470, 1838))
|
self.assertEqual(data.shape, (470, 1838))
|
||||||
filter_ = {
|
filter_ = {"filter": {"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]}}}
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
data = self.data.filter_dataframe(filter_["filter"])
|
||||||
self.assertEqual(data.shape, (497, 1838))
|
self.assertEqual(data.shape, (497, 1838))
|
||||||
|
|
||||||
def test_filter_annotation_no_uns(self):
|
def test_filter_annotation_no_uns(self):
|
||||||
filter_ = {
|
filter_ = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}}}
|
||||||
"filter": {
|
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "name", "values": ["RER1"]},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
data = self.data.filter_dataframe(filter_["filter"])
|
||||||
self.assertEqual(data.shape[1], 1)
|
self.assertEqual(data.shape[1], 1)
|
||||||
|
|
||||||
def test_filter_complex(self):
|
def test_filter_complex(self):
|
||||||
filter_ = {
|
filter_ = {
|
||||||
"filter": {
|
"filter": {
|
||||||
"var": {
|
"var": {"index": [1, 99, [200, 300]]},
|
||||||
"index": [1, 99, [200, 300]]
|
|
||||||
},
|
|
||||||
"obs": {
|
"obs": {
|
||||||
"annotation_value": [
|
"annotation_value": [
|
||||||
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
{"name": "louvain", "values": ["NK cells", "CD8 T cells"]},
|
||||||
{"name": "n_counts", "min": 3000},
|
{"name": "n_counts", "min": 3000},
|
||||||
],
|
],
|
||||||
"index": [1, 99, [1000, 2000]]
|
"index": [1, 99, [1000, 2000]],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data = self.data.filter_dataframe(filter_["filter"])
|
data = self.data.filter_dataframe(filter_["filter"])
|
||||||
@@ -116,13 +89,14 @@ class UtilTest(unittest.TestCase):
|
|||||||
self.assertEqual(self.data.schema, schema)
|
self.assertEqual(self.data.schema, schema)
|
||||||
|
|
||||||
def test_schema_produces_error(self):
|
def test_schema_produces_error(self):
|
||||||
self.data.data.obs["time"] = Series(list([time.time() for i in range(self.data.cell_count)]),
|
self.data.data.obs["time"] = Series(
|
||||||
dtype="datetime64[ns]")
|
list([time.time() for i in range(self.data.cell_count)]), dtype="datetime64[ns]"
|
||||||
|
)
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
self.data._create_schema()
|
self.data._create_schema()
|
||||||
|
|
||||||
def test_config(self):
|
def test_config(self):
|
||||||
self.assertEqual(self.data.features["layout"]["obs"], {'available': True, 'interactiveLimit': 50000})
|
self.assertEqual(self.data.features["layout"]["obs"], {"available": True, "interactiveLimit": 50000})
|
||||||
|
|
||||||
def test_layout(self):
|
def test_layout(self):
|
||||||
layout = self.data.layout(None)
|
layout = self.data.layout(None)
|
||||||
@@ -152,16 +126,8 @@ class UtilTest(unittest.TestCase):
|
|||||||
def test_filtered_annotation(self):
|
def test_filtered_annotation(self):
|
||||||
filter_ = {
|
filter_ = {
|
||||||
"filter": {
|
"filter": {
|
||||||
"obs": {
|
"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]},
|
||||||
"annotation_value": [
|
"var": {"annotation_value": [{"name": "name", "values": ["ATAD3C", "RER1"]}]},
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "name", "values": ["ATAD3C", "RER1"]},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
annotations = self.data.annotation(filter_["filter"], "obs")
|
annotations = self.data.annotation(filter_["filter"], "obs")
|
||||||
@@ -172,33 +138,13 @@ class UtilTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(annotations["data"]), 2)
|
self.assertEqual(len(annotations["data"]), 2)
|
||||||
|
|
||||||
def test_filtered_layout(self):
|
def test_filtered_layout(self):
|
||||||
filter_ = {
|
filter_ = {"filter": {"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]}}}
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
layout = self.data.layout(filter_["filter"])
|
layout = self.data.layout(filter_["filter"])
|
||||||
self.assertEqual(len(layout["coordinates"]), 497)
|
self.assertEqual(len(layout["coordinates"]), 497)
|
||||||
|
|
||||||
def test_diffexp_topN(self):
|
def test_diffexp_topN(self):
|
||||||
f1 = {
|
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
||||||
"filter": {
|
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
||||||
"obs": {
|
|
||||||
"index": [[0, 500]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f2 = {
|
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"index": [[500, 1000]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = self.data.diffexp_topN(f1["filter"], f2["filter"])
|
result = self.data.diffexp_topN(f1["filter"], f2["filter"])
|
||||||
self.assertEqual(len(result), 10)
|
self.assertEqual(len(result), 10)
|
||||||
result = self.data.diffexp_topN(f1["filter"], f2["filter"], 20)
|
result = self.data.diffexp_topN(f1["filter"], f2["filter"], 20)
|
||||||
@@ -213,15 +159,7 @@ class UtilTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(data_frame_var["obs"]), 2638)
|
self.assertEqual(len(data_frame_var["obs"]), 2638)
|
||||||
|
|
||||||
def test_filtered_data_frame(self):
|
def test_filtered_data_frame(self):
|
||||||
filter_ = {
|
filter_ = {"filter": {"obs": {"annotation_value": [{"name": "n_counts", "min": 3000}]}}}
|
||||||
"filter": {
|
|
||||||
"obs": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "n_counts", "min": 3000},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data_frame_obs = self.data.data_frame(filter_["filter"], "obs")
|
data_frame_obs = self.data.data_frame(filter_["filter"], "obs")
|
||||||
self.assertEqual(len(data_frame_obs["var"]), 1838)
|
self.assertEqual(len(data_frame_obs["var"]), 1838)
|
||||||
self.assertEqual(len(data_frame_obs["obs"]), 497)
|
self.assertEqual(len(data_frame_obs["obs"]), 497)
|
||||||
@@ -235,15 +173,7 @@ class UtilTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_data_single_gene(self):
|
def test_data_single_gene(self):
|
||||||
for axis in ["obs", "var"]:
|
for axis in ["obs", "var"]:
|
||||||
filter_ = {
|
filter_ = {"filter": {"var": {"annotation_value": [{"name": "name", "values": ["RER1"]}]}}}
|
||||||
"filter": {
|
|
||||||
"var": {
|
|
||||||
"annotation_value": [
|
|
||||||
{"name": "name", "values": ["RER1"]},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data_frame_var = self.data.data_frame(filter_["filter"], axis)
|
data_frame_var = self.data.data_frame(filter_["filter"], axis)
|
||||||
if axis == "obs":
|
if axis == "obs":
|
||||||
self.assertEqual(type(data_frame_var["var"][0]), int)
|
self.assertEqual(type(data_frame_var["var"][0]), int)
|
||||||
@@ -252,5 +182,5 @@ class UtilTest(unittest.TestCase):
|
|||||||
self.assertEqual(type(data_frame_var["obs"][0]), int)
|
self.assertEqual(type(data_frame_var["obs"][0]), int)
|
||||||
self.assertIsInstance(data_frame_var["var"][0], (list, tuple))
|
self.assertIsInstance(data_frame_var["var"][0], (list, tuple))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
ignore = E203
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.version_info[0:2] != (3, 6):
|
||||||
|
raise ImportError(
|
||||||
|
"cellxgene currently only supports python 3.6. Python 3.7 is known to fail; we will look at supporting "
|
||||||
|
"versions other than 3.6 in the future."
|
||||||
|
"See https://github.com/chanzuckerberg/cellxgene#conda-and-virtual-environments "
|
||||||
|
"for more help with installation."
|
||||||
|
)
|
||||||
|
|
||||||
with open("README.md", "rb") as fh:
|
with open("README.md", "rb") as fh:
|
||||||
long_description = fh.read().decode()
|
long_description = fh.read().decode()
|
||||||
@@ -8,7 +17,7 @@ with open("server/requirements.txt") as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="cellxgene",
|
name="cellxgene",
|
||||||
version="0.2.0",
|
version="0.4.0",
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
url="https://github.com/chanzuckerberg/cellxgene",
|
url="https://github.com/chanzuckerberg/cellxgene",
|
||||||
license="MIT",
|
license="MIT",
|
||||||
@@ -16,19 +25,24 @@ setup(
|
|||||||
author_email="cweaver@chanzuckerberg.com",
|
author_email="cweaver@chanzuckerberg.com",
|
||||||
description="Web application for exploration of large scale scRNA-seq datasets",
|
description="Web application for exploration of large scale scRNA-seq datasets",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type="text/markdown",
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
classifiers=(
|
classifiers=[
|
||||||
"Programming Language :: Python :: 3",
|
"Framework :: Flask",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
),
|
"Natural Language :: English",
|
||||||
entry_points={
|
"Operating System :: POSIX",
|
||||||
"console_scripts":
|
"Operating System :: Unix",
|
||||||
["cellxgene = server.cli.cli:cli"]
|
"Operating System :: MacOS :: MacOS X",
|
||||||
},
|
"Programming Language :: JavaScript",
|
||||||
extras_require=dict(
|
"Programming Language :: Python :: 3",
|
||||||
louvain=['python-igraph', 'louvain>=0.6'],
|
"Programming Language :: Python :: 3.6",
|
||||||
),
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
||||||
|
],
|
||||||
|
entry_points={"console_scripts": ["cellxgene = server.cli.cli:cli"]},
|
||||||
|
extras_require=dict(louvain=["python-igraph", "louvain>=0.6"]),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user