Compare commits

..
142 Commits
Author SHA1 Message Date
Arathi Mani bedbc87ed6 Release 0.16.7 2021-02-16 09:42:01 -08:00
Arathi Mani 01675a9db9 Bump version: 0.16.7-rc.0 → 0.16.7 2021-02-16 09:23:35 -08:00
Arathi Mani f91935ba0a Bump version: 0.16.6-rc.0 → 0.16.7-rc.0 2021-02-12 22:09:32 -08:00
Arathi Mani 491e643ae2 Bump version: 0.16.5-rc.0 → 0.16.6-rc.0 2021-02-12 22:07:25 -08:00
Arathi Mani 91bac7fef9 Bump version: 0.16.4-rc.0 → 0.16.5-rc.0 2021-02-12 22:04:47 -08:00
Arathi Mani 63631dad37 Bump version: 0.16.3-rc.0 → 0.16.4-rc.0 2021-02-12 22:02:16 -08:00
Arathi Mani 16b19defcf Bump version: 0.16.2-rc.0 → 0.16.3-rc.0 2021-02-12 21:45:13 -08:00
Arathi Mani 98eb91b005 Bump version: 0.16.1-rc.0 → 0.16.2-rc.0 2021-02-12 21:43:19 -08:00
Arathi Mani e2a3e41867 Bump version: 0.16.0 → 0.16.1-rc.0 2021-02-12 21:32:32 -08:00
Bruce Martin 036b5f8c0f Fix indexing bug in user-specified colors (#2051)
* repaint category value when color changes

* bug fix incorrect indexing of user colors

* add test for bug 2007

* lint
2021-02-08 18:00:03 -08:00
Bruce Martin e6281baa39 improve error message for OBO parse failure (#2053) 2021-02-05 12:19:40 -08:00
Bruce Martin 3aef21f76c alpha blending fix in scatterplots (#2033)
* explicitly specify alpha blending; rename NaN flag to accurately describe function

* fix incorret dest alpha blending function
2021-02-05 09:34:37 -08:00
Björn Grüning d821f0eac9 More cellxgene Galaxy information for the extensions (#2024)
This PR adds some more text and direct links to the cellxgene Galaxy section.

Thanks!


#### Reviewers
**Functional:** 

**Readability:** 

---

## Changes
- add
- remove
- modify
2021-02-03 14:38:54 -08:00
Marcus Kinsella 3c0b1d45db Fix deprecated np.unicode type (#2035)
Until numpy version 1.20.0, numpy.unicode was an alias for str in python3. In 1.20.0, it's fully deprecated and is an int. This is bad and breaks things. This commit drops the np.unicode alias and just uses str, as is advised here:
https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations
2021-02-03 09:22:27 -08:00
bmccandless 90a4ff7526 allow cellxgene datasets urls to have a trailing slash or not. (#2028)
#550
2021-01-20 15:19:13 -08:00
bmccandless d5ad823895 simple solution to the locust test problem (#2026)
In this solution, all the server requirements are installed.
This is a slightly overkill, but it avoid having to restructure
any of the server or test code to avoid unnecessary imports.

 #2019
2021-01-20 08:56:54 -08:00
bmccandless 0e48b335be update umap version (#2021)
There is an interface change in 0.5.0 which is not compatible with our
version of scanpy.
2021-01-13 13:01:46 -08:00
Ambrose J Carr e264724597 Add extension showcase to documentation (#1878) 2021-01-12 08:55:14 -05:00
bmccandless c7eb319817 P value and Log fold change not showing up for all DE results (#2016)
The HistogramFooter needs to distinguish between an undefined
value and a value of 0.  If the pvalAdj was 0, then the logFolChange
was previously not showing up.

 #1888
2021-01-08 09:44:17 -08:00
dependabot[bot] db559467a2 Bump ini from 1.3.5 to 1.3.7 in /client (#2000)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/npm/ini/commit/c74c8af35f32b801a7e82a8309eab792a95932f6"><code>c74c8af</code></a> 1.3.7</li>
<li><a href="https://github.com/npm/ini/commit/024b8b55ac1c980c6225607b007714c54eb501ba"><code>024b8b5</code></a> update deps, add linting</li>
<li><a href="https://github.com/npm/ini/commit/032fbaf5f0b98fce70c8cc380e0d05177a9c9073"><code>032fbaf</code></a> Use Object.create(null) to avoid default object property hazards</li>
<li><a href="https://github.com/npm/ini/commit/2da90391ef70db41d10f013e3a87f9a8c5d01a72"><code>2da9039</code></a> 1.3.6</li>
<li><a href="https://github.com/npm/ini/commit/cfea636f534b5ca7550d2c28b7d1a95d936d56c6"><code>cfea636</code></a> better git push script, before publish instead of after</li>
<li><a href="https://github.com/npm/ini/commit/56d2805e07ccd94e2ba0984ac9240ff02d44b6f1"><code>56d2805</code></a> do not allow invalid hazardous string as section name</li>
<li>See full diff in <a href="https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7">compare view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a href="https://www.npmjs.com/~isaacs">isaacs</a>, a new releaser for ini since your current version.</p>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ini&package-manager=npm_and_yarn&previous-version=1.3.5&new-version=1.3.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/chanzuckerberg/cellxgene/network/alerts).

</details>
2021-01-04 17:14:45 -08:00
dependabot[bot] 96362b0b98 Bump urijs from 1.19.2 to 1.19.5 in /client (#2012)
Bumps [urijs](https://github.com/medialize/URI.js) from 1.19.2 to 1.19.5.
- [Release notes](https://github.com/medialize/URI.js/releases)
- [Changelog](https://github.com/medialize/URI.js/blob/gh-pages/CHANGELOG.md)
- [Commits](https://github.com/medialize/URI.js/compare/v1.19.2...v1.19.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-04 16:45:23 -08:00
bmccandless 7599af252d Update compatibility test (#2009)
Add comment in requirements about h5py and anndata.

  #1963
2020-12-21 14:38:07 -06:00
Trent Smith f07e174a06 Fix PR Template (#1999) 2020-12-17 16:29:23 -08:00
Marcus Kinsella 46d02b1987 Handle schema v1.1.0 (#2002)
Correctly display datasets that follow schema version 1.1.0
2020-12-14 15:20:29 -08:00
Severiano Badajoz 0b91371ea8 check if viewport has changed and render accordingly (#1996) 2020-12-03 15:14:57 -08:00
Trent Smith c428242878 pull request template (#1997) 2020-12-03 12:13:31 -08:00
Isaiah Norton 9be086b1e8 Use sparse=True for from_pandas (#1994) 2020-12-02 13:39:19 -08:00
maniarathi 9a34264f92 Pin tiledb version to exclude 0.7.2 (#1993) 2020-11-30 11:54:33 -05:00
maniarathi 66e55ba59a Replace outdated locustio package with locust package. (#1992) 2020-11-30 10:11:57 -05:00
maniarathi f700fb7757 Setup codeql scanning for security and code quality analysis (#1991) 2020-11-24 21:53:10 -05:00
maniarathi ae310097dd Fix typo (#1990) 2020-11-24 12:23:21 -05:00
maniarathi ea70a35a01 Fixing locust scale tests for cellxgene loading apis and adding a Github Actions workflow to run the tests every Sunday. (#1988) 2020-11-24 09:08:57 -08:00
maniarathi 16718f392f Sorry! Undo-ing commit to main 2020-11-24 10:22:24 -05:00
maniarathi ad6b1928e4 Upgrade anndata and h5py 2020-11-24 10:21:24 -05:00
bmccandless 2cc02a84cb Convert float annotations if possible. (#1987)
* Convert float annotations if possible.

The client converts all arrays to floats.
If a category contains integer labels, and that category is copied, it will contains floats (e.g 1.0 instead of 1).
When that category is put back to the server, it fails in the tiledb code, which does not accept floats.
The solution is to convert a float category to integer, if possible.

  #1984

* updates
2020-11-20 17:01:53 -06:00
Marcus Kinsella f77038ad58 Permit other keys in the cxguser cookie (#1982) 2020-11-18 17:58:33 -08:00
Timmy Huang 2cf55ab819 thuang-compress-annotation (#1980)
* thuang-compress-annotation

* compress test

* use zlib.decompress directly
2020-11-17 14:05:07 -08:00
bmccandless 095db02439 Remove deprecated health endpoint (#1943)
NOTE:  do not push to main until the new path has been terraformed into all the environments.

 #1846
2020-11-13 10:49:11 -08:00
dependabot[bot]andSeveriano Badajoz 85fc000418 Bump dot-prop from 4.2.0 to 4.2.1 in /client (#1954)
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2020-11-09 22:05:34 -08:00
Isaiah Norton 34742b9a94 Add link to dev_docs in Contributing section (#1957) 2020-11-09 21:57:00 -08:00
06b88cda20 do not update GPU buffers if data has not changed (#1967)
Co-authored-by: maniarathi <mani.arathi@gmail.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2020-11-09 10:41:39 -08:00
bmccandless 23714bc9f8 Fix bug that occurs when all categories are removed. (#1974)
Previously if the user remove all annotations, the code would still generate a tiledb uri
in the write_labels call, and add that to the database.  A tiledb array would not be written in this case.
When the read_labels was then called, it would find the entry in the database, attempt to open
the tiledb array, then fail.

The patch here will set the tiledb_uri to the empty string if all categories are removed.
When read_labels is called, it will see the empty uri and return None.
Furthermore, if the database does have a tiledb_uri that does not exist, or cannot be read,
then the code will now log a warning, and return None (instead of throwing an exception,
which results in a server error).

 #1932
2020-11-06 18:04:14 -08:00
Marcus Kinsella e892e64685 Convert HGNC ids to their symbol (#1972)
There are entries in some var indexes like HGNC:18790. We'd like to convert that to its symbol, NSG1.
2020-11-06 09:27:49 -08:00
Marcus Kinsellaandmaniarathi 39a1124c35 Fix manifest to include schema definitions (#1965)
🤦

Co-authored-by: maniarathi <mani.arathi@gmail.com>
2020-11-05 09:41:05 -08:00
Bruce Martin b5856ff9bc fix accidental state error (#1966) 2020-11-04 18:46:04 -08:00
Severiano Badajoz d87551ce5e fix version check (#1968)
Inverted the schema version check so that we check that it IS 1.0.0.

![image](https://user-images.githubusercontent.com/8716829/98184898-5c897b00-1ec0-11eb-89f7-c293dfbe8439.png)
2020-11-04 17:43:41 -08:00
Severiano Badajozandmaniarathi a5c9ae2432 Data drawer final round of prod QA fixes (#1955)
* add long title

* add organism to Dataset Metadata and create headers

* begin HTMLTable for metadata

* switch out truncating for scrolling

* add optional chaining to redux state mapping

Co-authored-by: maniarathi <mani.arathi@gmail.com>
2020-11-04 09:43:43 -08:00
Marcus Kinsella 78176f9711 Add schema subcommand (#1939)
Add the `cellxgene schema apply` and `cellxgene schema validate` subcommands.

The first takes an h5ad file and a yaml with config information and produces a new h5ad that follows the cellxgene data integration schema.

The second takes an h5ad and checks if it follows the schema version written into its metadata.

Both are currently marked as "experimental" as the primary intended users are still at CZI.
2020-11-02 08:26:37 -08:00
bmccandless b9e132a00c Updates due dependency version changes. (#1960)
* Updates due dependency version changes.

h5py recently changes and now values once returned as str are now returned as bytes.
This would have caused a much larger change, so instead the version is restricted to <3.0.0.

This caused the bulk of the testing failues.
A few other changes were needed to make a few other tests pass.

 #1959
2020-11-01 12:36:38 -08:00
Madison Dunitz 3b6c46ba86 Fix dependency issues in compatibility tests (#1951)
* update reqs

* pin scanpy

* merge in fix for race conditions
2020-10-30 10:47:12 -05:00
bmccandless 6a1e5f71be fix race condition in test_oauth (#1956) 2020-10-29 11:05:23 -07:00
Severiano BadajozandMadison Dunitz 727af83152 remove conditional rendering cases from color legend (#1952)
* Revert "Remove Continuous vars with 1 value from histogram, add to info drawer (#1927)"

This reverts commit 242546371b.

* remove conditional rendering cases

* ignore pointer events

Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
2020-10-28 15:35:28 -07:00
Severiano Badajoz 924b518492 Revert "Remove Continuous vars with 1 value from histogram, add to info drawer (#1927)" (#1953)
This reverts commit 242546371b.
2020-10-27 17:12:13 -07:00
Madison Dunitz 946a910ef4 Fix compatibility test (#1948)
* update anndata version and warning about version

* update compatibility tests
2020-10-26 17:23:55 -05:00
bmccandless 7e9353c5f1 Fix bug in oauth. (#1949)
* Fix bug in oauth.

The error checking was too specific, and missed a case.
Make the error checking catch all exceptions.

  #1947

* Add logging when the cookie cannot be processed
2020-10-26 09:39:06 -07:00
bmccandless c106ebc525 smnall fix to the test suite. (#1944)
I noticed a few tests failed when run individually, but not as a suite.

 #1942
2020-10-23 15:14:31 -07:00
Severiano Badajoz 2fa206f2ad Add token invalidation tests to oauth tests (#1941)
* add tests

* run black

* run black and add disclaimer that tweaked errors on server

* lint

* change to get so it will return None

* tweak existing token instead of new one

* Trigger

* token is dict

* jsonify dict before encoding

* json dump instead of jsonify

* encode into bytes object

* use correct id token

* decode byte to string
2020-10-23 14:51:48 -07:00
bmccandless f41a023418 Minor changes to eb server to use Docker (#1938)
part of #1866
2020-10-22 17:04:08 -07:00
maniarathi 9793398737 Add in missing previous crossfilter which was causing the re-embedding feature to fail. (#1936) 2020-10-21 09:16:13 -07:00
maniarathi 377e4bccaa Remove errornous checking for converting float64 to float32. In reality the slight difference by downcasting is totally fine. (#1935) 2020-10-19 10:31:36 -07:00
bmccandlessandMadison Dunitz 6a741956e1 Update readme for eb server. (#1928)
* Update readme for eb server.

Update the README with new way of handling secrets.
Update portions that were out of date.
Add a section for Authentication and a placeholder for User Annotations.

Also remove an obsolete function that processes the AWS secrets.

  #1522

Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
2020-10-16 14:02:05 -07:00
Severiano Badajoz c9f9549118 Adopt JS standards once userinfo data is in frontend (#1930) 2020-10-16 11:59:28 -07:00
Madison Dunitz 242546371b Remove Continuous vars with 1 value from histogram, add to info drawer (#1927)
* remove single val continous metadata from histogram, add to info drawer

* refactor to save singleContinuous values in state

* fix edge case, single continuous values reappeard in rsb when clipped
2020-10-14 12:46:24 -05:00
maniarathi 798976e4c1 Fix custom color handling (#1929) 2020-10-13 15:47:56 -07:00
maniarathi 5325495123 Speed up dataset drawer rendering (#1926) 2020-10-12 11:20:58 -07:00
Severiano Badajozandczimergebot beb46bf3df add and check system arg to state auth type in e2e test(#1924)
* add and check system arg to state auth type

* add tolower

Co-authored-by: czimergebot <35308261+czimergebot@users.noreply.github.com>
2020-10-09 12:42:02 -06:00
maniarathi 86ff48ae36 Revert "Allow columns encoded in float64 to be rendered as part of continuous value histograms. (#1905)" (#1925)
This reverts commit b048fd8d9a.
2020-10-09 10:09:32 -07:00
Timmy Huang c01a2c72b6 thuang-1840-authn-prompt (#1911) 2020-10-08 16:57:53 -07:00
Severiano Badajoz 6677d0de56 disable profile picture (#1923) 2020-10-08 12:16:02 -06:00
Severiano Badajoz c4c48b9a57 create e2e test for auth buttons (#1907)
This PR adds a few helpful additions regarding authentication.

Changes:
* e2e tests are now run on test_oauth via a passed config.yaml 
* node dev server correctly handles `/login` and `/logout` endpoints to make developing for auth easier
* Introduced auth e2e tests to check that buttons display and work
2020-10-08 10:02:40 -07:00
bmccandless b5ec43c4b1 Add a function to check the configuration for errors. (#1919)
This can be used as a sanity check before a deployment:

  chanzuckerberg/single-cell#63
2020-10-08 08:44:09 -07:00
bmccandless 6c1756f852 Enhance the AppConfig with external config sources. (#1904)
* Enhance the AppConfig with external config sources.

The external config sources are currently environment variables
and AWS secrets manager.

The config file can be augmented with a section describing how
environmen variables and secrets can update config parameters.

benefits:
 - it will enable the config to draw from more than one secret.  This is useful
   for shared secrets between cellxgene and data portal, as well as auth0 secrets.
 - it will make it very straightforward to check the config before a deployment.

 Part of #1859
2020-10-07 15:38:42 -07:00
Severiano BadajozandTimmy Huang 1c4c501c43 Auth UI tweaks (#1915)
* remove auth buttons and dataset info from info menu

* add auth buttons to menubar

* remove auth from top left

* new auth buttons

* move infomenu to lsb dir

* styling fixes

* feedback

* more feedback

Co-authored-by: Timmy Huang <thuang@chanzuckerberg.com>
2020-10-07 15:02:55 -07:00
bmccandless cf77a8da9e Add "picture" to the /userinfo endpoint. (#1914)
* Add "picture" to the /userinfo endpoint.

This may be null or a URL.
 add picture for the test authentication method
2020-10-07 12:17:23 -07:00
Madison Dunitz eb108feb37 Performance test annotations (#1908)
* make testing plan

* create annotaions sets for different num categories/dataset size

* annotation creation testing

* create scale and perf tests for annotations

* create make commands for tests

* get cell count if not set in test_datasets dict
2020-10-07 12:36:02 -05:00
Leslie b386ca3425 Move link to cellxgene data portal higher (#1909) 2020-10-06 13:16:59 -07:00
maniarathi b048fd8d9a Allow columns encoded in float64 to be rendered as part of continuous value histograms. (#1905) 2020-10-06 12:59:07 -07:00
Leslie 3718e894ed Removed legacy landing page and updated cxg readme (#1897) 2020-10-02 12:10:55 -07:00
evanbiederstedt 1f9bba6f00 readme correction (#1896) 2020-10-01 22:44:59 -04:00
Timmy Huang 8bd4cbd1e5 1807-authN-smoke-test (#1898)
This PR does the following:

1. Add `login` and `logout` helper functions in `client/__tests__/e2e/cellxgeneActions.js`
2. Add conditional AuthN integration test in `client/__tests__/e2e/e2e.test.js`. The test will only run if env variable `TEST_AUTH_INTEGRATION` is `"true"`, which is only set in `single-cell-infra`'s Github Action flow. Corresponding PR [here](https://github.com/chanzuckerberg/single-cell-infra/pull/198)
2020-10-01 12:29:59 -07:00
dependabot[bot]andSeveriano Badajoz e6c996ca93 Bump bl from 4.0.2 to 4.0.3 in /client (#1810)
Bumps [bl](https://github.com/rvagg/bl) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/rvagg/bl/releases)
- [Commits](https://github.com/rvagg/bl/compare/v4.0.2...v4.0.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2020-09-30 15:24:51 -07:00
Timmy Huang 99152dd5aa add-menu-test-id (#1895) 2020-09-30 12:53:51 -07:00
Colin Megill 04a3c3c6b6 Partial fix for 1830 (#1863)
* Remove door icon from log in button
* Move log in and info buttons from the top bar to in line with the cellxgene icon and dataset name
* Hover over on login button should say "Log in to cellxgene"
* Show email

closes #1830
2020-09-30 11:45:10 -07:00
Madison Dunitz 998fa4762d run black formatter on repo (#1891)
* add black to lint make cmd

* add black dependency to installation to push test pipeline
2020-09-30 11:16:13 -05:00
Madison Dunitzandbmccandless 2ffe5ffcae remove AppFeature and all references to it in the code/tests (#1893)
* remove AppFeature and all references to it in the code/tests

Co-authored-by: bmccandless <bmccandless@chanzuckerberg.com>
2020-09-29 18:31:59 -05:00
maniarathi 4aabb8e092 Make sure there are more than 1 values in a category before rendering it (#1871) 2020-09-29 15:32:21 -07:00
Severiano Badajozandczimergebot 7bee09cd16 Add blueprint eslint plugin (#1892)
* add bp3 eslint plugin

* first eslint runthrough + manual changes

* small fixes

* update snapshots

* update h1 to h4

Co-authored-by: czimergebot <35308261+czimergebot@users.noreply.github.com>
2020-09-29 15:00:56 -07:00
Madison Dunitz af3c6e1d8e config refactor (#1854)
* split out config

* add tests for base and app config, refactor client config out of app config

* refactor default config retrieval

* create config test class and helper functions

* move default_config into server to fix import issue
2020-09-29 16:42:46 -05:00
bmccandless 1145f61c78 auth: logging out should keep the user on the same page (#1877)
previous behavior is that logout would redirect to the index page.
2020-09-29 13:42:24 -07:00
maniarathi 863ca8be03 Fix license years and add CZI (#1882) 2020-09-28 16:44:56 -07:00
Severiano Badajoz 21dfdb91a9 skip user annos when building dataset metadata (#1881) 2020-09-28 13:17:14 -07:00
Severiano Badajoz 374bb11279 Handle case where new drag starts while existing lasso is not finished (#1864)
* handle case where new drag starts while existing lasso is not finished

* flip variable
2020-09-28 10:34:47 -07:00
bmccandless 3e2d7174fd Add user email to the userinfo response (#1862)
We are planning to display the user's email address in the front end.

 #1830
2020-09-23 11:46:56 -07:00
bmccandless a817a94eec Bug reading the config file. (#1857)
The config file had a bug where it expected both a "server" and "dataset" section.
If one didn't exist, then it would raise an exception.
It should use the default server config or the defaul dataset config in those cases.
Added a test case that would have caught this.
2020-09-18 19:05:14 -07:00
Severiano Badajoz 210042814f Info Drawer format adjustments (#1853)
This PR tweaks the look and feel of the info drawer in response to QA from @signechambers1
2020-09-18 13:16:28 -07:00
bmccandless 14fbe0aa77 Fix the /health endpoint (#1847)
* Fix the /health endpoint

 #1846

Keep both the old and new locations until the deployments are upgraded.
2020-09-17 17:14:08 -07:00
bmccandless 25c272ae8e minor fix to auth redirect (#1845)
The previous version added and extra "/" to the url after login:
e.g:  https://cellxgene.dev.single-cell.czi.technology/d/pbmc3k.cxg//
2020-09-16 17:37:52 -07:00
maniarathi a63bf9d5a3 Change psycopg to be binary (#1842) 2020-09-16 14:46:59 -07:00
Severiano Badajoz 3e9cb0265e Fix InfoFormat parameter checking (#1831)
Went through and ensured that undefined/null values were caught and handled correctly in render functions.  Also documented some of the more complicated functions.

---
Closes #1825
2020-09-15 12:03:59 -07:00
Madison Dunitz 4f339e89b1 dont cache schema (#1836) 2020-09-15 11:20:00 -05:00
maniarathi 9fac6849a3 Fix import of anndata from master so that there aren't issues with scanpy version checking. (#1834) 2020-09-14 17:19:24 -07:00
bmccandless 342a9d774c app config bug fix: (#1833)
* app config bug fix:

When reading a config file that included per_dataset_config,
the dataroot specializations were applied, but not the default config.
This PR fixes that and also includes a test for this case.
2020-09-14 13:15:47 -07:00
bmccandless 6a7ae8bc8e Fixes from frontend/backend url separation (#1829)
* Fixes from frontend/backend url separation

This fixes the CORS and CSP headers.

Also, in thie commit, I removed the cors_supports_credentials config parameter,
which was recently introduced.
Instead, the logic determines the need to use CORS headers if the
web_page_url is set.

 #1778
2020-09-12 10:56:31 -07:00
maniarathi 4b240920e2 Pass in the previous crossfilter when creating a new annomatrix for a switched embedding in order to retain the previous selection of cells. (#1832)
* Pass in the previous crossfilter when creating a new annomatrix for a switched embedding in order to retain the previous selection of cells.

* Address Bruce's PR comment
2020-09-12 10:32:00 -07:00
bmccandless a7a4580944 separate backend base url from frontend (#1819)
* separate backend base url from frontend

This is needed for auth, and to support a different location for the backend api server,
than the frontend.

 part of chanzuckerberg/cellxgene#1778

new server config parameters:   app__api_base_url,   app__web_base_url

Also changed api_base_url in the oauth config section to "oauth_api_base_url" to
be less confusing with the app's api_base_url

Other minor changes:

changed how the jwt decode options are handled.
Previously they needed to be set in a test case, and there was some extra logic to handle that.
Now they are handled through comfig parameters, which makes it more general.

Also, add a feature to set the CORS support credentials, which seems
to be necessary for the backend/frontend separation, at least when run
locally.  This part is sort of experimental, and may be removed or changed later.
2020-09-11 09:50:16 -07:00
bmccandless 3f20f4a1f4 Change modify upgrade message to print to stderr instead of stdout (#1827)
When generating a config file, you can do this:
 > cellxgene launch --dump-default-config > myconfig.yaml

And then modify the myconfig.yaml.

However, if an upgrade is available then you would get extra lines in the yaml
file, which are not yaml code:

  There's a new version of cellxgene available (0.16.4)!
  To upgrade, run the following: pip install --upgrade cellxgene

To solve this problem, the upgrade messages are sent to stderr instead,
so they will appear on the screen and not in the config file.

Alternatives:
  One workaround is "cellxgene --no-upgrade-check launch --dump-default-config > myconfig.yaml"

But that's a bit verbose and not user friendly.

The way we've setup the upgrade check to be separate and before the launch sub command,
makes other code changes more involved.

 #1826
2020-09-11 09:24:25 -07:00
Severiano Badajoz 5583e91392 Pull config values into dataset overview drawer (#1814)
This PR adds multiple data to the dataset overview drawer provided by the config endpoint and formats them accordingly.  The appearance of this new data is contingent on `dataPortalProps.corpora_schema_version === "1.0.0"`

For QA launch cellxgene with a remixed dataset and click on the button in the upper left-hand corner or the updated button in the info menu.


![image](https://user-images.githubusercontent.com/8716829/92670435-de966280-f2c8-11ea-87f1-8591c959a586.png)


~~Review opening is blocked by merge of #1805~~

---

Closes #1319
2020-09-10 16:41:05 -07:00
Severiano Badajoz 89b68723cc Create dataset info drawer (#1805)
* create infoDrawer

* create read/writes to redux store

* reimplement reducer that vanished

* remove aboutURL stuff from title

* add formatting and style

* s/length/size and make metadata items list items

* remove comment

* remove empty singletons

* refactor into async react component

* Clean up skeleton

* swap out for loop for map

* add comment

* replace placeholder

* switch ternary for `&&`

* event handling fixes and PR feedback

* add button and move click handler to button

* ditch empty categories

* move drawer button handling to redux

* remove categorical move note

* PR feedback from colin

* update snapshot

* remove hover state
2020-09-09 17:55:43 -07:00
Ambrose J Carr 5781879da5 remove core team section (#1798) 2020-09-02 20:22:31 -04:00
bmccandless 0a27b2923a Add error message and exit if reembeddings is enabled and scanpy is n… (#1812)
* Add error message and exit if reembeddings is enabled and scanpy is not installed

  fixes #1811
2020-09-02 15:35:50 -07:00
Severiano Badajoz 437fd5feda Correctly check if mini histograms shouldn't be rendered (#1809)
* ensure that function returns a boolean value

* change function used to check if mini histogram should not render
2020-09-01 16:45:50 -07:00
bmccandless 54b42607ae Update the location of deployment assets for the eb server (#1806)
put deploy scripts in /static/cellxgene/deploy instead of /static/deploy

  fixed chanzuckerberg/corpora-data-portal#558
2020-08-31 18:26:35 -07:00
maniarathi ed865e9a57 Update the release process for community release to include release candidate versioning (#1802) 2020-08-31 16:16:21 -07:00
bmccandless f8cdb12892 Fix frontend mishandling of null userinfo (#1795)
* Fix frontend mishandling of null userinfo

If the authentication is disabled, the userinfo endpoint returns null.
This case needs to be handled.

 #1780

* Small fix for handling refesh tokens in auth
2020-08-26 13:01:50 -07:00
bmccandless ab1b9368a0 fix pca call in reembeddings (#1793)
This had the wrong dim passed into n_comps,
and so failed when the number of genes was less than 50.
2020-08-25 17:21:39 -07:00
Severiano Badajoz 0a10b3ec2a sort object keys to our specification before generating user colormap (#1792) 2020-08-25 12:25:55 -07:00
Prete eb05d1cb5c Update Dockerfile (#1775)
* Update Dockerfile

- Update Ubuntu Focal (20.04)
- Add `DEBIAN_FRONTEND=noninteractive` to prevent dialog boxes during installation

* Changed 'pip3 install --upgrade pip' to 'python3 -m pip install --upgrade pip' as described here https://github.com/pypa/pip/issues/5599
2020-08-25 09:37:38 -07:00
Timmy Huang 9a40b28172 thuang-fix-static-asset-font (#1791)
This seems to fix the font URL path, at least locally for both `:3000` and `:5005`

<img width="1296" alt="Screen Shot 2020-08-24 at 4 01 07 PM" src="https://user-images.githubusercontent.com/6309723/91106044-8a338780-e626-11ea-885f-e5c268f3ecf0.png">

<img width="1377" alt="Screen Shot 2020-08-24 at 4 01 28 PM" src="https://user-images.githubusercontent.com/6309723/91106047-8dc70e80-e626-11ea-95b0-62e47cc6303f.png">
2020-08-24 17:05:46 -07:00
65ea1b673f Dunitz 1685 hosted annotations (#1789)
* save tiledb array to s3, dont cache user annotations

* Add option to disable annotation filename prompt (#1787)

Co-authored-by: Madison Dunitz <dunitzm@gmail.com>

* set tiledb default context in cxg_adaptor

Co-authored-by: maniarathi <arathi.mani@chanzuckerberg.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2020-08-24 18:26:08 -05:00
maniarathi 5dfe0043c3 Serves static assets from each dataset root URL and switch the publicPath to be a relative path. (#1786) 2020-08-22 10:04:40 -07:00
maniarathi bc150a8469 Fixing bugs in cxg conversion tool (#1782) 2020-08-22 09:53:59 -07:00
maniarathi a5c9ffa880 When reading annotations from tiledb, check if the values are byte literals and if so, decode them. Also pin s3f3 to 0.4.2. (#1788) 2020-08-22 09:42:11 -07:00
Snyk bot fae9ac9382 Upgrade lodash from 4.17.15 to 4.17.20 (#1759)
<h3>Snyk has created this PR to fix one or more vulnerable packages in the `npm` dependencies of this project.</h3>

![merge advice](https://app.snyk.io/badges/merge-advice/?package_manager=npm&package_name=lodash&from_version=4.17.15&to_version=4.17.20&pr_id=31842747-752f-42e7-b1f2-8fa3f51d0e21&visibility=true&has_feature_flag=false)



#### Changes included in this PR

- Changes to the following files to upgrade the vulnerable dependencies to a fixed version:
    - client/package.json
    - client/package-lock.json



#### Vulnerabilities that will be fixed
##### With an upgrade:
Severity                   | Priority Score (*)                   | Issue                   | Breaking Change                   | Exploit Maturity
:-------------------------:|-------------------------|:-------------------------|:-------------------------|:-------------------------
![high severity](https://res.cloudinary.com/snyk/image/upload/w_20,h_20/v1561977819/icon/h.png "high severity")  |  **776/1000**  <br/> **Why?** Recently disclosed, Has a fix available, CVSS 9.8  | Prototype Pollution <br/>[SNYK-JS-LODASH-590103](https://snyk.io/vuln/SNYK-JS-LODASH-590103) |  No  | No Known Exploit 

(*) Note that the real score may have changed since the PR was raised.











Check the changes in this PR to ensure they won't cause issues with your project.



------------



**Note:** *You are seeing this because you or someone else with access to this repository has authorized Snyk to open fix PRs.*

For more information:  <img src="https://api.segment.io/v1/pixel/track?data=eyJ3cml0ZUtleSI6InJyWmxZcEdHY2RyTHZsb0lYd0dUcVg4WkFRTnNCOUEwIiwiYW5vbnltb3VzSWQiOiIzMTg0Mjc0Ny03NTJmLTQyZTctYjFmMi04ZmEzZjUxZDBlMjEiLCJldmVudCI6IlBSIHZpZXdlZCIsInByb3BlcnRpZXMiOnsicHJJZCI6IjMxODQyNzQ3LTc1MmYtNDJlNy1iMWYyLThmYTNmNTFkMGUyMSJ9fQ==" width="0" height="0"/>
🧐 [View latest project report](https://app.snyk.io/org/cellxgene/project/9195ddb9-6feb-469e-ad47-f5dc24c811fe)

🛠 [Adjust project settings](https://app.snyk.io/org/cellxgene/project/9195ddb9-6feb-469e-ad47-f5dc24c811fe/settings)

📚 [Read more about Snyk's upgrade and patch logic](https://support.snyk.io/hc/en-us/articles/360003891078-Snyk-patches-to-fix-vulnerabilities)

[//]: # (snyk:metadata:{"prId":"31842747-752f-42e7-b1f2-8fa3f51d0e21","dependencies":[{"name":"lodash","from":"4.17.15","to":"4.17.20"}],"packageManager":"npm","projectPublicId":"9195ddb9-6feb-469e-ad47-f5dc24c811fe","projectUrl":"https://app.snyk.io/org/cellxgene/project/9195ddb9-6feb-469e-ad47-f5dc24c811fe?utm_source=github&utm_medium=fix-pr","type":"auto","patch":[],"vulns":["SNYK-JS-LODASH-590103"],"upgrade":["SNYK-JS-LODASH-590103"],"isBreakingChange":false,"env":"prod","prType":"fix","templateVariants":["updated-fix-title","priorityScore","merge-advice-badge-shown"],"priorityScoreList":[776]})
2020-08-20 11:03:30 -07:00
bmccandless 924aaf9aef Allow user_annotations in the eb app (#1781) 2020-08-18 17:13:57 -07:00
bmccandless 950be4426d Handle the refresh token in oauth authentication (#1766)
* Handle the refresh token in oauth authentication

If the token has expired, then it can be refreshed to get a new token.
This is automatically handled by the server without the client being aware.

Also in the PR:
  - refactor the auth_oauth.py file to more simply handle the save/restore of the token,
    and the refresh token
  - added an end2end test for oauth, which also tests refresh.

* adding python-jose and Authlib to requirements-dev.txt

They are needed in the auth_oauth test
2020-08-18 14:41:15 -07:00
maniarathi 053f39d49e Cleaning up one script that makes use of the non-existent cxgtool. (#1765) 2020-08-17 18:40:26 -07:00
maniarathi 994c20c094 Move cxgtool into CLI and modularize conversion functions (#1701) 2020-08-17 17:28:29 -07:00
Severiano Badajoz 1acb8e4a6f Remove support for non-chromium Edge (#1761)
* bump browserlist Edge to 79+

* bump edge version on unsupported browser page
2020-08-17 16:51:58 -07:00
bmccandlessandColin Megill 298924fef5 Separate userinfo from the config endpoint (#1728)
* Separate userinfo from the config endpoint

previously information about if the user was logged in and their username
was part of the config endpoint.
However, the config endpoint was previously static, and has a cache control.
Rather than not caching the config, a new endpoint called "userinfo"
is created to handle that information.

The config endpoint still has the non-changing part of the authentication:

  config:
    authentication:
        requires_client_login:  True/False
        login: <uri to login endoint if requires_client_login is True>
        logout: <uri to logout endoint if requires_client_login is True>

The userinfo endpoint returns this information:

  userinfo:
    is_authenticated:  True/False
    username: <string if is_authenticated>

if authentication is not enabled then the config does not have an authentication key,
and userinfo returns None.

Also in the PR are a few minor code improvements and bug fixes

Co-authored-by: Colin Megill <colinmegill@gmail.com>
2020-08-17 13:41:03 -07:00
Colin Megill 4ad9f5875a xx, yy (#1754) 2020-08-17 11:55:49 -04:00
maniarathi 508889f74b Refactoring cxg utility classes in preparation for CXG conversion tooling (#1739) 2020-08-14 16:51:13 -07:00
Madison Dunitz b034055c35 update to get_secrets_key (#1755)
* raise exception when get_secrets fails, get db_uri and set as a default_dataset_config var

* log as info not an error
2020-08-14 18:17:21 -05:00
maniarathi 263e893b30 Revert "Patching (#1744)" (#1748)
This reverts commit 6848f7a8b2.
2020-08-14 11:22:39 -07:00
Madison Dunitz 6a82030558 remove db_uri secret (#1751)
* remove db_uri secret

* add test to catch bug in future
2020-08-14 12:38:46 -05:00
Severiano Badajoz 018f653ec6 Sunset Heroku support (#1740)
* remove experimental heroku

* add aiohttp for dataset loading via url

* Add heroku deprecation section to docs

* remove Heroku related files from root
2020-08-14 10:22:29 -07:00
bmccandless 905308e09f Move psycopg2==2.7.7 from requirements.txt to requirements-dev.txt (#1747) 2020-08-13 21:20:13 -07:00
bmccandless 3c04529523 Fix error message when datapath and dataroot are not provided (#1746)
* Fix error message when datapath and dataroot are not provided

Previously:
$ cellxgene launch
cellxgene] Starting the CLI...
AttributeError: 'NoneType' object has no attribute 'startswith'

With this fix:
$ cellxgene launch
[cellxgene] Starting the CLI...
Error: missing datapath

* lint
2020-08-13 21:10:02 -07:00
Madison Dunitz 2689d8d2c0 Create hosted user annotations [1685] (#1726)
* add function to retrieve latest annotation from db, db updates

* read and write tiledb arrays

* adding tests
2020-08-13 19:07:17 -05:00
Severiano Badajoz 1c4bb84f35 Properly generate hash and provide how-to (#1745)
* properly generate hash and provide how-to

* Add link to this PR
2020-08-13 16:50:54 -07:00
maniarathi 6848f7a8b2 Patching (#1744) 2020-08-13 14:36:42 -07:00
Timmy Huang dda530a67c add-GHActions-timeout-for-smoke-tests (#1743)
We need to explicitly set timeout for smoke tests, since GH Action's default is 360 mins (6 hours 😱 )

https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes

Thank you!
2020-08-13 13:18:01 -07:00
Severiano Badajoz a23aaa131d regenerate hash and fix url (#1742)
The script hash had a typo in it and was incorrectly generated.  The URL in the `img-src` directive also did not need to be encased in single-quotes.

Reviewers please double-check my hash generation against the inline-script here: https://github.com/chanzuckerberg/cellxgene/blob/main/client/configuration/webpack/obsoleteHTMLTemplate.html
2020-08-13 11:17:13 -07:00
211 changed files with 11862 additions and 3527 deletions
+12 -1
View File
@@ -1,5 +1,16 @@
[bumpversion] [bumpversion]
current_version = 0.16.0 current_version = 0.16.7
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<prerel>rc)\.(?P<prerelversion>\d+))?
serialize =
{major}.{minor}.{patch}-{prerel}.{prerelversion}
{major}.{minor}.{patch}
[bumpversion:part:prerel]
optional_value = release
values =
rc
release
[bumpversion:file:setup.py] [bumpversion:file:setup.py]
search = version="{current_version}" search = version="{current_version}"
+67
View File
@@ -0,0 +1,67 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Scan"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '0 8 * * *'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+5 -15
View File
@@ -25,10 +25,11 @@ jobs:
cellxgene-main-with-python-and-anndata-versions: cellxgene-main-with-python-and-anndata-versions:
name: python versions x anndata versions name: python versions x anndata versions
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8] python-version: [3.6, 3.7] # As of Oct 2020 Anndata is not compatible with 3.8
anndata-version: [0.6.22.post1, 0.7.1] anndata-version: [0.7.0, 0.7.1, 0.7.2, 0.7.3, 0.7.4, 0.7.5]
test-suite: [smoke-test, smoke-test-annotations] test-suite: [smoke-test, smoke-test-annotations]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -67,11 +68,6 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
path: cellxgene path: cellxgene
- name: Checkout tools repo
uses: actions/checkout@v2
with:
repository: theislab/anndata
path: anndata
- name: Install dependencies - name: Install dependencies
run: | run: |
cd cellxgene cd cellxgene
@@ -82,7 +78,7 @@ jobs:
# 2. install cellxgene # 2. install cellxgene
pip install --upgrade cellxgene pip install --upgrade cellxgene
# 3. install anndata # 3. install anndata
cd ../anndata && pip install -e . pip install git+https://github.com/theislab/anndata
- name: Tests - name: Tests
run: cd cellxgene && make unit-test ${{ matrix.test-suite }} run: cd cellxgene && make unit-test ${{ matrix.test-suite }}
@@ -102,17 +98,11 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
path: cellxgene path: cellxgene
- name: Checkout tools repo
uses: actions/checkout@v2
with:
repository: theislab/anndata
path: anndata
- name: Install dependencies - name: Install dependencies
run: | run: |
cd cellxgene cd cellxgene
sed -i -E 's/^anndata[>=]=[0-9]+.[0-9]+.[0-9]+$/anndata/g' server/requirements.txt sed -i -E 's/^anndata[>=]=[0-9]+.[0-9]+.[0-9]+$/anndata/g' server/requirements.txt
make pydist install-dist dev-env make pydist install-dist dev-env
cd ../anndata pip install git+https://github.com/theislab/anndata
pip install -e .
- name: Tests - name: Tests
run: cd cellxgene && make unit-test ${{ matrix.test-suite }} run: cd cellxgene && make unit-test ${{ matrix.test-suite }}
+4 -1
View File
@@ -31,9 +31,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install flake8 pip install flake8
pip install black
cd client cd client
npm install npm install
- name: Lint with flake8 - name: Format with black and lint with flake8
run: | run: |
make lint-server make lint-server
- name: Lint src with eslint - name: Lint src with eslint
@@ -73,6 +74,7 @@ jobs:
smoke-tests: smoke-tests:
runs-on: macos-latest runs-on: macos-latest
timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
@@ -102,6 +104,7 @@ jobs:
smoke-tests-annotations: smoke-tests-annotations:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up Python 3.7 - name: Set up Python 3.7
+30
View File
@@ -0,0 +1,30 @@
name: "Scale test cellxgene APIs for initial loading"
on:
schedule:
- cron: "0 0 * * Sun"
jobs:
locust-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install dependencies
run: |
pip install -r server/test/locust/requirements-locust.txt
- name: Dev Scale Test
run: |
locust -f server/test/locust/locustfile.py --headless -u 30 -r 10 --host https://api.cellxgene.dev.single-cell.czi.technology/cellxgene/e/ --run-time 5m 2>&1 | tee locust_dev_stats.txt
- name: Slack success webhook
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
DEV_STATS=$(tail -n 61 locust_dev_stats.txt)
DEV_MSG="\`\`\`CELLXGENE EXPLORER DEV SCALE TEST RESULTS: ${DEV_STATS}\`\`\`"
curl -X POST -H 'Content-type: application/json' --data "{'text':'${DEV_MSG}'}" $SLACK_WEBHOOK
+2 -1
View File
@@ -4,7 +4,8 @@ ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev python3-requests && \ apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev python3-requests python3-aiohttp && \
python3 -m pip install --upgrade pip && \
pip3 install cellxgene pip3 install cellxgene
ENTRYPOINT ["cellxgene"] ENTRYPOINT ["cellxgene"]
+2 -2
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013 Copyright (c) 2017-2020 Chan Zuckerberg Initiative
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in
@@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+2
View File
@@ -3,3 +3,5 @@ recursive-include server/common/web/static *
include server/requirements.txt include server/requirements.txt
include server/requirements-prepare.txt include server/requirements-prepare.txt
include server/converters/schema/hgnc_complete_set.txt.gz
include server/converters/schema/schema_definitions/*
+37 -15
View File
@@ -82,8 +82,9 @@ fmt-py:
lint: lint-server lint-client lint: lint-server lint-client
.PHONY: lint-server .PHONY: lint-server
lint-server: lint-server: fmt-py
flake8 server flake8 server --per-file-ignores='server/test/fixtures/dataset_config_outline.py:F821 server/test/fixtures/server_config_outline.py:F821 server/test/performance/scale_test_annotations.py:E501'
.PHONY: lint-client .PHONY: lint-client
lint-client: lint-client:
@@ -99,22 +100,32 @@ pydist: build
# RELEASE HELPERS # RELEASE HELPERS
# create new version to commit to main # Create new version to commit to main
.PHONY: release-stage-1 .PHONY: create-release-candidate
release-stage-1: dev-env bump clean-lite gen-package-lock create-release-candidate: dev-env bump-version clean-lite gen-package-lock
@echo "Version bumped part:$(PART) and client built. Ready to commit and push" @echo "Version bumped part:$(PART) and client built. Ready to commit and push"
# build dist and release to dev pypi # Bump the release candidate version if needed (i.e. the previous release candidate had errors).
.PHONY: release-stage-2 .PHONY: recreate-release-candidate
release-stage-2: dev-env pydist twine recreate-release-candidate: dev-env bump-release-candidate clean-lite gen-package-lock
@echo "Version bumped part:$(PART) and client built. Ready to commit and push"
# Build dist and release to Test PyPI
.PHONY: release-candidate-to-test-pypi
release-candidate-to-test-pypi: dev-env pydist twine
@echo "Dist built and uploaded to test.pypi.org" @echo "Dist built and uploaded to test.pypi.org"
@echo "Test the install:" @echo "Test the install:"
@echo " make install-release-test" @echo " make install-release-test"
@echo "Then upload to Pypi prod:"
@echo " make twine-prod"
.PHONY: release-stage-final # Build final dist (gets rid of the rc tag) and release final candidate to TestPyPI
release-stage-final: twine-prod .PHONY: release-final-to-test-pypi
release-final-to-test-pypi: dev-env bump-release clean-lite gen-package-lock pydist twine
@echo "Final release dist built and uploaded to test.pypi.org"
@echo "Test the install:"
@echo " make install-release-test"
.PHONY: release-final
release-final: twine-prod
@echo "Release uploaded to pypi.org" @echo "Release uploaded to pypi.org"
# DANGER: releases directly to prod # DANGER: releases directly to prod
@@ -136,11 +147,22 @@ dev-env-client:
dev-env-server: dev-env-server:
pip install -r server/requirements-dev.txt pip install -r server/requirements-dev.txt
# give PART=[major, minor, part] as param to make bump # Set PART=[major, minor, patch] as param to make bump.
.PHONY: bump # This will create a release candidate. (i.e. 0.16.1 -> 0.16.2-rc.0 for a patch bump)
bump: .PHONY: bump-version
bump-version:
bumpversion --config-file .bumpversion.cfg $(PART) bumpversion --config-file .bumpversion.cfg $(PART)
# Increments the release candidate version (i.e. 0.16.2-rc.1 -> 0.16.2-rc.2)
.PHONY: bump-release-candidate
bump-release-candidate:
bumpversion --config-file .bumpversion.cfg prerelversion --allow-dirty
# Finalizes the release candidate by removing the release candidate tag (i.e. 0.16.2-rc.2 -> 0.16.2).
.PHONY: bump-release
bump-release:
bumpversion --config-file .bumpversion.cfg prerel --allow-dirty
.PHONY: twine .PHONY: twine
twine: twine:
twine upload --repository-url https://test.pypi.org/legacy/ dist/* twine upload --repository-url https://test.pypi.org/legacy/ dist/*
+11
View File
@@ -0,0 +1,11 @@
#### Reviewers
**Functional:**
**Readability:**
---
## Changes
- add
- remove
- modify
-1
View File
@@ -1 +0,0 @@
web: gunicorn --chdir server/eb app:application --log-file -
+13 -20
View File
@@ -67,37 +67,30 @@ For any errors, [report bugs on Github](https://github.com/chanzuckerberg/cellxg
### Contributing ### Contributing
We warmly welcome contributions from the community! Please see our [contributing guide](https://chanzuckerberg.github.io/cellxgene/posts/contribute) and don't hesitate to open an issue or send a pull request to improve cellxgene. We warmly welcome contributions from the community! Please see our [contributing guide](https://chanzuckerberg.github.io/cellxgene/posts/contribute) and don't hesitate to open an issue or send a pull request to improve cellxgene. Please see the [dev_docs](https://github.com/chanzuckerberg/cellxgene/tree/main/dev_docs) for pull request suggestions, unit test details, local documentation preview, and other development specifics.
This project adheres to the Contributor Covenant [code of conduct](https://github.com/chanzuckerberg/.github/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to opensource@chanzuckerberg.com. This project adheres to the Contributor Covenant [code of conduct](https://github.com/chanzuckerberg/.github/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to opensource@chanzuckerberg.com.
### Reuse ### Reuse
This project was started with the sole goal of empowering the scientific community to explore and understand their data. As such, we encourage other scientific tool builders in academia or industry to adopt the patterns, tools, and code from this project, and reach out to us with ideas or questions. All code is freely available for reuse under the [MIT license](https://opensource.org/licenses/MIT). This project was started with the sole goal of empowering the scientific community to explore and understand their data.
As such, we encourage other scientific tool builders in academia or industry to adopt the patterns, tools, and code from
this project. All code is freely available for reuse under the [MIT license](https://opensource.org/licenses/MIT).
Before extending cellxgene, we encourage you to reach out to us with ideas or questions. It might be possible that an
extension could be directly contributed, which would make it available for a wider audience, or that it's on our
[roadmap](./docs/posts/roadmap.md) and under active development.
See the [cellxgene extensions](./docs/posts/extensions.md) section of our documentation for examples of community use and cellxgene extensions.
### Security ### Security
If you believe you have found a security issue, we would appreciate notification. Please send email to <security@chanzuckerberg.com>. If you believe you have found a security issue, we would appreciate notification. Please send email to <security@chanzuckerberg.com>.
# About # Inspiration
### Core team We've been heavily inspired by several other related single-cell visualization projects, including the [UCSC Cell Browser](http://cells.ucsc.edu/), [Cytoscape](http://www.cytoscape.org/), [Xena](https://xena.ucsc.edu/), [ASAP](https://asap.epfl.ch/), [GenePattern](http://genepattern-notebook.org/), and many others. We hope to explore collaborations where useful as this community works together on improving interactive visualization for single-cell data.
The current core team:
- Colin Megill, frontend & product design
- Bruce Martin, software engineer
- Sidney Bell, computational biologist
- Lia Prins, designer
- Severiano Badajoz, software engineer
We would also like to gratefully acknowledge contributions from past core team members:
- Charlotte Weaver, software engineer
### Inspiration
We've been heavily inspired by several other related single-cell visualization projects, including the [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/), and many others. We hope to explore collaborations where useful as this community works together on improving interactive visualization for single-cell data.
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.
-29
View File
@@ -1,29 +0,0 @@
{
"name": "cellxgene",
"description": "An interactive explorer for single-cell transcriptomics data",
"repository": "https://github.com/chanzuckerberg/cellxgene",
"logo": "https://cellxgene-example-data.czi.technology/favicon.png",
"keywords": [
"scientific",
"visualization",
"scrna-seq",
"transcriptomics",
"dataviz"
],
"buildpacks": [
{
"url": "heroku/nodejs"
},
{
"url": "heroku/python"
}
],
"stack": "heroku-18",
"env": {
"DATASET": {
"description": "Link to dataset",
"value": "https://cellxgene-example-data.czi.technology/pbmc3k.h5ad",
"required": "true"
}
}
}
+4 -2
View File
@@ -3,6 +3,8 @@ include ../common.mk
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../server/test/fixtures/pbmc3k-annotations.csv) ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../server/test/fixtures/pbmc3k-annotations.csv)
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS)) ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
CXG_CONFIG := $(if $(CXG_CONFIG), $(CXG_CONFIG), ./__tests__/e2e/test_config.yaml)
# Packaging # Packaging
.PHONY: clean .PHONY: clean
clean: clean:
@@ -31,9 +33,9 @@ start-frontend:
.PHONY: smoke-test .PHONY: smoke-test
smoke-test: smoke-test:
start_server_and_test \ start_server_and_test \
'CXG_OPTIONS="--disable-annotations" $(MAKE) start-server' \ 'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
$(CXG_SERVER_PORT) \ $(CXG_SERVER_PORT) \
'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" npm run e2e -- --verbose false' 'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" CXG_AUTH_TYPE="test" npm run e2e -- --verbose false'
# start an instance of cellxgene and run the end-to-end annotations tests # start an instance of cellxgene and run the end-to-end annotations tests
.PHONY: smoke-test-annotations .PHONY: smoke-test-annotations
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`did launch page launched 1`] = `"<span style=\\"width: 185px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`; exports[`did launch page launched 1`] = `"<span style=\\"max-width: 155px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`; exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"max-width: 265px;\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start; width: 100%; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`;
@@ -2,22 +2,22 @@
exports[`annotations stacked bar graph renders 1`] = ` exports[`annotations stacked bar graph renders 1`] = `
Array [ Array [
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>", "<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas class=\\"bp3-popover-targer\\" width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2133</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>", "<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2133</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
] ]
`; `;
exports[`annotations stacked bar graph renders 2`] = ` exports[`annotations stacked bar graph renders 2`] = `
Array [ Array [
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>", "<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-TEST-LABEL\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-TEST-LABEL\\" data-testclass=\\"categorical-value\\" aria-label=\\"TEST-LABEL\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">TEST-</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">LABEL</span><span style=\\"position: absolute; right: 0px; color: black;\\">LABEL</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-TEST-LABEL\\" style=\\"color: black;\\">0</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:TEST-LABEL:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
"<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas class=\\"bp3-popover-targer\\" width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2638</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>", "<div class=\\"categorical__value___2RKaC\\" data-testclass=\\"categorical-row\\" style=\\"padding: 4px 0px 4px 7px; display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 2px; border-radius: 2px;\\"><div style=\\"margin: 0px; padding: 0px; user-select: none; width: 220px; display: flex; justify-content: space-between;\\"><div style=\\"display: flex; align-items: baseline;\\"><label for=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" class=\\"bp3-control bp3-checkbox\\" style=\\"margin: 0px;\\"><input id=\\"value-toggle-checkbox-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value-select\\" data-testid=\\"categorical-value-select-TEST-CATEGORY-unassigned\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span data-testid=\\"categorical-value-TEST-CATEGORY-unassigned\\" data-testclass=\\"categorical-value\\" aria-label=\\"unassigned\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 63px; color: rgb(171, 171, 171); font-style: italic; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: rgb(171, 171, 171); font-style: italic; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">unass</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">igned</span><span style=\\"position: absolute; right: 0px; color: rgb(171, 171, 171);\\">igned</span></span></span></span></span></span></div><span style=\\"flex-shrink: 0;\\"><canvas width=\\"100\\" height=\\"11\\" style=\\"margin-right: 5px; width: 100px; height: 11px;\\"></canvas></span></div><div><span><span data-testclass=\\"categorical-value-count\\" data-testid=\\"categorical-value-count-TEST-CATEGORY-unassigned\\" style=\\"color: rgb(171, 171, 171); font-style: italic;\\">2638</span><svg display=\\"none\\" style=\\"margin-left: 5px; width: 11px; height: 11px; background-color: inherit;\\"></svg><span><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><button type=\\"button\\" data-testclass=\\"seeActions\\" data-testid=\\"TEST-CATEGORY:unassigned:see-actions\\" class=\\"bp3-button bp3-minimal bp3-small\\" tabindex=\\"0\\" style=\\"margin-left: 2px; position: relative; top: -1px; min-height: 16px;\\"><span icon=\\"more\\" class=\\"bp3-icon bp3-icon-more\\"><svg data-icon=\\"more\\" width=\\"10\\" height=\\"10\\" viewBox=\\"0 0 16 16\\"><desc>more</desc><path d=\\"M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z\\" fill-rule=\\"evenodd\\"></path></svg></span></button></span></span></span></span></div></div>",
] ]
`; `;
exports[`annotations truncate midpoint whitespace 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`; exports[`annotations truncate midpoint whitespace 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`;
exports[`annotations truncate midpoint whitespace 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`; exports[`annotations truncate midpoint whitespace 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-123 456\\" data-testclass=\\"categorical-value\\" aria-label=\\"123 456\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">123</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">&nbsp;456</span><span style=\\"position: absolute; right: 0px; color: black;\\">&nbsp;456</span></span></span></span>"`;
exports[`annotations truncate single character 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`; exports[`annotations truncate single character 1`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
exports[`annotations truncate single character 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 187px; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`; exports[`annotations truncate single character 2`] = `"<span data-testid=\\"categorical-value-TEST-CATEGORY-T\\" data-testclass=\\"categorical-value\\" aria-label=\\"T\\" class=\\"\\" tabindex=\\"0\\" style=\\"width: 187px; color: black; font-style: normal; display: inline-block; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px;\\"><span style=\\"width: 100%; color: black; font-style: normal; display: flex; overflow: hidden; line-height: 1.1em; height: 1.1em; vertical-align: middle; margin-right: 16px; justify-content: flex-start; padding: 0px;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">T</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\"></span><span style=\\"position: absolute; right: 0px; color: black;\\"></span></span></span></span>"`;
+73
View File
@@ -13,8 +13,11 @@ import {
getTestClass, getTestClass,
getTestId, getTestId,
isElementPresent, isElementPresent,
goToPage,
} from "./puppeteerUtils"; } from "./puppeteerUtils";
import { appUrlBase } from "./config";
export async function drag(testId, start, end, lasso = false) { export async function drag(testId, start, end, lasso = false) {
const layout = await waitByID(testId); const layout = await waitByID(testId);
const elBox = await layout.boxModel(); const elBox = await layout.boxModel();
@@ -312,4 +315,74 @@ export async function assertCategoryDoesNotExist(categoryName) {
await expect(result).toBe(false); await expect(result).toBe(false);
} }
export async function login() {
const email = `cellxgene-smoke-test+${process.env.DEPLOYMENT_STAGE}@chanzuckerberg.com`;
const password = "Test1111";
await goToPage(appUrlBase);
await clickOn("log-in");
// (thuang): Auth0 form is unstable and unsafe for input until verified
await waitUntilFormFieldStable('[name="email"]');
await expect(page).toFillForm("form", {
email,
password,
});
await Promise.all([
page.waitForNavigation({ waitUntil: "networkidle0" }),
expect(page).toClick('[name="submit"]'),
]);
expect(page.url()).toContain(appUrlBase);
}
export async function logout() {
await clickOnUntil("user-info", async () => {
await waitByID("log-out");
await Promise.all([
page.waitForNavigation({ waitUntil: "networkidle0" }),
clickOn("log-out"),
]);
});
await waitByID("log-in");
}
async function waitUntilFormFieldStable(selector) {
const MAX_RETRY = 10;
const WAIT_FOR_MS = 200;
const EXPECTED_VALUE = "aaa";
let retry = 0;
while (retry < MAX_RETRY) {
try {
await expect(page).toFill(selector, EXPECTED_VALUE);
const fieldHandle = await expect(page).toMatchElement(selector);
const fieldValue = await page.evaluate(
(input) => input.value,
fieldHandle
);
expect(fieldValue).toBe(EXPECTED_VALUE);
break;
} catch (error) {
retry += 1;
await page.waitFor(WAIT_FOR_MS);
}
}
if (retry === MAX_RETRY) {
throw Error("clickOnUntil() assertion failed!");
}
}
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */ /* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
+33
View File
@@ -17,6 +17,7 @@ import {
goToPage, goToPage,
typeInto, typeInto,
waitByID, waitByID,
clickOnUntil,
} from "./puppeteerUtils"; } from "./puppeteerUtils";
import { import {
@@ -31,6 +32,8 @@ import {
runDiffExp, runDiffExp,
selectCategory, selectCategory,
subset, subset,
login,
logout,
} from "./cellxgeneActions"; } from "./cellxgeneActions";
const data = datasets[DATASET]; const data = datasets[DATASET];
@@ -518,4 +521,34 @@ test("lasso moves after pan", async () => {
expect(panCount).toBe(initialCount); expect(panCount).toBe(initialCount);
}); });
const describeIfCalledByMakeFileTarget =
process.env.CXG_AUTH_TYPE?.toLowerCase() === "test"
? describe
: describe.skip;
describeIfCalledByMakeFileTarget("auth buttons", () => {
test("login then logout", async () => {
await goToPage(appUrlBase);
await clickOnUntil("log-in", async () => {
await page.waitForNavigation({ waitUntil: "networkidle0" });
await waitByID("user-info");
});
await logout();
});
});
const conditionalDescribe =
process.env.TEST_AUTH_INTEGRATION === "true" ? describe : describe.skip;
conditionalDescribe("AuthN Integration", () => {
it("logs in", async () => {
await login();
});
it("logs out", async () => {
await login();
await logout();
});
});
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */ /* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
+3 -3
View File
@@ -17,7 +17,9 @@ setDefaultOptions({ timeout: 20 * 1000 });
jest.retryTimes(ENV_DEFAULT.RETRY_ATTEMPTS); jest.retryTimes(ENV_DEFAULT.RETRY_ATTEMPTS);
(async () => { beforeEach(async () => {
await jestPuppeteer.resetBrowser();
const userAgent = await browser.userAgent(); const userAgent = await browser.userAgent();
await page.setUserAgent(`${userAgent}bot`); await page.setUserAgent(`${userAgent}bot`);
@@ -53,6 +55,4 @@ jest.retryTimes(ENV_DEFAULT.RETRY_ATTEMPTS);
} }
} }
}); });
})().catch((error) => {
console.error("puppeteer.setup.js error", error);
}); });
+47
View File
@@ -0,0 +1,47 @@
server:
app:
force_https: true
# By default, cellxgene will serve api requests from the same base url as the webpage.
# In general api_base_url and web_base_url will not need to be set.
# There are two reasons to set these parameters:
# 1. Oauth authentication is used; the oauth server will redirect back to the api_base_url after login,
# which then redirects back to the web_base_url. If the web_base_url is not set, it will default to
# the api_base_url. If oauth authentication is used, the api_base_url must be set.
# For a local test (where the server runs on "http://localhost:<port>"), then the api_base_url may be
# set to the string "local".
# 2. The cellxgene deploymnent is in an environment where the webpage and api have
# different base urls. In this case both api_base_url and web_base_url must be set.
# It is up to the server admin to ensure that the networking is setup correctly for this environment.
api_base_url: http://localhost:5005
web_base_url: http://localhost:3000
authentication:
# The authentication types may be "none", "session", "oauth"
# none: No authentication support, features like user_annotations must not be enabled.
# session: A session based userid is automatically generated. (no params needed)
# oauth: oauth2 is used for authentication; parameters are defined in params_oauth.
type: test
dataset:
app:
about_legal_tos: null
about_legal_privacy: null
presentation:
max_categories: 1000
custom_colors: true
user_annotations:
enable: false
type: local_file_csv
local_file_csv:
directory: null
file: null
ontology:
enable: false
obo_location: null
embeddings:
names: []
enable_reembedding: false
@@ -0,0 +1,198 @@
/* eslint-disable no-bitwise -- unsigned right shift better than Math.round */
/*
test color helpers
*/
import {
createColorTable,
loadUserColorConfig,
} from "../../../src/util/stateManager/colorHelpers";
import * as Dataframe from "../../../src/util/dataframe";
describe("categorical color helpers", () => {
/*
Primary test constraint for categorical colors is that they are ordered/identified
by schema order, NOT by value. Ie,
scale(schemaIndex) should match rgb[obsOffset]
*/
const schema = indexSchema({
annotations: {
obs: {
columns: [
{
name: "name_0",
type: "string",
writable: false,
},
{
name: "continuousColumn",
type: "float32",
writable: false,
},
{
categories: [
"CD4 T cells",
"CD14+ Monocytes",
"B cells",
"CD8 T cells",
"NK cells",
"FCGR3A+ Monocytes",
"Dendritic cells",
"Megakaryocytes",
],
name: "categoricalColumn",
type: "categorical",
writable: false,
},
],
index: "name_0",
},
var: {
columns: [
{
name: "name_0",
type: "string",
writable: false,
},
],
index: "name_0",
},
},
dataframe: {
nObs: 2638,
nVar: 1838,
type: "float32",
},
layout: {},
});
const catColCategories = schema.annotations.obs.columns[2].categories;
const obsDataframe = new Dataframe.Dataframe(
[schema.dataframe.nObs, 2],
[
new Float32Array(schema.dataframe.nObs).map(() => Math.random()),
new Array(schema.dataframe.nObs)
.fill("")
.map(
() =>
catColCategories[(Math.random() * catColCategories.length) >>> 0]
),
],
null,
new Dataframe.KeyIndex(["continuousColumn", "categoricalColumn"])
);
test("default category order", () => {
const ct = createColorTable(
"color by categorical metadata",
"categoricalColumn",
obsDataframe,
schema
);
expect(ct).toBeDefined();
const data = obsDataframe.col("categoricalColumn").asArray();
const cats = schema.annotations.obsByName.categoricalColumn.categories;
for (let i = 0; i < schema.dataframe.nObs; i += 1) {
expect(makeScale(ct.rgb[i])).toEqual(ct.scale(cats.indexOf(data[i])));
}
});
test("shuffle category order", () => {
const schemaClone = indexSchema(JSON.parse(JSON.stringify(schema)));
shuffle(schemaClone.annotations.obsByName.categoricalColumn.categories);
const ct = createColorTable(
"color by categorical metadata",
"categoricalColumn",
obsDataframe,
schemaClone
);
expect(ct).toBeDefined();
const data = obsDataframe.col("categoricalColumn").asArray();
const cats = schemaClone.annotations.obsByName.categoricalColumn.categories;
for (let i = 0; i < schemaClone.dataframe.nObs; i += 1) {
expect(makeScale(ct.rgb[i])).toEqual(ct.scale(cats.indexOf(data[i])));
}
});
test("user defined color order", () => {
const cats = schema.annotations.obsByName.categoricalColumn.categories;
const shuffleCats = shuffle(
Array.from(schema.annotations.obsByName.categoricalColumn.categories)
);
const userDefinedColorTable = {
categoricalColumn: shuffleCats.reduce((acc, label) => {
acc[label] = randRGBColor();
return acc;
}, {}),
};
const userColors = loadUserColorConfig(userDefinedColorTable);
expect(userColors).toBeDefined();
const ct = createColorTable(
"color by categorical metadata",
"categoricalColumn",
obsDataframe,
schema,
userColors
);
expect(ct).toBeDefined();
const data = obsDataframe.col("categoricalColumn").asArray();
for (let i = 0; i < schema.dataframe.nObs; i += 1) {
expect(makeScale(ct.rgb[i])).toEqual(
ct.scale(cats.indexOf(data[i])).toString()
);
}
});
});
/*
TODO:
1. mix up category order in schema to make sure it works with varied order
2. user defined colors
*/
function indexSchema(schema) {
schema.annotations.obsByName = Object.fromEntries(
schema.annotations?.obs?.columns?.map((v) => [v.name, v]) ?? []
);
schema.annotations.varByName = Object.fromEntries(
schema.annotations?.var?.columns?.map((v) => [v.name, v]) ?? []
);
schema.layout.obsByName = Object.fromEntries(
schema.layout?.obs?.map((v) => [v.name, v]) ?? []
);
schema.layout.varByName = Object.fromEntries(
schema.layout?.var?.map((v) => [v.name, v]) ?? []
);
return schema;
}
function makeScale(rgb) {
// make a scale string from a rgb float triple
return `rgb(${(rgb[0] * 255) >>> 0}, ${(rgb[1] * 255) >>> 0}, ${
(rgb[2] * 256) >>> 0
})`;
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i -= 1) {
const j = (Math.random() * (i + 1)) >>> 0;
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function randHexColor() {
const hex = ((Math.random() * 255) >>> 0).toString(16);
return `0${hex}`.slice(-2);
}
function randRGBColor() {
return `#${randHexColor()}${randHexColor()}${randHexColor()}`;
}
/* eslint-enable no-bitwise -- unsigned right shift better than Math.round */
+8
View File
@@ -4,6 +4,7 @@ module.exports = {
extends: [ extends: [
"airbnb", "airbnb",
"plugin:eslint-comments/recommended", "plugin:eslint-comments/recommended",
"plugin:@blueprintjs/recommended",
"plugin:compat/recommended", "plugin:compat/recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
"prettier/react", "prettier/react",
@@ -39,6 +40,7 @@ module.exports = {
}, },
}, },
rules: { rules: {
"react/jsx-no-target-blank": "off",
"eslint-comments/require-description": ["error"], "eslint-comments/require-description": ["error"],
"no-magic-numbers": "off", "no-magic-numbers": "off",
"no-nested-ternary": "off", "no-nested-ternary": "off",
@@ -64,6 +66,12 @@ module.exports = {
"LabeledStatement", "LabeledStatement",
"WithStatement", "WithStatement",
], ],
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: true,
},
],
}, },
overrides: [ overrides: [
{ {
@@ -75,7 +75,7 @@
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/edge.png" src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/edge.png"
style="width: 80px; height: 80px;" style="width: 80px; height: 80px;"
/> />
<div>Edge ≥ 15</div> <div>Edge ≥ 79</div>
</a> </a>
</div> </div>
</div> </div>
@@ -31,7 +31,11 @@ const devConfig = {
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
loader: "file-loader", loader: "file-loader",
include: [nodeModules, fonts], include: [nodeModules, fonts],
query: { name: "static/assets/[name].[ext]" }, query: {
name: "static/assets/[name].[ext]",
// (thuang): This is needed to make sure @font url path is '/static/assets/'
publicPath: "/",
},
}, },
], ],
}, },
@@ -45,7 +45,11 @@ const prodConfig = {
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
loader: "file-loader", loader: "file-loader",
include: [nodeModules, fonts], include: [nodeModules, fonts],
query: { name: "static/assets/[name]-[contenthash].[ext]" }, query: {
name: "static/assets/[name]-[contenthash].[ext]",
// (thuang): This is needed to make sure @font url path is '../static/assets/'
publicPath: "static/",
},
}, },
], ],
}, },
@@ -7,7 +7,7 @@ const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
const src = path.resolve("src"); const src = path.resolve("src");
const nodeModules = path.resolve("node_modules"); const nodeModules = path.resolve("node_modules");
const publicPath = "/"; const publicPath = "";
const rawObsoleteHTMLTemplate = fs.readFileSync( const rawObsoleteHTMLTemplate = fs.readFileSync(
`${__dirname}/obsoleteHTMLTemplate.html`, `${__dirname}/obsoleteHTMLTemplate.html`,
+473 -62
View File
@@ -1,6 +1,6 @@
{ {
"name": "cellxgene", "name": "cellxgene",
"version": "0.16.0", "version": "0.16.7",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -4156,6 +4156,216 @@
"tslib": "~1.10.0" "tslib": "~1.10.0"
} }
}, },
"@blueprintjs/eslint-plugin": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@blueprintjs/eslint-plugin/-/eslint-plugin-0.3.0.tgz",
"integrity": "sha512-bQEdE4ApEHxCDV8hT9uIxeRbDFKOtRLBT3/Zy3Ku+nowDAYl/8jwZKp6lJuR/nqvsfuIXTnVef6ivwdBEieQfA==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "^4.2.0",
"eslint": "^7.9.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.3.0.tgz",
"integrity": "sha512-cmmIK8shn3mxmhpKfzMMywqiEheyfXLV/+yPDnOTvQX/ztngx7Lg/OD26J8gTZfkLKUmaEBxO2jYP3keV7h2OQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/scope-manager": "4.3.0",
"@typescript-eslint/types": "4.3.0",
"@typescript-eslint/typescript-estree": "4.3.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz",
"integrity": "sha512-ZAI7xjkl+oFdLV/COEz2tAbQbR3XfgqHEGy0rlUXzfGQic6EBCR4s2+WS3cmTPG69aaZckEucBoTxW9PhzHxxw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.3.0",
"@typescript-eslint/visitor-keys": "4.3.0",
"debug": "^4.1.1",
"globby": "^11.0.1",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
}
},
"acorn": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz",
"integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==",
"dev": true
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"eslint": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.10.0.tgz",
"integrity": "sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@eslint/eslintrc": "^0.1.3",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.0.1",
"doctrine": "^3.0.0",
"enquirer": "^2.3.5",
"eslint-scope": "^5.1.1",
"eslint-utils": "^2.1.0",
"eslint-visitor-keys": "^1.3.0",
"espree": "^7.3.0",
"esquery": "^1.2.0",
"esutils": "^2.0.2",
"file-entry-cache": "^5.0.1",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^5.0.0",
"globals": "^12.1.0",
"ignore": "^4.0.6",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash": "^4.17.19",
"minimatch": "^3.0.4",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"progress": "^2.0.0",
"regexpp": "^3.1.0",
"semver": "^7.2.1",
"strip-ansi": "^6.0.0",
"strip-json-comments": "^3.1.0",
"table": "^5.2.3",
"text-table": "^0.2.0",
"v8-compile-cache": "^2.0.3"
},
"dependencies": {
"eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"requires": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
}
}
}
},
"eslint-visitor-keys": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true
},
"espree": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz",
"integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==",
"dev": true,
"requires": {
"acorn": "^7.4.0",
"acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.3.0"
}
},
"esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"requires": {
"estraverse": "^5.2.0"
},
"dependencies": {
"estraverse": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
}
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
"integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
"dev": true,
"requires": {
"type-fest": "^0.8.1"
}
},
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"@blueprintjs/icons": { "@blueprintjs/icons": {
"version": "3.19.0", "version": "3.19.0",
"resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.19.0.tgz", "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.19.0.tgz",
@@ -4184,6 +4394,76 @@
"minimist": "^1.2.0" "minimist": "^1.2.0"
} }
}, },
"@eslint/eslintrc": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz",
"integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.1.1",
"espree": "^7.3.0",
"globals": "^12.1.0",
"ignore": "^4.0.6",
"import-fresh": "^3.2.1",
"js-yaml": "^3.13.1",
"lodash": "^4.17.19",
"minimatch": "^3.0.4",
"strip-json-comments": "^3.1.1"
},
"dependencies": {
"acorn": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz",
"integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==",
"dev": true
},
"ajv": {
"version": "6.12.5",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz",
"integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"eslint-visitor-keys": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"dev": true
},
"espree": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz",
"integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==",
"dev": true,
"requires": {
"acorn": "^7.4.0",
"acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.3.0"
}
},
"globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
"integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
"dev": true,
"requires": {
"type-fest": "^0.8.1"
}
},
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
}
}
},
"@hapi/address": { "@hapi/address": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
@@ -5137,6 +5417,32 @@
} }
} }
}, },
"@nodelib/fs.scandir": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
"integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "2.0.3",
"run-parallel": "^1.1.9"
}
},
"@nodelib/fs.stat": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
"integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
"dev": true
},
"@nodelib/fs.walk": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
"integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
"dev": true,
"requires": {
"@nodelib/fs.scandir": "2.1.3",
"fastq": "^1.6.0"
}
},
"@npmcli/move-file": { "@npmcli/move-file": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz",
@@ -5489,6 +5795,22 @@
"eslint-utils": "^2.0.0" "eslint-utils": "^2.0.0"
} }
}, },
"@typescript-eslint/scope-manager": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz",
"integrity": "sha512-cTeyP5SCNE8QBRfc+Lgh4Xpzje46kNUhXYfc3pQWmJif92sjrFuHT9hH4rtOkDTo/si9Klw53yIr+djqGZS1ig==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.3.0",
"@typescript-eslint/visitor-keys": "4.3.0"
}
},
"@typescript-eslint/types": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.3.0.tgz",
"integrity": "sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw==",
"dev": true
},
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "2.34.0", "version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz",
@@ -5512,6 +5834,24 @@
} }
} }
}, },
"@typescript-eslint/visitor-keys": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz",
"integrity": "sha512-xZxkuR7XLM6RhvLkgv9yYlTcBHnTULzfnw4i6+z2TGBLy9yljAypQaZl9c3zFvy7PNI7fYWyvKYtohyF8au3cw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.3.0",
"eslint-visitor-keys": "^2.0.0"
},
"dependencies": {
"eslint-visitor-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
"dev": true
}
}
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@@ -6387,9 +6727,9 @@
} }
}, },
"bl": { "bl": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz",
"integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==",
"dev": true, "dev": true,
"requires": { "requires": {
"buffer": "^5.5.0", "buffer": "^5.5.0",
@@ -6397,16 +6737,6 @@
"readable-stream": "^3.4.0" "readable-stream": "^3.4.0"
}, },
"dependencies": { "dependencies": {
"buffer": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz",
"integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==",
"dev": true,
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
},
"readable-stream": { "readable-stream": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -7633,6 +7963,15 @@
"xdg-basedir": "^3.0.0" "xdg-basedir": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"dot-prop": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"dev": true,
"requires": {
"is-obj": "^1.0.0"
}
},
"make-dir": { "make-dir": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@@ -8796,6 +9135,15 @@
} }
} }
}, },
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"requires": {
"path-type": "^4.0.0"
}
},
"doctrine": { "doctrine": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -8935,12 +9283,20 @@
} }
}, },
"dot-prop": { "dot-prop": {
"version": "4.2.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"is-obj": "^1.0.0" "is-obj": "^2.0.0"
},
"dependencies": {
"is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"dev": true
}
} }
}, },
"duplexer3": { "duplexer3": {
@@ -10187,6 +10543,31 @@
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true "dev": true
}, },
"fast-glob": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz",
"integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.0",
"merge2": "^1.3.0",
"micromatch": "^4.0.2",
"picomatch": "^2.2.1"
},
"dependencies": {
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
}
}
},
"fast-json-stable-stringify": { "fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -10202,6 +10583,15 @@
"resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz",
"integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw=="
}, },
"fastq": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz",
"integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==",
"dev": true,
"requires": {
"reusify": "^1.0.4"
}
},
"favicons": { "favicons": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/favicons/-/favicons-5.5.0.tgz", "resolved": "https://registry.npmjs.org/favicons/-/favicons-5.5.0.tgz",
@@ -11159,6 +11549,28 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
}, },
"globby": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz",
"integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==",
"dev": true,
"requires": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.1.1",
"ignore": "^5.1.4",
"merge2": "^1.3.0",
"slash": "^3.0.0"
},
"dependencies": {
"array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
}
}
},
"got": { "got": {
"version": "6.7.1", "version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
@@ -11691,6 +12103,12 @@
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true "dev": true
}, },
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
},
"ignore-walk": { "ignore-walk": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
@@ -11832,9 +12250,9 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==",
"dev": true "dev": true
}, },
"internal-slot": { "internal-slot": {
@@ -13787,9 +14205,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.19", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
}, },
"lodash._reinterpolate": { "lodash._reinterpolate": {
"version": "3.0.0", "version": "3.0.0",
@@ -14189,6 +14607,12 @@
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
}, },
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true
},
"methods": { "methods": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -15059,8 +15483,7 @@
"pako": { "pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
"dev": true
}, },
"parallel-transform": { "parallel-transform": {
"version": "1.2.0", "version": "1.2.0",
@@ -15251,6 +15674,12 @@
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
"dev": true "dev": true
}, },
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"pbkdf2": { "pbkdf2": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
@@ -15582,20 +16011,10 @@
"vendors": "^1.0.0" "vendors": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"dot-prop": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
"integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
"dev": true,
"requires": {
"is-obj": "^2.0.0"
}
},
"is-obj": { "is-obj": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
"dev": true
}, },
"postcss-selector-parser": { "postcss-selector-parser": {
"version": "3.1.2", "version": "3.1.2",
@@ -15682,20 +16101,10 @@
"postcss-selector-parser": "^3.0.0" "postcss-selector-parser": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"dot-prop": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
"integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
"dev": true,
"requires": {
"is-obj": "^2.0.0"
}
},
"is-obj": { "is-obj": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
"dev": true
}, },
"postcss-selector-parser": { "postcss-selector-parser": {
"version": "3.1.2", "version": "3.1.2",
@@ -17065,6 +17474,12 @@
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
}, },
"reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
"rgb-regex": { "rgb-regex": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
@@ -17100,6 +17515,12 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
}, },
"run-parallel": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
"integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
"dev": true
},
"run-queue": { "run-queue": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
@@ -18291,20 +18712,10 @@
"postcss-selector-parser": "^3.0.0" "postcss-selector-parser": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"dot-prop": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
"integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==",
"dev": true,
"requires": {
"is-obj": "^2.0.0"
}
},
"is-obj": { "is-obj": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
"dev": true
}, },
"postcss-selector-parser": { "postcss-selector-parser": {
"version": "3.1.2", "version": "3.1.2",
@@ -19413,9 +19824,9 @@
} }
}, },
"urijs": { "urijs": {
"version": "1.19.2", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.2.tgz", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.5.tgz",
"integrity": "sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==", "integrity": "sha512-48z9VGWwdCV5KfizHsE05DWS5fhK6gFlx5MjO7xu0Krc5FGPWzjlXEVV0nPMrdVuP7xmMHiPZ2HoYZwKOFTZOg==",
"dev": true "dev": true
}, },
"urix": { "urix": {
+5 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "cellxgene", "name": "cellxgene",
"version": "0.16.0", "version": "0.16.7",
"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,7 +34,7 @@
"Safari >= 10.1", "Safari >= 10.1",
"iOS >= 10.3", "iOS >= 10.3",
"Firefox >= 60", "Firefox >= 60",
"Edge >= 15", "Edge >= 79",
"not Explorer > 0" "not Explorer > 0"
], ],
"dependencies": { "dependencies": {
@@ -52,8 +52,9 @@
"gl-matrix": "^3.3.0", "gl-matrix": "^3.3.0",
"gl-vec3": "^1.1.3", "gl-vec3": "^1.1.3",
"is-number": "^7.0.0", "is-number": "^7.0.0",
"lodash": "^4.17.19", "lodash": "^4.17.20",
"memoize-one": "^5.1.1", "memoize-one": "^5.1.1",
"pako": "^1.0.11",
"react": "^16.13.1", "react": "^16.13.1",
"react-async": "^10.0.1", "react-async": "^10.0.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
@@ -84,6 +85,7 @@
"@babel/preset-react": "^7.10.4", "@babel/preset-react": "^7.10.4",
"@babel/register": "^7.10.5", "@babel/register": "^7.10.5",
"@babel/runtime": "^7.10.5", "@babel/runtime": "^7.10.5",
"@blueprintjs/eslint-plugin": "^0.3.0",
"@sentry/webpack-plugin": "^1.12.0", "@sentry/webpack-plugin": "^1.12.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^26.1.0", "babel-jest": "^26.1.0",
+32 -19
View File
@@ -1,46 +1,59 @@
/* eslint-disable */ const chalk = require("chalk");
// jshint esversion: 6 const express = require("express");
var path = require("path"); const favicon = require("serve-favicon");
var historyApiFallback = require("connect-history-api-fallback"); const webpack = require("webpack");
var chalk = require("chalk"); const devMiddleware = require("webpack-dev-middleware");
var express = require("express"); const config = require("../configuration/webpack/webpack.config.dev");
var favicon = require("serve-favicon"); const utils = require("./utils");
var webpack = require("webpack");
var config = require("../configuration/webpack/webpack.config.dev");
var utils = require("./utils");
process.env.NODE_ENV = "development"; process.env.NODE_ENV = "development";
const CLIENT_PORT = process.env.CXG_CLIENT_PORT; const CLIENT_PORT = process.env.CXG_CLIENT_PORT;
const { CXG_SERVER_PORT } = process.env;
const API = {
prefix: `http://localhost:${CXG_SERVER_PORT}/`,
};
// Set up compiler // Set up compiler
var compiler = webpack(config); const compiler = webpack(config);
compiler.plugin("invalid", () => { compiler.hooks.invalid.tap("invalid", () => {
utils.clearConsole(); utils.clearConsole();
console.log("Compiling..."); console.log("Compiling...");
}); });
compiler.plugin("done", (stats) => { compiler.hooks.done.tap("done", (stats) => {
utils.formatStats(stats, CLIENT_PORT); utils.formatStats(stats, CLIENT_PORT);
}); });
// Launch server // Launch server
var app = express(); const app = express();
app.use(historyApiFallback({ verbose: false }));
app.use( app.use(
require("webpack-dev-middleware")(compiler, { devMiddleware(compiler, {
logLevel: "warn", logLevel: "warn",
publicPath: config.output.publicPath, publicPath: config.output.publicPath,
index: true,
}) })
); );
app.use(favicon("./favicon.png")); app.use(favicon("./favicon.png"));
app.get("*", (req, res) => { app.get("/login", async (req, res) => {
res.sendFile(path.resolve("index.html")); try {
res.redirect(`${API.prefix}login?dataset=http://localhost:${CLIENT_PORT}`);
} catch (err) {
console.error(err);
}
});
app.get("/logout", async (req, res) => {
try {
res.redirect(`${API.prefix}logout?dataset=http://localhost:${CLIENT_PORT}`);
} catch (err) {
console.error(err);
}
}); });
app.listen(CLIENT_PORT, (err) => { app.listen(CLIENT_PORT, (err) => {
+4 -2
View File
@@ -2,6 +2,7 @@
Action creators for user annotation Action creators for user annotation
*/ */
import _ from "lodash"; import _ from "lodash";
import pako from "pako";
import * as globals from "../globals"; import * as globals from "../globals";
import { MatrixFBS, AnnotationsHelpers } from "../util/stateManager"; import { MatrixFBS, AnnotationsHelpers } from "../util/stateManager";
@@ -153,7 +154,7 @@ export const annotationCreateLabelInCategory = (
assignSelected assignSelected
) => async (dispatch, getState) => { ) => async (dispatch, getState) => {
/* /*
Add a new label to a user-defined category. If assignSelected is true, assign Add a new label to a user-defined category. If assignSelected is true, assign
the label to all currently selected cells. the label to all currently selected cells.
*/ */
const { const {
@@ -347,6 +348,7 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
const df = await annoMatrix.fetch("obs", writableAnnotations(annoMatrix)); const df = await annoMatrix.fetch("obs", writableAnnotations(annoMatrix));
const matrix = MatrixFBS.encodeMatrixFBS(df); const matrix = MatrixFBS.encodeMatrixFBS(df);
const compressedMatrix = pako.deflate(matrix);
try { try {
const queryString = const queryString =
!dataCollectionNameIsReadOnly && !!dataCollectionName !dataCollectionNameIsReadOnly && !!dataCollectionName
@@ -358,7 +360,7 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
`${globals.API.prefix}${globals.API.version}annotations/obs${queryString}`, `${globals.API.prefix}${globals.API.version}annotations/obs${queryString}`,
{ {
method: "PUT", method: "PUT",
body: matrix, body: compressedMatrix,
headers: new Headers({ headers: new Headers({
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
}), }),
+16 -9
View File
@@ -5,20 +5,23 @@ action creators related to embeddings choice
import { AnnoMatrixObsCrossfilter } from "../annoMatrix"; import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers"; import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
export async function _switchEmbedding(prevAnnoMatrix, newEmbeddingName) { export async function _switchEmbedding(
prevAnnoMatrix,
prevCrossfilter,
newEmbeddingName
) {
/* /*
DRY helper used by this and reembedding action creators DRY helper used by this and reembedding action creators
*/ */
const base = prevAnnoMatrix.base(); const base = prevAnnoMatrix.base();
const embeddingDf = await base.fetch("emb", newEmbeddingName); const embeddingDf = await base.fetch("emb", newEmbeddingName);
const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf); const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix).select( const obsCrossfilter = await new AnnoMatrixObsCrossfilter(
"emb", annoMatrix,
newEmbeddingName, prevCrossfilter.obsCrossfilter
{ ).select("emb", newEmbeddingName, {
mode: "all", mode: "all",
} });
);
return [annoMatrix, obsCrossfilter]; return [annoMatrix, obsCrossfilter];
} }
@@ -30,9 +33,13 @@ export const layoutChoiceAction = (newLayoutChoice) => async (
On layout choice, make sure we have selected all on the previous layout, AND the new On layout choice, make sure we have selected all on the previous layout, AND the new
layout. layout.
*/ */
const { annoMatrix: prevAnnoMatrix } = getState(); const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevCrossfilter,
} = getState();
const [annoMatrix, obsCrossfilter] = await _switchEmbedding( const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
prevAnnoMatrix, prevAnnoMatrix,
prevCrossfilter,
newLayoutChoice newLayoutChoice
); );
dispatch({ dispatch({
+12
View File
@@ -41,6 +41,17 @@ async function configFetch(dispatch) {
}); });
} }
async function userInfoFetch(dispatch) {
return fetchJson("userinfo").then((response) => {
const { userinfo: userInfo } = response || {};
dispatch({
type: "userInfo load complete",
userInfo,
});
return userInfo;
});
}
function prefetchEmbeddings(annoMatrix) { function prefetchEmbeddings(annoMatrix) {
/* /*
prefetch requests for all embeddings prefetch requests for all embeddings
@@ -62,6 +73,7 @@ const doInitialDataLoad = () =>
configFetch(dispatch), configFetch(dispatch),
schemaFetch(dispatch), schemaFetch(dispatch),
userColorsFetchAndLoad(dispatch), userColorsFetchAndLoad(dispatch),
userInfoFetch(dispatch),
]); ]);
const baseDataUrl = `${globals.API.prefix}${globals.API.version}`; const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
+2 -1
View File
@@ -79,10 +79,11 @@ export function requestReembed() {
type: "reembed: request completed", type: "reembed: request completed",
}); });
const { annoMatrix: prevAnnoMatrix } = getState(); const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevCrossfilter } = getState();
const base = prevAnnoMatrix.base().addEmbedding(schema); const base = prevAnnoMatrix.base().addEmbedding(schema);
const [annoMatrix, obsCrossfilter] = await _switchEmbedding( const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
base, base,
prevCrossfilter,
schema.name schema.name
); );
dispatch({ dispatch({
@@ -3,17 +3,19 @@ import { connect } from "react-redux";
import { import {
Button, Button,
Tooltip,
InputGroup,
Dialog,
Classes, Classes,
Code,
Colors, Colors,
Dialog,
InputGroup,
Tooltip,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
@connect((state) => ({ @connect((state) => ({
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null, idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
annotations: state.annotations, annotations: state.annotations,
auth: state.config?.authentication, auth: state.config?.authentication,
userInfo: state.userInfo,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
})) }))
class FilenameDialog extends React.Component { class FilenameDialog extends React.Component {
@@ -91,13 +93,19 @@ class FilenameDialog extends React.Component {
}; };
render() { render() {
const { writableCategoriesEnabled, annotations, idhash, auth } = this.props; const {
writableCategoriesEnabled,
annotations,
idhash,
userInfo,
} = this.props;
const { filenameText } = this.state; const { filenameText } = this.state;
return writableCategoriesEnabled && return writableCategoriesEnabled &&
annotations.promptForFilename &&
!annotations.dataCollectionNameIsReadOnly && !annotations.dataCollectionNameIsReadOnly &&
!annotations.dataCollectionName && !annotations.dataCollectionName &&
auth.is_authenticated ? ( userInfo.is_authenticated ? (
<Dialog <Dialog
icon="tag" icon="tag"
title="Annotations Collection" title="Annotations Collection"
@@ -138,9 +146,9 @@ class FilenameDialog extends React.Component {
<div> <div>
<p> <p>
Your annotations are stored in this file: Your annotations are stored in this file:
<code className="bp3-code"> <Code>
{filenameText}-{idhash}.csv {filenameText}-{idhash}.csv
</code> </Code>
</p> </p>
<p style={{ fontStyle: "italic" }}> <p style={{ fontStyle: "italic" }}>
(We added a unique ID to your filename) (We added a unique ID to your filename)
@@ -5,12 +5,13 @@ https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
https://bl.ocks.org/mbostock/3019563 https://bl.ocks.org/mbostock/3019563
*/ */
import React, { useEffect, useRef, useState, useCallback } from "react"; import React, { useEffect, useRef, useState, useCallback } from "react";
import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core"; import { Button, ButtonGroup, Icon, Tooltip } from "@blueprintjs/core";
import { connect } from "react-redux"; import { connect } from "react-redux";
import * as d3 from "d3"; import * as d3 from "d3";
import { interpolateCool } from "d3-scale-chromatic"; import { interpolateCool } from "d3-scale-chromatic";
import Async from "react-async"; import Async from "react-async";
import memoize from "memoize-one"; import memoize from "memoize-one";
import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../globals"; import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
import { histogramContinuous } from "../../util/dataframe/histogram"; import { histogramContinuous } from "../../util/dataframe/histogram";
@@ -26,7 +27,7 @@ function maybeScientific(x) {
const _ticks = x.ticks(4); const _ticks = x.ticks(4);
if (x.domain().some((n) => Math.abs(n) >= 10000)) { if (x.domain().some((n) => Math.abs(n) >= 10000)) {
/* /*
heuristic: if the last tick d3 wants to render has one significant heuristic: if the last tick d3 wants to render has one significant
digit ie., 2000, render 2e+3, but if it's anything else ie., 42000000 render digit ie., 2000, render 2e+3, but if it's anything else ie., 42000000 render
4.20e+n 4.20e+n
@@ -99,7 +100,7 @@ const HistogramFooter = React.memo(
pvalAdj, pvalAdj,
}) => { }) => {
/* /*
Footer of each histogram. Will render range, title, and optionally Footer of each histogram. Will render range, title, and optionally
differential expression info. differential expression info.
Required props: Required props:
@@ -145,7 +146,7 @@ const HistogramFooter = React.memo(
</span> </span>
</div> </div>
{logFoldChange && pvalAdj ? ( {logFoldChange !== undefined && pvalAdj !== undefined ? (
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -214,10 +215,7 @@ const HistogramHeader = React.memo(
> >
{onScatterPlotXClick && onScatterPlotYClick ? ( {onScatterPlotXClick && onScatterPlotYClick ? (
<span> <span>
<span <Icon icon={IconNames.SCATTER_PLOT} style={{ marginRight: 7 }} />
style={{ marginRight: 7 }}
className="bp3-icon-standard bp3-icon-scatter-plot"
/>
<ButtonGroup style={{ marginRight: 7 }}> <ButtonGroup style={{ marginRight: 7 }}>
<Button <Button
data-testid={`plot-x-${fieldId}`} data-testid={`plot-x-${fieldId}`}
@@ -1,7 +1,13 @@
import React, { useRef, useEffect } from "react"; import React, { useRef, useEffect } from "react";
import { connect, shallowEqual } from "react-redux"; import { connect, shallowEqual } from "react-redux";
import { FaChevronRight, FaChevronDown } from "react-icons/fa"; import { FaChevronRight, FaChevronDown } from "react-icons/fa";
import { AnchorButton, Button, Tooltip, Position } from "@blueprintjs/core"; import {
AnchorButton,
Button,
Classes,
Position,
Tooltip,
} from "@blueprintjs/core";
import { Flipper, Flipped } from "react-flip-toolkit"; import { Flipper, Flipped } from "react-flip-toolkit";
import Async from "react-async"; import Async from "react-async";
import memoize from "memoize-one"; import memoize from "memoize-one";
@@ -301,9 +307,12 @@ const StillLoading = ({ metadataField, checkboxID }) => {
alignItems: "flex-start", alignItems: "flex-start",
}} }}
> >
<label htmlFor={checkboxID} className="bp3-control bp3-checkbox"> <label
htmlFor={checkboxID}
className={`${Classes.CONTROL} ${Classes.CHECKBOX}`}
>
<input disabled id={checkboxID} checked type="checkbox" /> <input disabled id={checkboxID} checked type="checkbox" />
<span className="bp3-control-indicator" /> <span className={Classes.CONTROL_INDICATOR} />
</label> </label>
<Truncate> <Truncate>
<span <span
@@ -375,7 +384,10 @@ const CategoryHeader = React.memo(
alignItems: "flex-start", alignItems: "flex-start",
}} }}
> >
<label className="bp3-control bp3-checkbox" htmlFor={checkboxID}> <label
className={`${Classes.CONTROL} ${Classes.CHECKBOX}`}
htmlFor={checkboxID}
>
<input <input
id={checkboxID} id={checkboxID}
data-testclass="category-select" data-testclass="category-select"
@@ -385,7 +397,7 @@ const CategoryHeader = React.memo(
checked={selectionState === "all"} checked={selectionState === "all"}
type="checkbox" type="checkbox"
/> />
<span className="bp3-control-indicator" /> <span className={Classes.CONTROL_INDICATOR} />
</label> </label>
<span <span
role="menuitem" role="menuitem"
@@ -491,19 +503,7 @@ const CategoryRender = React.memo(
/* /*
Entire category has a single value, special case. Entire category has a single value, special case.
*/ */
const theOneValue = categorySummary.categoryValues[0]; return null;
return (
<div style={{ marginBottom: 10, marginTop: 4 }}>
<Truncate>
<span style={{ maxWidth: 150, fontWeight: 700 }}>
{metadataField}
</span>
</Truncate>
<Truncate>
<span style={{ maxWidth: 150 }}>{`: ${theOneValue}`}</span>
</Truncate>
</div>
);
} }
/* /*
+30 -7
View File
@@ -1,6 +1,6 @@
// jshint esversion: 6 // jshint esversion: 6
import React from "react"; import React from "react";
import { Button } from "@blueprintjs/core"; import { AnchorButton, Tooltip, Position } from "@blueprintjs/core";
import { connect } from "react-redux"; import { connect } from "react-redux";
import * as globals from "../../globals"; import * as globals from "../../globals";
import Category from "./category"; import Category from "./category";
@@ -15,6 +15,7 @@ import actions from "../../actions";
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
schema: state.annoMatrix?.schema, schema: state.annoMatrix?.schema,
ontology: state.ontology, ontology: state.ontology,
userInfo: state.userInfo,
})) }))
class Categories extends React.Component { class Categories extends React.Component {
constructor(props) { constructor(props) {
@@ -127,7 +128,12 @@ class Categories extends React.Component {
newCategoryText, newCategoryText,
expandedCats, expandedCats,
} = this.state; } = this.state;
const { writableCategoriesEnabled, schema, ontology } = this.props; const {
writableCategoriesEnabled,
schema,
ontology,
userInfo,
} = this.props;
const ontologyEnabled = ontology?.enabled ?? false; const ontologyEnabled = ontology?.enabled ?? false;
/* all names, sorted in display order. Will be rendered in this order */ /* all names, sorted in display order. Will be rendered in this order */
const allCategoryNames = ControlsHelpers.selectableCategoryNames( const allCategoryNames = ControlsHelpers.selectableCategoryNames(
@@ -179,7 +185,9 @@ class Categories extends React.Component {
{/* READ ONLY CATEGORICAL FIELDS */} {/* READ ONLY CATEGORICAL FIELDS */}
{/* this is duplicative but flat, could be abstracted */} {/* this is duplicative but flat, could be abstracted */}
{allCategoryNames.map((catName) => {allCategoryNames.map((catName) =>
!schema.annotations.obsByName[catName].writable ? ( !schema.annotations.obsByName[catName].writable &&
(schema.annotations.obsByName[catName].categories?.length > 1 ||
!schema.annotations.obsByName[catName].categories) ? (
<Category <Category
key={catName} key={catName}
metadataField={catName} metadataField={catName}
@@ -203,15 +211,30 @@ class Categories extends React.Component {
)} )}
{writableCategoriesEnabled ? ( {writableCategoriesEnabled ? (
<div> <Tooltip
<Button content={
userInfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
position={Position.RIGHT}
boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<AnchorButton
type="button"
data-testid="open-annotation-dialog" data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode} onClick={this.handleEnableAnnoMode}
intent="primary" intent="primary"
disabled={!userInfo.is_authenticated}
> >
Create new category Create new category
</Button> </AnchorButton>
</div> </Tooltip>
) : null} ) : null}
</div> </div>
); );
@@ -4,12 +4,13 @@ import * as d3 from "d3";
import { import {
Button, Button,
Classes,
Icon,
Menu, Menu,
MenuItem, MenuItem,
Popover, Popover,
Position,
Icon,
PopoverInteractionKind, PopoverInteractionKind,
Position,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as globals from "../../../globals"; import * as globals from "../../../globals";
import styles from "../categorical.css"; import styles from "../categorical.css";
@@ -81,7 +82,7 @@ class CategoryValue extends React.Component {
get shouldRenderStackedBarOrHistogram() { get shouldRenderStackedBarOrHistogram() {
const { colorAccessor, isColorBy, annotations } = this.props; const { colorAccessor, isColorBy, annotations } = this.props;
return colorAccessor && !isColorBy && !annotations.isEditingLabelName; return !!colorAccessor && !isColorBy && !annotations.isEditingLabelName;
} }
handleDeleteValue = () => { handleDeleteValue = () => {
@@ -173,7 +174,7 @@ class CategoryValue extends React.Component {
Checks to see if at least one of the following changed: Checks to see if at least one of the following changed:
* world state * world state
* the color accessor (what is currently being colored by) * the color accessor (what is currently being colored by)
* if this catagorical value's selection status has changed * if this categorical value's selection status has changed
* the crossfilter (ie, global selection state) * the crossfilter (ie, global selection state)
If and only if true, update the component If and only if true, update the component
@@ -200,6 +201,13 @@ class CategoryValue extends React.Component {
const newCount = newCategorySummary.categoryValueCounts[newCategoryIndex]; const newCount = newCategorySummary.categoryValueCounts[newCategoryIndex];
const countChanged = count !== newCount; const countChanged = count !== newCount;
// If the user edits an annotation that is currently colored-by, colors may be re-assigned.
// This test is conservative - it may cause re-rendering of entire category (all labels)
// if any one changes, but only for the currently colored-by category.
const colorMightHaveChanged =
nextProps.colorAccessor === nextProps.metadataField &&
props.categorySummary !== nextProps.categorySummary;
return ( return (
labelChanged || labelChanged ||
valueSelectionChange || valueSelectionChange ||
@@ -207,7 +215,8 @@ class CategoryValue extends React.Component {
annotationsChange || annotationsChange ||
editingLabel || editingLabel ||
dilationChange || dilationChange ||
countChanged countChanged ||
colorMightHaveChanged
); );
}; };
@@ -410,7 +419,6 @@ class CategoryValue extends React.Component {
return ( return (
<MiniStackedBar <MiniStackedBar
/* eslint-disable react/jsx-props-no-spreading -- Disable unneeded on next release of eslint-config-airbnb */
{...{ {...{
colorTable, colorTable,
domainValues, domainValues,
@@ -418,7 +426,6 @@ class CategoryValue extends React.Component {
domain, domain,
occupancy, occupancy,
}} }}
/* eslint-enable react/jsx-props-no-spreading -- enable */
height={VALUE_HEIGHT} height={VALUE_HEIGHT}
width={CHART_WIDTH} width={CHART_WIDTH}
/> />
@@ -439,7 +446,9 @@ class CategoryValue extends React.Component {
if ( if (
!this.shouldRenderStackedBarOrHistogram || !this.shouldRenderStackedBarOrHistogram ||
!AnnotationsHelpers.isContinuousAnnotation(schema, colorAccessor) // This function returns true on categorical annotations(when stacked bar should not render),
// in cases where the colorAccessor is a gene this function will return undefined since genes do not live on the schema
AnnotationsHelpers.isCategoricalAnnotation(schema, colorAccessor) === true
) { ) {
return null; return null;
} }
@@ -459,14 +468,12 @@ class CategoryValue extends React.Component {
return ( return (
<MiniHistogram <MiniHistogram
/* eslint-disable react/jsx-props-no-spreading -- Disable unneeded on next release of eslint-config-airbnb */
{...{ {...{
colorScale, colorScale,
xScale, xScale,
yScale, yScale,
bins, bins,
}} }}
/* eslint-enable react/jsx-props-no-spreading -- enable */
obsOrVarContinuousFieldDisplayName={colorAccessor} obsOrVarContinuousFieldDisplayName={colorAccessor}
domainLabel={label} domainLabel={label}
height={VALUE_HEIGHT} height={VALUE_HEIGHT}
@@ -562,7 +569,7 @@ class CategoryValue extends React.Component {
<div style={{ display: "flex", alignItems: "baseline" }}> <div style={{ display: "flex", alignItems: "baseline" }}>
<label <label
htmlFor={valueToggleLabel} htmlFor={valueToggleLabel}
className="bp3-control bp3-checkbox" className={`${Classes.CONTROL} ${Classes.CHECKBOX}`}
style={{ margin: 0 }} style={{ margin: 0 }}
> >
<input <input
@@ -574,7 +581,7 @@ class CategoryValue extends React.Component {
type="checkbox" type="checkbox"
/> />
<span <span
className="bp3-control-indicator" className={Classes.CONTROL_INDICATOR}
onMouseEnter={this.handleMouseExit} onMouseEnter={this.handleMouseExit}
onMouseLeave={this.handleMouseEnter} onMouseLeave={this.handleMouseEnter}
/> />
@@ -3,10 +3,10 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import * as d3 from "d3"; import * as d3 from "d3";
import { import {
Classes,
Popover, Popover,
PopoverInteractionKind, PopoverInteractionKind,
Position, Position,
Classes,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
@connect((state) => ({ @connect((state) => ({
@@ -18,8 +18,8 @@ class Occupancy extends React.PureComponent {
_HEIGHT = 11; _HEIGHT = 11;
createHistogram = () => { createHistogram = () => {
/* /*
Knowing that colorScale is based off continous data, Knowing that colorScale is based off continous data,
createHistogram fetches the continous data in relation to the cells releveant to the catagory value. createHistogram fetches the continous data in relation to the cells releveant to the catagory value.
It then seperates that data into 50 bins for drawing the mini-histogram It then seperates that data into 50 bins for drawing the mini-histogram
*/ */
@@ -75,8 +75,8 @@ class Occupancy extends React.PureComponent {
}; };
createOccupancyStack = () => { createOccupancyStack = () => {
/* /*
Knowing that the color scale is based off of catagorical data, Knowing that the color scale is based off of catagorical data,
createOccupancyStack obtains a map showing the number if cells per colored value createOccupancyStack obtains a map showing the number if cells per colored value
Using the colorScale a stack of colored bars is drawn representing the map Using the colorScale a stack of colored bars is drawn representing the map
*/ */
@@ -155,7 +155,7 @@ class Occupancy extends React.PureComponent {
popoverClassName={Classes.POPOVER_CONTENT_SIZING} popoverClassName={Classes.POPOVER_CONTENT_SIZING}
> >
<canvas <canvas
className="bp3-popover-targer" className={Classes.POPOVER_TARGET}
style={{ style={{
marginRight: 5, marginRight: 5,
width: this._WIDTH, width: this._WIDTH,
+25 -58
View File
@@ -11,20 +11,20 @@ import {
// create continuous color legend // create continuous color legend
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f // http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
const continuous = (selectorId, colorscale, colorAccessor) => { const continuous = (selectorId, colorScale, colorAccessor) => {
const legendheight = 200; const legendHeight = 200;
const legendwidth = 80; const legendWidth = 80;
const margin = { top: 10, right: 60, bottom: 10, left: 2 }; const margin = { top: 10, right: 60, bottom: 10, left: 2 };
const canvas = d3 const canvas = d3
.select(selectorId) .select(selectorId)
.style("height", `${legendheight}px`) .style("height", `${legendHeight}px`)
.style("width", `${legendwidth}px`) .style("width", `${legendWidth}px`)
.append("canvas") .append("canvas")
.attr("height", legendheight - margin.top - margin.bottom) .attr("height", legendHeight - margin.top - margin.bottom)
.attr("width", 1) .attr("width", 1)
.style("height", `${legendheight - margin.top - margin.bottom}px`) .style("height", `${legendHeight - margin.top - margin.bottom}px`)
.style("width", `${legendwidth - margin.left - margin.right}px`) .style("width", `${legendWidth - margin.left - margin.right}px`)
.style("position", "absolute") .style("position", "absolute")
.style("top", `${margin.top + 1}px`) .style("top", `${margin.top + 1}px`)
.style("left", `${margin.left + 1}px`) .style("left", `${margin.left + 1}px`)
@@ -37,18 +37,18 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const legendscale = d3 const legendScale = d3
.scaleLinear() .scaleLinear()
.range([1, legendheight - margin.top - margin.bottom]) .range([1, legendHeight - margin.top - margin.bottom])
.domain([ .domain([
colorscale.domain()[1], colorScale.domain()[1],
colorscale.domain()[0], colorScale.domain()[0],
]); /* we flip this to make viridis colors dark if high in the color scale */ ]); /* we flip this to make viridis colors dark if high in the color scale */
// image data hackery based on http://bl.ocks.org/mbostock/048d21cf747371b11884f75ad896e5a5 // image data hackery based on http://bl.ocks.org/mbostock/048d21cf747371b11884f75ad896e5a5
const image = ctx.createImageData(1, legendheight); const image = ctx.createImageData(1, legendHeight);
d3.range(legendheight).forEach((i) => { d3.range(legendHeight).forEach((i) => {
const c = d3.rgb(colorscale(legendscale.invert(i))); const c = d3.rgb(colorScale(legendScale.invert(i)));
image.data[4 * i] = c.r; image.data[4 * i] = c.r;
image.data[4 * i + 1] = c.g; image.data[4 * i + 1] = c.g;
image.data[4 * i + 2] = c.b; image.data[4 * i + 2] = c.b;
@@ -66,20 +66,20 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
}); });
*/ */
const legendaxis = d3 const legendAxis = d3
.axisRight(legendscale) .axisRight(legendScale)
.ticks(6) .ticks(6)
.tickFormat( .tickFormat(
d3.format( d3.format(
legendscale.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : "," legendScale.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
) )
); );
const svg = d3 const svg = d3
.select(selectorId) .select(selectorId)
.append("svg") .append("svg")
.attr("height", `${legendheight}px`) .attr("height", `${legendHeight}px`)
.attr("width", `${legendwidth}px`) .attr("width", `${legendWidth}px`)
.style("position", "absolute") .style("position", "absolute")
.style("left", "0px") .style("left", "0px")
.style("top", "0px"); .style("top", "0px");
@@ -89,16 +89,16 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
.attr("class", "axis") .attr("class", "axis")
.attr( .attr(
"transform", "transform",
`translate(${legendwidth - margin.left - margin.right + 3},${margin.top})` `translate(${legendWidth - margin.left - margin.right + 3},${margin.top})`
) )
.call(legendaxis); .call(legendAxis);
// text label for the y axis // text label for the y axis
svg svg
.append("text") .append("text")
.attr("transform", "rotate(-90)") .attr("transform", "rotate(-90)")
.attr("y", 2) .attr("y", 2)
.attr("x", 0 - legendheight / 2) .attr("x", 0 - legendHeight / 2)
.attr("dy", "1em") .attr("dy", "1em")
.style("text-anchor", "middle") .style("text-anchor", "middle")
.style("fill", "white") .style("fill", "white")
@@ -110,24 +110,7 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
colors: state.colors, colors: state.colors,
})) }))
class ContinuousLegend extends React.Component { class ContinuousLegend extends React.Component {
constructor(props) { async componentDidUpdate(prevProps) {
super(props);
this.ref = null;
this.state = {
colorAccessor: null,
colorScale: null,
};
}
componentDidMount() {
this.updateState(null);
}
componentDidUpdate(prevProps) {
this.updateState(prevProps);
}
async updateState(prevProps) {
const { annoMatrix, colors } = this.props; const { annoMatrix, colors } = this.props;
if (!colors || !annoMatrix) return; if (!colors || !annoMatrix) return;
@@ -161,35 +144,19 @@ class ContinuousLegend extends React.Component {
); );
} }
} }
this.setState({
colorAccessor,
colorScale: colorTable.scale,
});
} }
} }
render() { render() {
const { colorAccessor, colorScale } = this.state;
if (
colorScale?.domain &&
colorScale.domain()[1] === colorScale.domain()[0]
) {
/* it's a single value, not a distribution, min max are the same */
return null;
}
return ( return (
<div <div
id="continuous_legend" id="continuous_legend"
ref={(ref) => {
this.ref = ref;
}}
style={{ style={{
display: colorAccessor ? "inherit" : "none",
position: "absolute", position: "absolute",
left: 8, left: 8,
top: 35, top: 35,
zIndex: 1, zIndex: 1,
pointerEvents: "none",
}} }}
/> />
); );
+5 -4
View File
@@ -2,13 +2,14 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useAsync } from "react-async"; import { useAsync } from "react-async";
import { import {
ButtonGroup,
Popover,
Button, Button,
ButtonGroup,
H4,
Popover,
Position,
Radio, Radio,
RadioGroup, RadioGroup,
Tooltip, Tooltip,
Position,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as globals from "../../globals"; import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
@@ -80,7 +81,7 @@ class Embedding extends React.PureComponent {
width: 400, width: 400,
}} }}
> >
<h1>Embedding Choice</h1> <H4>Embedding Choice</H4>
<p style={{ fontStyle: "italic" }}> <p style={{ fontStyle: "italic" }}>
There are {schema?.dataframe?.nObs} cells in the entire dataset. There are {schema?.dataframe?.nObs} cells in the entire dataset.
</p> </p>
@@ -6,11 +6,12 @@ import fuzzysort from "fuzzysort";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Suggest } from "@blueprintjs/select"; import { Suggest } from "@blueprintjs/select";
import { import {
MenuItem,
Button, Button,
ControlGroup,
FormGroup, FormGroup,
InputGroup, InputGroup,
ControlGroup, Intent,
MenuItem,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as globals from "../../globals"; import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
@@ -278,7 +279,7 @@ class AddGenes extends React.Component {
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
/> />
<Button <Button
className="bp3-button bp3-intent-primary" intent={Intent.PRIMARY}
data-testid="add-gene" data-testid="add-gene"
loading={userDefinedGenesLoading} loading={userDefinedGenesLoading}
onClick={() => this.handleClick(activeItem)} onClick={() => this.handleClick(activeItem)}
+16 -6
View File
@@ -14,7 +14,7 @@ export default function drawPointsRegl(regl) {
uniform float nPoints; uniform float nPoints;
uniform float minViewportDimension; uniform float minViewportDimension;
varying vec4 fragColor; varying lowp vec4 fragColor;
const float zBottom = 0.99; const float zBottom = 0.99;
const float zMiddle = 0.; const float zMiddle = 0.;
@@ -27,23 +27,23 @@ export default function drawPointsRegl(regl) {
${glPointSize} ${glPointSize}
void main() { void main() {
bool isNaN, isSelected, isHighlight; bool isBackground, isSelected, isHighlight;
getFlags(flag, isNaN, isSelected, isHighlight); getFlags(flag, isBackground, isSelected, isHighlight);
float size = pointSize(nPoints, minViewportDimension, isSelected, isHighlight); float size = pointSize(nPoints, minViewportDimension, isSelected, isHighlight);
gl_PointSize = size * pow(distance, 0.5); gl_PointSize = size * pow(distance, 0.5);
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle); float z = isBackground ? zBottom : (isHighlight ? zTop : zMiddle);
vec3 xy = projView * vec3(position, 1.); vec3 xy = projView * vec3(position, 1.);
gl_Position = vec4(xy.xy, z, 1.); gl_Position = vec4(xy.xy, z, 1.);
float alpha = isNaN ? 0.9 : 1.0; float alpha = isBackground ? 0.9 : 1.0;
fragColor = vec4(color, alpha); fragColor = vec4(color, alpha);
}`, }`,
frag: ` frag: `
precision mediump float; precision mediump float;
varying vec4 fragColor; varying lowp vec4 fragColor;
void main() { void main() {
if (length(gl_PointCoord.xy - 0.5) > 0.5) { if (length(gl_PointCoord.xy - 0.5) > 0.5) {
discard; discard;
@@ -67,5 +67,15 @@ export default function drawPointsRegl(regl) {
count: regl.prop("count"), count: regl.prop("count"),
primitive: "points", primitive: "points",
blend: {
enable: true,
func: {
srcRGB: "src alpha",
srcAlpha: 1,
dstRGB: 0,
dstAlpha: "zero",
},
},
}); });
} }
+30 -13
View File
@@ -22,6 +22,12 @@ import CentroidLabels from "./overlays/centroidLabels";
import actions from "../../actions"; import actions from "../../actions";
import renderThrottle from "../../util/renderThrottle"; import renderThrottle from "../../util/renderThrottle";
import {
flagBackground,
flagSelected,
flagHighlight,
} from "../../util/glHelpers";
/* /*
Simple 2D transforms control all point painting. There are three: Simple 2D transforms control all point painting. There are three:
* model - convert from underlying per-point coordinate to a layout. * model - convert from underlying per-point coordinate to a layout.
@@ -62,10 +68,6 @@ function createModelTF() {
return m; return m;
} }
const flagSelected = 1;
const flagNaN = 2;
const flagHighlight = 4;
@connect((state) => ({ @connect((state) => ({
annoMatrix: state.annoMatrix, annoMatrix: state.annoMatrix,
crossfilter: state.obsCrossfilter, crossfilter: state.obsCrossfilter,
@@ -159,8 +161,9 @@ class Graph extends React.Component {
const flags = new Float32Array(nObs); const flags = new Float32Array(nObs);
if (colorByData) { if (colorByData) {
for (let i = 0, len = flags.length; i < len; i += 1) { for (let i = 0, len = flags.length; i < len; i += 1) {
if (!Number.isFinite(colorByData[i])) { const val = colorByData[i];
flags[i] = flagNaN; if (typeof val === "number" && !Number.isFinite(val)) {
flags[i] = flagBackground;
} }
} }
} }
@@ -730,14 +733,28 @@ class Graph extends React.Component {
); );
}); });
updateReglAndRender(asyncProps) { updateReglAndRender(asyncProps, prevAsyncProps) {
const { positions, colors, flags } = asyncProps; const { positions, colors, flags, height, width } = asyncProps;
this.cachedAsyncProps = asyncProps; this.cachedAsyncProps = asyncProps;
const { pointBuffer, colorBuffer, flagBuffer } = this.state; const { pointBuffer, colorBuffer, flagBuffer } = this.state;
pointBuffer({ data: positions, dimension: 2 }); let needToRenderCanvas = false;
colorBuffer({ data: colors, dimension: 3 });
flagBuffer({ data: flags, dimension: 1 }); if (height !== prevAsyncProps?.height || width !== prevAsyncProps?.width) {
this.renderCanvas(); needToRenderCanvas = true;
}
if (positions !== prevAsyncProps?.positions) {
pointBuffer({ data: positions, dimension: 2 });
needToRenderCanvas = true;
}
if (colors !== prevAsyncProps?.colors) {
colorBuffer({ data: colors, dimension: 3 });
needToRenderCanvas = true;
}
if (flags !== prevAsyncProps?.flags) {
flagBuffer({ data: flags, dimension: 1 });
needToRenderCanvas = true;
}
if (needToRenderCanvas) this.renderCanvas();
} }
updateColorTable(colors, colorDf) { updateColorTable(colors, colorDf) {
@@ -906,7 +923,7 @@ class Graph extends React.Component {
<Async.Fulfilled> <Async.Fulfilled>
{(asyncProps) => { {(asyncProps) => {
if (regl && !shallowEqual(asyncProps, this.cachedAsyncProps)) { if (regl && !shallowEqual(asyncProps, this.cachedAsyncProps)) {
this.updateReglAndRender(asyncProps); this.updateReglAndRender(asyncProps, this.cachedAsyncProps);
} }
return null; return null;
}} }}
+24 -5
View File
@@ -10,6 +10,7 @@ const Lasso = () => {
let lassoPolygon; let lassoPolygon;
let lassoPath; let lassoPath;
let closePath; let closePath;
let lassoInProgress;
const polygonToPath = (polygon) => const polygonToPath = (polygon) =>
`M${polygon.map((d) => d.join(",")).join("L")}`; `M${polygon.map((d) => d.join(",")).join("L")}`;
@@ -25,8 +26,18 @@ const Lasso = () => {
lassoPolygon = [d3.mouse(svg.node())]; // current x y of mouse within element lassoPolygon = [d3.mouse(svg.node())]; // current x y of mouse within element
if (lassoPath) { if (lassoPath) {
// If the existing path is in progress
if (lassoInProgress) {
// cancel the existing lasso
handleCancel();
// Don't continue with current drag start
return;
}
lassoPath.remove(); lassoPath.remove();
} }
// We're starting a new drag
lassoInProgress = true;
lassoPath = g lassoPath = g
.append("path") .append("path")
@@ -67,25 +78,33 @@ const Lasso = () => {
} }
}; };
const handleCancel = () => {
lassoPath.remove();
closePath = closePath?.remove();
lassoPath = null;
lassoPolygon = null;
closePath = null;
dispatch.call("cancel");
};
const handleDragEnd = () => { const handleDragEnd = () => {
// remove the close path // remove the close path
closePath.remove(); closePath.remove();
closePath = null; closePath = null;
// succesfully closed // successfully closed
if ( if (
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) < distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
closeDistance closeDistance
) { ) {
lassoInProgress = false;
lassoPath.attr("d", `${polygonToPath(lassoPolygon)}Z`); lassoPath.attr("d", `${polygonToPath(lassoPolygon)}Z`);
dispatch.call("end", lasso, lassoPolygon); dispatch.call("end", lasso, lassoPolygon);
// otherwise cancel // otherwise cancel
} else { } else {
lassoPath.remove(); handleCancel();
lassoPath = null;
lassoPolygon = null;
dispatch.call("cancel");
} }
}; };
@@ -0,0 +1,63 @@
import React, { PureComponent } from "react";
import { connect } from "react-redux";
import { Drawer } from "@blueprintjs/core";
import InfoFormat from "./infoFormat";
import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers";
@connect((state) => {
return {
schema: state.annoMatrix.schema,
datasetTitle: state.config?.displayNames?.dataset ?? "",
aboutURL: state.config?.links?.["about-dataset"],
isOpen: state.controls.datasetDrawer,
dataPortalProps: state.config?.["corpora_props"],
};
})
class InfoDrawer extends PureComponent {
handleClose = () => {
const { dispatch } = this.props;
dispatch({ type: "toggle dataset drawer" });
};
render() {
const {
position,
aboutURL,
datasetTitle,
schema,
isOpen,
dataPortalProps,
} = this.props;
const allCategoryNames = selectableCategoryNames(schema).sort();
const singleValueCategories = new Map();
allCategoryNames.forEach((catName) => {
const isUserAnno = schema?.annotations?.obsByName[catName]?.writable;
const colSchema = schema.annotations.obsByName[catName];
if (!isUserAnno && colSchema.categories?.length === 1) {
singleValueCategories.set(catName, colSchema.categories[0]);
}
});
return (
<Drawer
title="Dataset Overview"
onClose={this.handleClose}
{...{ isOpen, position }}
>
<InfoFormat
{...{
datasetTitle,
aboutURL,
singleValueCategories,
dataPortalProps: dataPortalProps ?? {},
}}
/>
</Drawer>
);
}
}
export default InfoDrawer;
@@ -0,0 +1,199 @@
import { H3, H1, UL, HTMLTable, Classes } from "@blueprintjs/core";
import React from "react";
const renderContributors = (contributors, affiliations) => {
// eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII
if (!contributors || contributors.length === 0 || true) return null;
return (
<>
<H3>Contributors</H3>
<p>
{contributors.map((contributor) => {
const { email, name, institution } = contributor;
return (
<span key={name}>
{name}
{email && `(${email})`}
<sup>{affiliations.indexOf(institution) + 1}</sup>
</span>
);
})}
</p>
{renderAffiliations(affiliations)}
</>
);
};
// generates a list of unique institutions by order of appearance in contributors
const buildAffiliations = (contributors = []) => {
const affiliations = [];
contributors.forEach((contributor) => {
const { institution } = contributor;
if (affiliations.indexOf(institution) === -1) {
affiliations.push(institution);
}
});
return affiliations;
};
const renderAffiliations = (affiliations) => {
if (affiliations.length === 0) return null;
return (
<>
<H3>Affiliations</H3>
<UL>
{affiliations.map((item, index) => (
<div key={item}>
<sup>{index + 1}</sup>
{" "}
{item}
</div>
))}
</UL>
</>
);
};
const renderDOILink = (type, doi) => {
if (!doi) return null;
return (
<>
<H3>{type}</H3>
<p>
<a href={doi} target="_blank" rel="noopener">
{doi}
</a>
</p>
</>
);
};
const ONTOLOGY_KEY = "ontology_term_id";
// Render list of metadata attributes found in categorical field
const renderDatasetMetadata = (singleValueCategories, corporaMetadata) => {
if (singleValueCategories.size === 0) return null;
return (
<>
<H3>Dataset Metadata</H3>
<HTMLTable
striped
condensed
style={{ display: "block", width: "100%", overflowX: "auto" }}
>
<thead>
<tr>
<th>Field</th>
<th>Label</th>
<th>Ontology ID</th>
</tr>
</thead>
<tbody>
{Object.entries(corporaMetadata).map(([key, value]) => {
return (
<tr {...{ key }}>
<td>{`${key}:`}</td>
<td>{value}</td>
<td />
</tr>
);
})}
{Array.from(singleValueCategories).reduce((elems, pair) => {
const [category, value] = pair;
// If the value is empty skip it
if (!value) return elems;
// If this category is a ontology term, let's add its value to the previous node
if (String(category).includes(ONTOLOGY_KEY)) {
const prevElem = elems.pop();
const newChildren = [...prevElem.props.children];
newChildren.splice(2, 1, [<td key="ontology">{value}</td>]);
// Props aren't extensible so we must clone and alter the component to append the new child
elems.push(
React.cloneElement(prevElem, prevElem.props, newChildren)
);
} else {
// Create the list item
elems.push(
<tr key={category}>
<td>{`${category}:`}</td>
<td>{value}</td>
<td />
</tr>
);
}
return elems;
}, [])}
</tbody>
</HTMLTable>
</>
);
};
// Renders any links found in the config where link_type is not "SUMMARY"
// If there are no links in the config, render the aboutURL
const renderLinks = (projectLinks, aboutURL) => {
if (!projectLinks && !aboutURL) return null;
if (projectLinks)
return (
<>
<H3>Project Links</H3>
<UL>
{projectLinks.map((link) => {
if (link.link_type === "SUMMARY") return null;
return (
<li key={link.link_name}>
<a href={link.link_url} target="_blank" rel="noopener">
{link.link_name}
</a>
</li>
);
})}
</UL>
</>
);
return (
<>
<H3>More Info</H3>
<p>
<a href={aboutURL} target="_blank" rel="noopener">
{aboutURL}
</a>
</p>
</>
);
};
const InfoFormat = React.memo(
({ datasetTitle, singleValueCategories, aboutURL, dataPortalProps = {} }) => {
if (["1.0.0", "1.1.0"].indexOf(dataPortalProps.version?.["corpora_schema_version"]) === -1) {
dataPortalProps = {};
}
const {
title,
publication_doi: doi,
preprint_doi: preprintDOI,
organism,
contributors,
project_links: projectLinks,
} = dataPortalProps;
const affiliations = buildAffiliations(contributors);
return (
<div className={Classes.DIALOG_BODY}>
<div className={Classes.DIALOG_BODY}>
<H1>{title ?? datasetTitle}</H1>
{renderContributors(contributors, affiliations)}
{renderDatasetMetadata(singleValueCategories, { organism })}
{renderLinks(projectLinks, aboutURL)}
{renderDOILink("DOI", doi)}
{renderDOILink("Preprint DOI", preprintDOI)}
</div>
</div>
);
}
);
export default InfoFormat;
@@ -0,0 +1,72 @@
// jshint esversion: 6
import React from "react";
import { Button, Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
const InformationMenu = React.memo((props) => {
const { libraryVersions, tosURL, privacyURL } = props;
return (
<Popover
content={
<Menu>
<MenuItem
href="https://chanzuckerberg.github.io/cellxgene/"
target="_blank"
icon="book"
text="Documentation"
rel="noopener"
/>
<MenuItem
href="https://join-cellxgene-users.herokuapp.com/"
target="_blank"
icon="chat"
text="Chat"
rel="noopener"
/>
<MenuItem
href="https://github.com/chanzuckerberg/cellxgene"
target="_blank"
icon="git-branch"
text="Github"
rel="noopener"
/>
<MenuItem target="_blank" text={libraryVersions?.cellxgene || null} />
<MenuItem text="MIT License" />
{tosURL && (
<MenuItem
href={tosURL}
target="_blank"
text="Terms of Service"
rel="noopener"
/>
)}
{privacyURL && (
<MenuItem
href={privacyURL}
target="_blank"
text="Privacy Policy"
rel="noopener"
/>
)}
</Menu>
}
position={Position.BOTTOM_RIGHT}
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<Button
data-testid="menu"
type="button"
icon={IconNames.INFO_SIGN}
style={{
cursor: "pointer",
verticalAlign: "middle",
}}
/>
</Popover>
);
});
export default InformationMenu;
@@ -1,22 +1,44 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Button } from "@blueprintjs/core";
import * as globals from "../../globals"; import * as globals from "../../globals";
import Logo from "../framework/logo"; import Logo from "../framework/logo";
import Truncate from "../util/truncate"; import Truncate from "../util/truncate";
import InfoDrawer from "../infoDrawer/infoDrawer";
import InformationMenu from "./infoMenu";
const DATASET_TITLE_WIDTH = 190;
const DATASET_TITLE_FONT_SIZE = 14; const DATASET_TITLE_FONT_SIZE = 14;
@connect((state) => ({ @connect((state) => {
datasetTitle: state.config?.displayNames?.dataset ?? "", const { corpora_props: corporaProps } = state.config;
aboutURL: state.config?.links?.["about-dataset"], const correctVersion =
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, ["1.0.0", "1.1.0"].indexOf(corporaProps?.version?.["corpora_schema_version"]) > -1;
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, return {
})) datasetTitle: state.config?.displayNames?.dataset ?? "",
libraryVersions: state.config?.["library_versions"],
aboutLink: state.config?.links?.["about-dataset"],
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
title: correctVersion ? corporaProps?.title : undefined,
};
})
class LeftSideBar extends React.Component { class LeftSideBar extends React.Component {
handleClick = () => {
const { dispatch } = this.props;
dispatch({ type: "toggle dataset drawer" });
};
render() { render() {
const { datasetTitle, aboutURL } = this.props; const {
datasetTitle,
libraryVersions,
aboutLink,
privacyURL,
tosURL,
dispatch,
title,
} = this.props;
return ( return (
<div <div
@@ -26,65 +48,64 @@ class LeftSideBar extends React.Component {
width: globals.leftSidebarWidth, width: globals.leftSidebarWidth,
zIndex: 1, zIndex: 1,
borderBottom: `1px solid ${globals.lighterGrey}`, borderBottom: `1px solid ${globals.lighterGrey}`,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}} }}
> >
<Logo size={30} /> <div>
<span <Logo size={28} />
style={{
fontSize: 28,
position: "relative",
top: -6,
fontWeight: "bold",
marginLeft: 5,
color: globals.logoColor,
userSelect: "none",
}}
>
cell
<span <span
style={{ style={{
position: "relative",
top: 1,
fontWeight: 300,
fontSize: 24, fontSize: 24,
position: "relative",
top: -6,
fontWeight: "bold",
marginLeft: 5,
color: globals.logoColor,
userSelect: "none",
}} }}
> >
× cell
<span
style={{
position: "relative",
top: 1,
fontWeight: 300,
fontSize: 24,
}}
>
×
</span>
gene
</span> </span>
gene </div>
</span> <div style={{ marginRight: 5, height: "100%" }}>
<div <Button
style={{ minimal
fontSize: DATASET_TITLE_FONT_SIZE, style={{
position: "relative", fontSize: DATASET_TITLE_FONT_SIZE,
top: -6, position: "relative",
display: "inline-block", top: -1,
width: DATASET_TITLE_WIDTH, }}
marginLeft: "7px", onClick={this.handleClick}
height: "1.2em", >
overflow: "hidden",
wordBreak: "break-all",
}}
>
{aboutURL ? (
<Truncate> <Truncate>
<a <span style={{ maxWidth: 155 }} data-testid="header">
style={{ width: 185 }} {title ?? datasetTitle}
href={aboutURL}
data-testid="header"
target="_blank"
rel="noopener noreferrer"
>
{datasetTitle}
</a>
</Truncate>
) : (
<Truncate>
<span style={{ width: 185 }} data-testid="header">
{datasetTitle}
</span> </span>
</Truncate> </Truncate>
)} </Button>
<InfoDrawer />
<InformationMenu
{...{
libraryVersions,
aboutLink,
tosURL,
privacyURL,
dispatch,
}}
/>
</div> </div>
</div> </div>
); );
+165 -22
View File
@@ -1,32 +1,175 @@
import React from "react"; import React, { useState } from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core";
import {
AnchorButton,
Button,
MenuItem,
Tooltip,
Popover,
Menu,
Elevation,
PopoverPosition,
Checkbox,
Card,
} from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../globals"; import * as globals from "../../globals";
import styles from "./menubar.css"; import styles from "./menubar.css";
import { storageGet, storageSet, KEYS } from "../util/localStorage";
const BASE_EMOJI = [0x1f9d1, 0x1f468, 0x1f469];
const SKIN_TONES = [0x1f3fb, 0x1f3fc, 0x1f3fd, 0x1f3fe, 0x1f3ff];
const MICROSCOPE = 0x1f52c;
const ZERO_WIDTH_JOINER = 0x0200d;
const LOGIN_PROMPT_OFF = "off";
const Auth = React.memo((props) => { const Auth = React.memo((props) => {
const { auth } = props; const [isPromptOpen, setIsPromptOpen] = useState(shouldShowPrompt());
if (!auth || (auth && !auth.requires_client_login)) return null; const { auth, userInfo } = props;
return ( const isAuthenticated = userInfo && userInfo.is_authenticated;
<div className={`bp3-button-group ${styles.menubarButton}`}>
<Tooltip window.userInfo = userInfo;
content="Log in or log out of cellxgene"
position="bottom" const randomInt = Math.random() * 15;
hoverOpenDelay={globals.tooltipHoverOpenDelay} const sexIndex = Math.floor(randomInt / 5);
> const skinToneIndex = Math.floor(randomInt % 5);
<AnchorButton
type="button" const scientist = String.fromCodePoint(
data-testid="auth-button" BASE_EMOJI[sexIndex],
disabled={false} SKIN_TONES[skinToneIndex],
icon={!auth.is_authenticated ? "log-in" : "log-out"} ZERO_WIDTH_JOINER,
href={!auth.is_authenticated ? auth.login : auth.logout} MICROSCOPE
>
{!auth.is_authenticated ? "Log In" : "Log Out"}
</AnchorButton>
</Tooltip>
</div>
); );
if (!shouldShowAuth()) return null;
if (isAuthenticated) {
const PopoverContent = (
<Menu>
<MenuItem
data-testid="user-email"
text={`Logged in as: ${userInfo.email}`}
/>
<MenuItem
data-testid="log-out"
text="Log Out"
href={auth.logout}
icon={IconNames.LOG_OUT}
/>
</Menu>
);
return (
<Popover content={PopoverContent}>
<Button
data-testid="user-info"
className={styles.menubarButton}
style={{ padding: 0 }}
>
{/* eslint-disable-next-line no-constant-condition -- disable profile picture until CSP is tweaked */}
{userInfo?.picture && false ? (
<img alt="profile" size="21px" src={userInfo?.picture} />
) : (
<span style={{ fontSize: "18px" }}>{scientist}</span>
)}
</Button>
</Popover>
);
}
const LoginButton = (
<Tooltip
content="Log in to cellxgene"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="log-in"
href={auth.login}
className={styles.menubarButton}
>
Log In
</AnchorButton>
</Tooltip>
);
if (isPromptOpen) {
return (
<Popover
position={PopoverPosition.AUTO_END}
isOpen
content={<PromptContent setIsPromptOpen={setIsPromptOpen} />}
onInteraction={setIsPromptOpen}
>
{LoginButton}
</Popover>
);
}
return LoginButton;
function shouldShowAuth() {
return auth && auth.requires_client_login;
}
function shouldShowPrompt() {
if (storageGet(KEYS.LOGIN_PROMPT) === LOGIN_PROMPT_OFF) return false;
return shouldShowAuth && !isAuthenticated;
}
}); });
function PromptContent({ setIsPromptOpen }) {
const [isChecked, setIsChecked] = useState(false);
function handleOKClick() {
if (isChecked) {
storageSet(KEYS.LOGIN_PROMPT, LOGIN_PROMPT_OFF);
}
setIsPromptOpen(false);
}
function handleCheckboxChange() {
setIsChecked(!isChecked);
}
return (
<Card style={{ width: "500px" }} elevation={Elevation.TWO}>
<p>
Logging in will enable you to create your own categories and labels.
Logging in later will reset cellxgene to the default view and cause you
to lose progress.
</p>
<Checkbox
style={{ width: "230px" }}
checked={isChecked}
onChange={handleCheckboxChange}
data-testid="login-hint-do-not-show-again"
>
Do not show me this message again
</Checkbox>
<div
style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}
>
<Button
onClick={handleOKClick}
intent="primary"
data-testid="login-hint-yes"
>
Acknowledge
</Button>
</div>
</Card>
);
}
export default Auth; export default Auth;
+14 -9
View File
@@ -1,12 +1,16 @@
import React from "react"; import React from "react";
import { import {
Position,
Button, Button,
Popover, ButtonGroup,
NumericInput,
Icon, Icon,
Intent,
NumericInput,
Popover,
Position,
Tooltip, Tooltip,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { tooltipHoverOpenDelay } from "../../globals"; import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css"; import styles from "./menubar.css";
@@ -28,13 +32,13 @@ const Clip = React.memo((props) => {
pendingClipPercentiles?.clipPercentileMin ?? clipPercentileMin; pendingClipPercentiles?.clipPercentileMin ?? clipPercentileMin;
const clipMax = const clipMax =
pendingClipPercentiles?.clipPercentileMax ?? clipPercentileMax; pendingClipPercentiles?.clipPercentileMax ?? clipPercentileMax;
const activeClipClass = const intent =
clipPercentileMin > 0 || clipPercentileMax < 100 clipPercentileMin > 0 || clipPercentileMax < 100
? " bp3-intent-warning" ? Intent.INTENT_WARNING
: ""; : Intent.NONE;
return ( return (
<div className={`bp3-button-group ${styles.menubarButton}`}> <ButtonGroup className={`${styles.menubarButton}`}>
<Popover <Popover
target={ target={
<Tooltip <Tooltip
@@ -45,7 +49,8 @@ const Clip = React.memo((props) => {
<Button <Button
type="button" type="button"
data-testid="visualization-settings" data-testid="visualization-settings"
className={`bp3-button bp3-icon-timeline-bar-chart ${activeClipClass}`} intent={intent}
icon={IconNames.TIMELINE_BAR_CHART}
style={{ style={{
cursor: "pointer", cursor: "pointer",
}} }}
@@ -126,7 +131,7 @@ const Clip = React.memo((props) => {
</div> </div>
} }
/> />
</div> </ButtonGroup>
); );
}); });
+4 -12
View File
@@ -6,8 +6,8 @@ import * as globals from "../../globals";
import styles from "./menubar.css"; import styles from "./menubar.css";
import actions from "../../actions"; import actions from "../../actions";
import Clip from "./clip"; import Clip from "./clip";
import AuthButtons from "./authButtons"; import AuthButtons from "./authButtons";
import InformationMenu from "./infoMenu";
import Subset from "./subset"; import Subset from "./subset";
import UndoRedoReset from "./undoRedo"; import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons"; import DiffexpButtons from "./diffexpButtons";
@@ -42,6 +42,7 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
celllist2: state.differential.celllist2, celllist2: state.differential.celllist2,
libraryVersions: state.config?.["library_versions"], libraryVersions: state.config?.["library_versions"],
auth: state.config?.authentication, auth: state.config?.authentication,
userInfo: state.userInfo,
undoDisabled: state["@@undoable/past"].length === 0, undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0, redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"], aboutLink: state.config?.links?.["about-dataset"],
@@ -203,7 +204,6 @@ class MenuBar extends React.PureComponent {
render() { render() {
const { const {
dispatch, dispatch,
libraryVersions,
disableDiffexp, disableDiffexp,
undoDisabled, undoDisabled,
redoDisabled, redoDisabled,
@@ -211,15 +211,13 @@ class MenuBar extends React.PureComponent {
clipPercentileMin, clipPercentileMin,
clipPercentileMax, clipPercentileMax,
graphInteractionMode, graphInteractionMode,
aboutLink,
showCentroidLabels, showCentroidLabels,
privacyURL,
tosURL,
categoricalSelection, categoricalSelection,
colorAccessor, colorAccessor,
subsetPossible, subsetPossible,
subsetResetPossible, subsetResetPossible,
enableReembedding, enableReembedding,
userInfo,
auth, auth,
} = this.props; } = this.props;
const { pendingClipPercentiles } = this.state; const { pendingClipPercentiles } = this.state;
@@ -246,13 +244,7 @@ class MenuBar extends React.PureComponent {
zIndex: 3, zIndex: 3,
}} }}
> >
<AuthButtons auth={auth} /> <AuthButtons {...{ auth, userInfo }} />
<InformationMenu
libraryVersions={libraryVersions}
aboutLink={aboutLink}
tosURL={tosURL}
privacyURL={privacyURL}
/>
<UndoRedoReset <UndoRedoReset
dispatch={dispatch} dispatch={dispatch}
undoDisabled={undoDisabled} undoDisabled={undoDisabled}
-77
View File
@@ -1,77 +0,0 @@
// jshint esversion: 6
import React from "react";
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
import styles from "./menubar.css";
const InformationMenu = React.memo((props) => {
const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
return (
<div className={`bp3-button-group ${styles.menubarButton}`}>
<Popover
content={
<Menu>
{aboutLink ? (
<MenuItem
href={aboutLink}
target="_blank"
icon="document-open"
text="About this dataset"
/>
) : (
""
)}
<MenuItem
href="https://chanzuckerberg.github.io/cellxgene/"
target="_blank"
icon="help"
text="Help"
/>
<MenuItem
href="https://join-cellxgene-users.herokuapp.com/"
target="_blank"
icon="chat"
text="Chat"
/>
<MenuItem
href="https://github.com/chanzuckerberg/cellxgene"
target="_blank"
icon="git-branch"
text="Github"
/>
<MenuItem
target="_blank"
text={
libraryVersions && libraryVersions.cellxgene
? libraryVersions.cellxgene
: null
}
/>
<MenuItem text="MIT License" />
{tosURL ? (
<MenuItem href={tosURL} target="_blank" text="Terms of Service" />
) : null}
{privacyURL ? (
<MenuItem
href={privacyURL}
target="_blank"
text="Privacy Policy"
/>
) : null}
</Menu>
}
position={Position.BOTTOM_RIGHT}
>
<Button
type="button"
className="bp3-button bp3-icon-info-sign"
style={{
cursor: "pointer",
}}
/>
</Popover>
</div>
);
});
export default InformationMenu;
+6 -5
View File
@@ -1,12 +1,13 @@
import React from "react"; import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core"; import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { tooltipHoverOpenDelay } from "../../globals"; import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css"; import styles from "./menubar.css";
const UndoRedo = React.memo((props) => { const UndoRedo = React.memo((props) => {
const { undoDisabled, redoDisabled, dispatch } = props; const { undoDisabled, redoDisabled, dispatch } = props;
return ( return (
<div className={`bp3-button-group ${styles.menubarButton}`}> <ButtonGroup className={`${styles.menubarButton}`}>
<Tooltip <Tooltip
content="Undo" content="Undo"
position="bottom" position="bottom"
@@ -14,7 +15,7 @@ const UndoRedo = React.memo((props) => {
> >
<AnchorButton <AnchorButton
type="button" type="button"
className="bp3-button bp3-icon-undo" icon={IconNames.UNDO}
disabled={undoDisabled} disabled={undoDisabled}
onClick={() => { onClick={() => {
dispatch({ type: "@@undoable/undo" }); dispatch({ type: "@@undoable/undo" });
@@ -32,7 +33,7 @@ const UndoRedo = React.memo((props) => {
> >
<AnchorButton <AnchorButton
type="button" type="button"
className="bp3-button bp3-icon-redo" icon={IconNames.REDO}
disabled={redoDisabled} disabled={redoDisabled}
onClick={() => { onClick={() => {
dispatch({ type: "@@undoable/redo" }); dispatch({ type: "@@undoable/redo" });
@@ -43,7 +44,7 @@ const UndoRedo = React.memo((props) => {
data-testid="redo" data-testid="redo"
/> />
</Tooltip> </Tooltip>
</div> </ButtonGroup>
); );
}); });
@@ -72,7 +72,6 @@ export default class MiniHistogram extends React.PureComponent {
popoverClassName={Classes.POPOVER_CONTENT_SIZING} popoverClassName={Classes.POPOVER_CONTENT_SIZING}
> >
<canvas <canvas
className="bp3-popover-targer"
style={{ style={{
marginRight: 5, marginRight: 5,
width, width,
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react"; import React from "react";
export default class MiniStackedBar extends React.PureComponent { export default class MiniStackedBar extends React.PureComponent {
@@ -59,7 +58,6 @@ export default class MiniStackedBar extends React.PureComponent {
return ( return (
<canvas <canvas
className="bp3-popover-targer"
style={{ style={{
marginRight: 5, marginRight: 5,
width, width,
@@ -13,7 +13,7 @@ export default function drawPointsRegl(regl) {
uniform float nPoints; uniform float nPoints;
uniform float minViewportDimension; uniform float minViewportDimension;
varying vec4 fragColor; varying lowp vec4 fragColor;
const float zBottom = 0.99; const float zBottom = 0.99;
const float zMiddle = 0.; const float zMiddle = 0.;
@@ -26,22 +26,22 @@ export default function drawPointsRegl(regl) {
${glPointSize} ${glPointSize}
void main() { void main() {
bool isNaN, isSelected, isHighlight; bool isBackground, isSelected, isHighlight;
getFlags(flag, isNaN, isSelected, isHighlight); getFlags(flag, isBackground, isSelected, isHighlight);
gl_PointSize = pointSize(nPoints, minViewportDimension, isSelected, isHighlight); gl_PointSize = pointSize(nPoints, minViewportDimension, isSelected, isHighlight);
float z = isNaN ? zBottom : (isHighlight ? zTop : zMiddle); float z = isBackground ? zBottom : (isHighlight ? zTop : zMiddle);
vec3 xy = projection * vec3(position, 1.); vec3 xy = projection * vec3(position, 1.);
gl_Position = vec4(xy.xy, z, 1.); gl_Position = vec4(xy.xy, z, 1.);
float alpha = isNaN ? 0.9 : 1.0; float alpha = isBackground ? 0.9 : 1.0;
fragColor = vec4(color, alpha); fragColor = vec4(color, alpha);
}`, }`,
frag: ` frag: `
precision mediump float; precision mediump float;
varying vec4 fragColor; varying lowp vec4 fragColor;
void main() { void main() {
if (length(gl_PointCoord.xy - 0.5) > 0.5) { if (length(gl_PointCoord.xy - 0.5) > 0.5) {
discard; discard;
@@ -64,5 +64,15 @@ export default function drawPointsRegl(regl) {
count: regl.prop("count"), count: regl.prop("count"),
primitive: "points", primitive: "points",
blend: {
enable: true,
func: {
srcRGB: "src alpha",
srcAlpha: 1,
dstRGB: 0,
dstAlpha: "zero",
},
},
}); });
} }
@@ -16,10 +16,11 @@ import {
createColorQuery, createColorQuery,
} from "../../util/stateManager/colorHelpers"; } from "../../util/stateManager/colorHelpers";
import renderThrottle from "../../util/renderThrottle"; import renderThrottle from "../../util/renderThrottle";
import {
const flagSelected = 1; flagBackground,
const flagNaN = 2; flagSelected,
const flagHighlight = 4; flagHighlight,
} from "../../util/glHelpers";
function createProjectionTF(viewportWidth, viewportHeight) { function createProjectionTF(viewportWidth, viewportHeight) {
/* /*
@@ -135,8 +136,9 @@ class Scatterplot extends React.PureComponent {
const flags = new Float32Array(nObs); const flags = new Float32Array(nObs);
if (colorByData) { if (colorByData) {
for (let i = 0, len = flags.length; i < len; i += 1) { for (let i = 0, len = flags.length; i < len; i += 1) {
if (!Number.isFinite(colorByData[i])) { const val = colorByData[i];
flags[i] = flagNaN; if (typeof val === "number" && !Number.isFinite(val)) {
flags[i] = flagBackground;
} }
} }
} }
@@ -528,8 +530,8 @@ class Scatterplot extends React.PureComponent {
return ( return (
<ScatterplotAxis <ScatterplotAxis
minimized={minimized} minimized={minimized}
scatterplotYYaccessor={scatterplotXXaccessor} scatterplotYYaccessor={scatterplotYYaccessor}
scatterplotXXaccessor={scatterplotYYaccessor} scatterplotXXaccessor={scatterplotXXaccessor}
xScale={asyncProps.xScale} xScale={asyncProps.xScale}
yScale={asyncProps.yScale} yScale={asyncProps.yScale}
/> />
+6 -25
View File
@@ -8,26 +8,7 @@ import {
Colors, Colors,
Icon, Icon,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import { storageGet, storageSet, KEYS } from "../util/localStorage";
const CookieDecision = "cxg.cookieDecision";
function storageGet(key, defaultValue = null) {
try {
const val = window.localStorage.getItem(key);
if (val === null) return defaultValue;
return val;
} catch (e) {
return defaultValue;
}
}
function storageSet(key, value) {
try {
window.localStorage.setItem(key, value);
} catch {
// continue
}
}
@connect((state) => ({ @connect((state) => ({
tosURL: state.config?.parameters?.["about_legal_tos"], tosURL: state.config?.parameters?.["about_legal_tos"],
@@ -37,7 +18,7 @@ class TermsPrompt extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
const { tosURL, privacyURL } = this.props; const { tosURL, privacyURL } = this.props;
const cookieDecision = storageGet(CookieDecision, null); const cookieDecision = storageGet(KEYS.COOKIE_DECISION, null);
const hasDecided = cookieDecision !== null; const hasDecided = cookieDecision !== null;
this.state = { this.state = {
hasDecided, hasDecided,
@@ -55,7 +36,7 @@ class TermsPrompt extends React.PureComponent {
handleOK = () => { handleOK = () => {
this.setState({ isOpen: false }); this.setState({ isOpen: false });
storageSet(CookieDecision, "yes"); storageSet(KEYS.COOKIE_DECISION, "yes");
if (window.cookieDecisionCallback instanceof Function) { if (window.cookieDecisionCallback instanceof Function) {
try { try {
window.cookieDecisionCallback(); window.cookieDecisionCallback();
@@ -67,7 +48,7 @@ class TermsPrompt extends React.PureComponent {
handleNo = () => { handleNo = () => {
this.setState({ isOpen: false }); this.setState({ isOpen: false });
storageSet(CookieDecision, "no"); storageSet(KEYS.COOKIE_DECISION, "no");
}; };
renderTos() { renderTos() {
@@ -84,7 +65,7 @@ class TermsPrompt extends React.PureComponent {
}} }}
href={tosURL} href={tosURL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener"
> >
terms of service terms of service
</a> </a>
@@ -106,7 +87,7 @@ class TermsPrompt extends React.PureComponent {
}} }}
href={privacyURL} href={privacyURL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener"
> >
privacy policy privacy policy
</a> </a>
@@ -0,0 +1,22 @@
export const KEYS = {
COOKIE_DECISION: "cxg.cookieDecision",
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
};
export function storageGet(key, defaultValue = null) {
try {
const val = window.localStorage.getItem(key);
if (val === null) return defaultValue;
return val;
} catch (e) {
return defaultValue;
}
}
export function storageSet(key, value) {
try {
window.localStorage.setItem(key, value);
} catch {
// continue
}
}
+5 -2
View File
@@ -7,6 +7,8 @@ const SPLIT_STYLE = {
display: "flex", display: "flex",
overflow: "hidden", overflow: "hidden",
justifyContent: "flex-start", justifyContent: "flex-start",
width: "100%", // There are probably additional styles that we don't want to stack
padding: 0,
}; };
const FIRST_HALF_STYLE = { const FIRST_HALF_STYLE = {
@@ -40,7 +42,7 @@ export default (props) => {
) { ) {
throw Error("Only pass a single child with text to Truncate"); throw Error("Only pass a single child with text to Truncate");
} }
const originalString = children.props.children; const originalString = String(children.props.children);
let firstString; let firstString;
let secondString; let secondString;
@@ -58,7 +60,7 @@ export default (props) => {
} }
} }
const inheritedColor = children.props.style.color; const inheritedColor = children.props.style?.color;
const splitStyle = { ...children.props.style, ...SPLIT_STYLE }; const splitStyle = { ...children.props.style, ...SPLIT_STYLE };
const secondHalfContentStyle = { const secondHalfContentStyle = {
@@ -93,6 +95,7 @@ export default (props) => {
preventOverflow: { enabled: false }, preventOverflow: { enabled: false },
hide: { enabled: false }, hide: { enabled: false },
}} }}
targetProps={{ style: children.props.style }}
> >
{newChildren} {newChildren}
</Tooltip> </Tooltip>
+4
View File
@@ -26,6 +26,7 @@ const Annotations = (
categoryBeingEdited: null, categoryBeingEdited: null,
categoryAddingNewLabel: null, categoryAddingNewLabel: null,
labelEditable: { category: null, label: null }, labelEditable: { category: null, label: null },
promptForFilename: true,
}, },
action action
) => { ) => {
@@ -37,10 +38,13 @@ const Annotations = (
action.config.parameters?.[ action.config.parameters?.[
"annotations-data-collection-name-is-read-only" "annotations-data-collection-name-is-read-only"
] ?? false; ] ?? false;
const promptForFilename =
action.config.parameters?.["user_annotation_collection_name_enabled"];
return { return {
...state, ...state,
dataCollectionNameIsReadOnly, dataCollectionNameIsReadOnly,
dataCollectionName, dataCollectionName,
promptForFilename,
}; };
} }
+8
View File
@@ -20,6 +20,8 @@ const Controls = (
scatterplotXXaccessor: null, // just easier to read scatterplotXXaccessor: null, // just easier to read
scatterplotYYaccessor: null, scatterplotYYaccessor: null,
graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */, graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */,
datasetDrawer: false,
}, },
action action
) => { ) => {
@@ -162,6 +164,12 @@ const Controls = (
scatterplotYYaccessor: null, scatterplotYYaccessor: null,
}; };
/**************************
Dataset Drawer
**************************/
case "toggle dataset drawer":
return { ...state, datasetDrawer: !state.datasetDrawer };
default: default:
return state; return state;
} }
+2
View File
@@ -4,6 +4,7 @@ import thunk from "redux-thunk";
import cascadeReducers from "./cascade"; import cascadeReducers from "./cascade";
import undoable from "./undoable"; import undoable from "./undoable";
import config from "./config"; import config from "./config";
import userInfo from "./userInfo";
import annoMatrix from "./annoMatrix"; import annoMatrix from "./annoMatrix";
import obsCrossfilter from "./obsCrossfilter"; import obsCrossfilter from "./obsCrossfilter";
import categoricalSelection from "./categoricalSelection"; import categoricalSelection from "./categoricalSelection";
@@ -41,6 +42,7 @@ const Reducer = undoable(
["pointDilation", pointDialation], ["pointDilation", pointDialation],
["reembedController", reembedController], ["reembedController", reembedController],
["autosave", autosave], ["autosave", autosave],
["userInfo", userInfo],
]), ]),
[ [
"annoMatrix", "annoMatrix",
+26
View File
@@ -0,0 +1,26 @@
const UserInfo = (state = {}, action) => {
switch (action.type) {
case "initial data load start":
return {
...state,
loading: true,
error: null,
};
case "userInfo load complete":
return {
...state,
loading: false,
error: null,
...action.userInfo,
};
case "initial data load error":
return {
...state,
error: action.error,
};
default:
return state;
}
};
export default UserInfo;
+10 -4
View File
@@ -8,18 +8,24 @@ PointFlags:
We want a bitmask-like flag structure, but due to webgl limitations We want a bitmask-like flag structure, but due to webgl limitations
must emulate it with floats. must emulate it with floats.
Supported flags are:
selected: the point is currently selected
highlight: the point is currently highlighted
background: the point is background information
*/ */
// for JS // for JS
export const flagSelected = 1; export const flagSelected = 1;
export const flagNaN = 2; export const flagBackground = 2;
export const flagHighlight = 4; export const flagHighlight = 4;
// for GLSL // for GLSL
export const glPointFlags = ` export const glPointFlags = `
const float flagSelected = 1.; const float flagSelected = 1.;
const float flagNaN = 2.; const float flagBackground = 2.;
const float flagHighlight = 4.; const float flagHighlight = 4.;
bool isLowBitSet(float f) { bool isLowBitSet(float f) {
@@ -32,12 +38,12 @@ export const glPointFlags = `
} }
void getFlags(in float flag, void getFlags(in float flag,
out bool isNaN, out bool isBackground,
out bool isSelected, out bool isSelected,
out bool isHighlight) { out bool isHighlight) {
isSelected = isLowBitSet(flag); isSelected = isLowBitSet(flag);
flag = shiftRightOne(flag); flag = shiftRightOne(flag);
isNaN = isLowBitSet(flag); isBackground = isLowBitSet(flag);
flag = shiftRightOne(flag); flag = shiftRightOne(flag);
isHighlight = isLowBitSet(flag); isHighlight = isLowBitSet(flag);
} }
+22 -8
View File
@@ -55,8 +55,8 @@ create colors scale and RGB array and return as object. Parameters:
* userColors - optional user color table * userColors - optional user color table
Returns: Returns:
{ {
scale: color scale scale: function, mapping label index to color scale
rgb: cell to color mapping rgb: cell label to color mapping
} }
*/ */
function _createColorTable( function _createColorTable(
@@ -70,7 +70,7 @@ function _createColorTable(
case "color by categorical metadata": { case "color by categorical metadata": {
const data = colorByData.col(colorByAccessor).asArray(); const data = colorByData.col(colorByAccessor).asArray();
if (userColors && colorByAccessor in userColors) { if (userColors && colorByAccessor in userColors) {
return createUserColors(data, colorByAccessor, userColors); return createUserColors(data, colorByAccessor, schema, userColors);
} }
return createColorsByCategoricalMetadata(data, colorByAccessor, schema); return createColorsByCategoricalMetadata(data, colorByAccessor, schema);
} }
@@ -91,27 +91,41 @@ function _createColorTable(
} }
export const createColorTable = memoize(_createColorTable); export const createColorTable = memoize(_createColorTable);
/**
* Create two category label-indexed objects:
* - colors: maps label to RGB triplet for that label (used by graph, etc)
* - scale: function which given label returns d3 color scale for label
* Order doesn't matter - everything is keyed by label value.
*/
export function loadUserColorConfig(userColors) { export function loadUserColorConfig(userColors) {
const convertedUserColors = {}; const convertedUserColors = {};
Object.keys(userColors).forEach((category) => { Object.keys(userColors).forEach((category) => {
const [colors, scaleMap] = Object.keys(userColors[category]).reduce( const [colors, scaleMap] = Object.keys(userColors[category]).reduce(
(acc, label, i) => { (acc, label) => {
const color = parseRGB(userColors[category][label]); const color = parseRGB(userColors[category][label]);
acc[0][label] = color; acc[0][label] = color;
acc[1][i] = d3.rgb(255 * color[0], 255 * color[1], 255 * color[2]); acc[1][label] = d3.rgb(255 * color[0], 255 * color[1], 255 * color[2]);
return acc; return acc;
}, },
[{}, {}] [{}, {}]
); );
const scale = (i) => scaleMap[i]; const scale = (label) => scaleMap[label];
convertedUserColors[category] = { colors, scale }; convertedUserColors[category] = { colors, scale };
}); });
return convertedUserColors; return convertedUserColors;
} }
function _createUserColors(data, colorAccessor, userColors) { function _createUserColors(data, colorAccessor, schema, userColors) {
const { colors, scale } = userColors[colorAccessor]; const { colors, scale: scaleByLabel } = userColors[colorAccessor];
const rgb = createRgbArray(data, colors); const rgb = createRgbArray(data, colors);
// color scale function param is INDEX (offset) into schema categories. It is NOT label value.
// See createColorsByCategoricalMetadata() for another example.
const { categories } = schema.annotations.obsByName[colorAccessor];
const categoryMap = new Map();
categories.forEach((label, idx) => categoryMap.set(idx, label));
const scale = (idx) => scaleByLabel(categoryMap.get(idx));
return { rgb, scale }; return { rgb, scale };
} }
const createUserColors = memoize(_createUserColors); const createUserColors = memoize(_createUserColors);
+5 -5
View File
@@ -178,26 +178,26 @@ function promoteTypedArray(o) {
*/ */
if (isFpTypedArray(o) || Array.isArray(o)) return o; if (isFpTypedArray(o) || Array.isArray(o)) return o;
let TyepdArrayCtor; let TypedArrayCtor;
switch (o.constructor) { switch (o.constructor) {
case Int8Array: case Int8Array:
case Uint8Array: case Uint8Array:
case Uint8ClampedArray: case Uint8ClampedArray:
case Int16Array: case Int16Array:
case Uint16Array: case Uint16Array:
TyepdArrayCtor = Float32Array; TypedArrayCtor = Float32Array;
break; break;
case Int32Array: case Int32Array:
case Uint32Array: case Uint32Array:
TyepdArrayCtor = Float64Array; TypedArrayCtor = Float64Array;
break; break;
default: default:
throw new Error("Unexpected data type returned from server."); throw new Error("Unexpected data type returned from server.");
} }
if (o.constructor === TyepdArrayCtor) return o; if (o.constructor === TypedArrayCtor) return o;
return new TyepdArrayCtor(o); return new TypedArrayCtor(o);
} }
export function matrixFBSToDataframe(arrayBuffers) { export function matrixFBSToDataframe(arrayBuffers) {
+2
View File
@@ -1,3 +1,5 @@
## UPDATE (9/30/2020): Starting today, the name Corpora will only be used as the internal project name, with cellxgene Data Portal being the official product name
# CXG Data Format Specification # CXG Data Format Specification
Document Status: _draft_ Document Status: _draft_
+71 -61
View File
@@ -1,4 +1,4 @@
# cellxgene release process # cellxgene Release Process
_This document defines the release process for cellxgene_ _This document defines the release process for cellxgene_
@@ -16,71 +16,65 @@ The release process should result in the following side-effects:
Note all release tags pushed to GitHub MUST follow semantic versioning. Note all release tags pushed to GitHub MUST follow semantic versioning.
## Recipe ## Releasing a Major or Minor Version of cellxgene
Follow these steps to create a release. Please scroll down the section below for how to release a patch version. Follow these steps to create a major or minor release.
1. Preparation: 1. Preparation:
- python3.6 environment, and a cellxgene clone - python3.6 environment, and a cellxgene clone
- Define the release version number, using [semantic versioning](https://semver.org/), - Define the release version number, using [semantic versioning](https://semver.org/), and specifying all three digits (e.g., 0.3.0)
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)
- Write the release title and release notes and add to 2. Create a release branch, eg, `release-version-0.16.0`
[release notes document](https://docs.google.com/document/d/1KnHwkYfhyWO5H8BDcMu7y3ogjvq5Yi4OwpmZ8DB6w0Y/edit) 3. In the release branch, run `make create-release-candidate PART=[major | minor | patch]` where you choose major/minor/patch depending on which part of the version is being bumped (e.g., `0.2.9` -> `0.3.0` is minor version bump). This will bump the version and create a release *candidate* version (i.e. `0.3.0-rc.0`).
2. Create a release branch, eg, `release-version` 4. Commit and push the new branch. This will trigger tests to ensure that your branch isn't broken.
3. In the release branch: 5. Upload the release candidate to Test PyPI by running the command `make release-candidate-to-test-pypi`. (Make sure you are registered for PyPI and Test PyPI and you have write access to the cellxgene PyPI package for both).
- Run `make release-stage-1 PART=[major | minor | patch]` where you choose major/minor/patch depending on which part of the version 6. Verify the release candidate in a fresh virtual environment by running `make install-release-test` which installs the cellxgene build you just uploaded the Test PyPI.
is being bumped (eg, 0.2.9->0.3 is minor). 7. If you find errors with the release candidate, run `make recreate-release-candidate` to increment the release candidate version (i.e. `0.3.0-rc.0` -> `0.3.0-rc.1`). Then go back to Steps 5 and 6 to re-upload and re-test the new release candidate.
4. Commit and push the new branch 8. If everything looks good, push the release to Test PyPI without the release candidate tag by running the command `make release-final-to-test-pypi` (i.e. `0.3.0-rc.1` -> `0.3.0`).
5. Create a PR for the release. - **NOTE:** Once you push the final release version to Test PyPI, you cannot ever re-upload the build again. If you need to make changes to the build, you will have to "burn" the version number and bump the part again and go back to step 1 with a brand new version number. For example, if you upload `0.3.0` to Test PyPI and realize there's a bug, you will have to create a new version `0.4.0` and there will be no `0.3.0` version of cellxgene. This is why testing the release candidate is very important.
- [optional] As needed, conduct PR review. 9. Create a PR for the release and conduct a PR review.
6. Merge to the `main` branch 10. Merge to the `main` branch.
7. Publish to pypi by performing the following steps (assumes you that you have registered for pypi, 11. Publish to PyPI (prod) (assuming you that you have registered for PyPI, and that you have write access to the cellxgene pypi package) by running `make release-final`.
and that you have write access to the cellxgene pypi package): 12. Test the installation in a fresh virtual environment by running `pip install --no-cache-dir cellxgene`.
- Build the distribution and upload to test pypi `make release-stage-2` 13. Create Github release using the version number and release notes ([instructions](https://help.github.com/articles/creating-releases/)):
- Test the test installation in a fresh virtual environment using `make install-release-test` - Draft new release
- Upload the package to real pypi using `make release-stage-final` - Type version name matching release version number from (1)
- Test the installation in a fresh virtual environment using `pip install cellxgene` - Select `main` as release branch (ensure you merged the release PR)
8. Create Github release using the version number and release notes - Type title `Release {version num}`
([instructions](https://help.github.com/articles/creating-releases/)). - [optional] Check pre-release if this release is not ready for production
- Draft new release - Publish Release
- Type version name matching release version number from (1)
- Select `main` as release branch (ensure you merged the release PR)
- Type title `Release {version num}`
- [optional] Check pre-release if this release is not ready for production
- Publish Release
The optional steps are for testing purposes, and are recommended The optional steps are for testing purposes, and are recommended for publishing any major releases, and any releases that significantly change the packaging (e.g. new bundled files, new dependencies, etc.)
for publishing any major releases, and any releases that significantly
change the packaging (e.g. new bundled files, new dependencies, etc.)
### Point release (special case) ### Releasing a Patch Version of cellxgene (special case)
To make a bugfix release (a point release) when there are already other changes in `main` we need to do a modified version of our release process. The difference is that instead of using `main` we are going make our release branch off of the tag for the release we want to patch. We cherrypick the commits that we want to include in the patch. Then instead of merging to `main`, we create the release directly off of the branch. To make a bugfix release (a point release/patch release) when there are already other changes in `main` we need to do a modified version of our release process. The difference is that instead of using `main` we are going make our release branch off of the tag for the release we want to patch. We cherrypick the commits that we want to include in the patch. Then instead of merging to `main`, we create the release directly off of the branch.
1. (same as above) Preparation: 1. (same as above) Preparation:
- python3.6 environment, and a cellxgene clone - python3.6 environment, and a cellxgene clone
- Define the release version number, using [semantic versioning](https://semver.org/), - Define the release version number, using [semantic versioning](https://semver.org/), and specifying all three digits (e.g., 0.3.2) (for this you will update the last digit to represent a bugfix change).
and specifying all three digits (eg, 0.3.0) (for this you will update the last digit to represent a bugfix change) - Write the release title and release notes and add to [release notes document](https://docs.google.com/document/d/1KnHwkYfhyWO5H8BDcMu7y3ogjvq5Yi4OwpmZ8DB6w0Y/edit)
- 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 off of the tag for the release you want to update. 2. Create a release branch off of the tag for the release you want to update.
- Checkout the tag for the release you want to fix. ex. if we are fixing 0.9.0: `git checkout 0.9.0` - Checkout the tag for the release you want to fix. For example, if we are fixing 0.9.0: `git checkout 0.9.0`.
- Create a branch from that tag. `git branch release-0.9.1` - Create a branch from that tag. `git branch release-version-0.9.1`
3. Cherrypick the commits that you want included in this patch. 3. Cherrypick the commits that you want included in this patch.
- Test that the cherrypicked commits landed and fixed the issue - Test that the cherrypicked commits landed and fixed the issue locally.
- We WILL NOT merge this branch back into `main`, these commits should already exist in `main`. - We **WILL NOT** merge this branch back into `main` as these commits should already exist in `main`.
4. In the release branch: 4. In the release branch (i.e. `release-version-0.9.1`), run `make create-release-candidate PART=patch` to bump the patch version and create the first release candidate (i.e. `0.9.1-rc.0`).
- Run `make release-stage-1 PART=patch`. 5. Run `make release-candidate-to-test-pypi` to upload the release candidate to Test PyPI.
5. Commit and push the new branch. DO NOT MAKE A PR OR MERGE TO `main`. 6. Verify the release candidate in a fresh virtual environment by running `make install-release-test` which installs the cellxgene build you just uploaded the Test PyPI.
- wait for release to pass the tests 7. If you find errors with the release candidate, run `make recreate-release-candidate` to increment the release candidate version (i.e. `0.9.1-rc.0` -> `0.9.1-rc.1`). Then go back to Steps 5 and 6 to re-upload and re-test the new release candidate.
6. Publish to pypi by performing the following steps (assumes you that you have registered for pypi, 8. If everything looks good, push the final version of the release to Test PyPI without the release candidate tag by running the command `make release-final-to-test-pypi` (i.e. `0.9.1-rc.1` -> `0.9.1`).
and that you have write access to the cellxgene pypi package): - Build the distribution and upload to test pypi `make release-stage-2` - Test the test installation in a fresh virtual environment using `make install-release-test` - Upload the package to real pypi using `make release-stage-final` - Test the installation in a fresh virtual environment using - **NOTE:** Once you push the final release version to Test PyPI, you cannot ever re-upload the build again. If you need to make changes to the build, you will have to "burn" the version number and bump the part again and go back to step 1 with a brand new version number. For example, if you upload `0.9.1` to Test PyPI and realize there's a bug, you will have to create a new version `0.9.2` and there will be no `0.9.1` version of cellxgene. This is why testing the release candidate is very important.
`pip install --no-cache-dir cellxgene` 9. Commit and push the new branch. DO NOT MAKE A PR OR MERGE TO `main`.
7. Create Github release using the version number and release notes - Wait for release to pass the tests.
10. Publish to PyPI (prod) (assuming you that you have registered for PyPI, and that you have write access to the cellxgene pypi package) by running `make release-final`.
11. Test the installation in a fresh virtual environment by running `pip install --no-cache-dir cellxgene`.
12. Create Github release using the version number and release notes
([instructions](https://help.github.com/articles/creating-releases/)). ([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)
- _Different than above_ Select the release-branch you pushed at step 5 as release branch - [**_Different than above_**] Select the release-branch you pushed at step 5 as release branch
- 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
@@ -93,36 +87,52 @@ _PyPi doesn't allow you to reupload a release with the same version number_
If you accidentally burned a release number you want to use on prod, you have a few options: If you accidentally burned a release number you want to use on prod, you have a few options:
1. OPTION 1: Create distribution `make pydist`; test release locally `pip install dist/<release tarball>`; 1. OPTION 1: Create distribution `make pydist`; test release locally `pip install dist/<release tarball>`;
then upload to prod `make release-stage-final`. then upload to prod `make release-final`.
2. OPTION 2: (DANGER) release directly to prod: `make release-directly-to-prod`. 2. OPTION 2: (DANGER) release directly to prod: `make release-directly-to-prod`.
3. OPTION 3: If the release was burned on prod as well run from Step 3 again with option 3. OPTION 3: If the release was burned on prod as well run from Step 3 again with option PART=patch until you get to an unburned version.
PART=patch until you get to an unburned version.
### The release doesn't install or fails your tests when you install it ### The release doesn't install or fails your tests when you install it
Delete it from pypi - Go to pypi.org -> sign in -> go to the cellxgene package -> click manage -> then in the options drop down click delete -> follow the instructions. You will not be able to use that release number again. If it is a minor bug and not a major regression, you can just release a patch. Delete it from pypi - Go to pypi.org -> sign in -> go to the cellxgene package -> click manage -> then in the options drop down click delete -> follow the instructions. You will not be able to use that release number again. If it is a minor bug and not a major regression, you can just release a patch.
### If you need to run stage final on a different computer than stage 2 ### If you need to run the final upload to PyPI (prod) on a different computer than where you ran the command to upload to Test PyPI.
If you run stage final without running stage 2 first, the dist will not have been build on the computer running stage final. The solution is to run `make release-directly-to-prod`. This both builds the distribution files and then releases directly to prod pypi.org. If you run `make release-final` without running `make release-final-to-test-pypi` first, the dist will not have been build on the computer running the final PyPI push. The solution is to run `make release-directly-to-prod`. This both builds the distribution files and then releases directly to prod pypi.org.
## Stage Details ## Command Details
### Stage 1 - `make release-stage-1` ### Initial creation stage - `make create-release-candidate PART=[major | minor | patch]`
1. Pip installs requirements-dev 1. Pip installs requirements-dev
2. Bumps version by [PART] 2. Bumps version by [PART] and creates the first release candidate.
3. Deletes build directory, client/build, dist and cellxgene.egg-info 3. Deletes build directory, client/build, dist and cellxgene.egg-info
4. Creates the package-lock.json 4. Creates the package-lock.json
### Stage 2 - `make release-stage-2` ### Test PyPI upload stage - `make release-candidate-to-test-pypi`
1. Pip installs requirements-dev 1. Pip installs requirements-dev
2. Builds client and server 2. Builds client and server
3. Creates distribution release (sdist) 3. Creates distribution release (sdist)
4. Uploads to test.pypi.org 4. Uploads to test.pypi.org
### Recreating release candidate stage(s) - `make recreate-release-candidate`
### Stage final - `make release-stage-final` 1. Pip installs requirements-dev
2. Bumps release candidate version number.
3. Deletes build directory, client/build, dist and cellxgene.egg-info
4. Creates the package-lock.json
### Penultimate stage, final release to Test PyPI - `make release-final-to-test-pypi`
1. Pip installs requirements-dev
2. Removes release candidate tag from the version number.
3. Deletes build directory, client/build, dist and cellxgene.egg-info
4. Creates the package-lock.json
5. Pip installs requirements-dev
6. Builds client and server
7. Creates distribution release (sdist)
8. Uploads to test.pypi.org
### Final stage - `make release-final`
** Does not build distribution ** ** Does not build distribution **
1. Uploads to pypi.org 1. Uploads to pypi.org
+175
View File
@@ -0,0 +1,175 @@
# Cellxgene Schema Guide
Datasets included in the [data portal](https://cellxgene.cziscience.com/) and hosted cellxgene need to follow the schema
described [here](https://github.com/chanzuckerberg/corpora-data-portal/blob/main/backend/schema/corpora_schema.md). That
schema defines some required fields, requirements about feature labels, and some optional fields that mostly help with
presentation.
The number of fields is rather low, and we expect that information needed to populate those fields should either already
be present in datasets prepared by a submitter or be easy to obtain. However, this still leaves the task of actually
manipulating the dataset so that it follows the schema: adjusting field names, ensuring proper ontologies are used,
converting gene symbols to a common set, etc. This can be tedious and error-prone, and at the beginning of the hosted
cellxgene project, this was always done with engineering support. As we increase the rate at which we add data, we want
to eliminate the need for engineering support so that ultimately submitters themselves can create files that follow the
schema.
## `cellxgene schema apply`
To enable this, we have a new cellxgene subcommand, `cellxgene schema`, that handles applying and verifying the schema.
Its first subcommand, `cellxgene schema apply`, takes three inputs:
1. A source h5ad file. The input needs to be an AnnData file, so if a submitter has, say, a serialized Seurat or
SingleCellExperiment object, it needs to be converted to AnnData first. This can be done with
[sceasy](https://github.com/cellgeni/sceasy) or via
[Seurat](https://satijalab.org/seurat/v3.1/conversion_vignette.html).
2. A configuration yaml file that describes the fields to add and conversions to apply (see below).
3. A name for the new h5ad file that should follow the schema.
### Configuration yaml
The configuration yaml file describes how to apply the schema. This is an example of a "skeleton" yaml that has all the
fields required for the 1.0.0 schema but is not yet filled in with any logic:
```
uns:
version:
corpora_schema_version: 1.0.0
corpora_encoding_version: 0.1.0
contributors:
title:
layer_descriptions:
preprint_doi:
publication_doi:
organism_ontology_term_id:
obs:
tissue_ontology_term_id:
assay_ontology_term_id:
disease_ontology_term_id:
cell_type_ontology_term_id:
sex:
ethnicity_ontology_term_id:
development_stage_ontology_term_id:
fixup_gene_symbols:
```
#### Unstructured metadata
The first section is `uns`, which includes metadata fields that describe the whole dataset (see
[here](https://anndata.readthedocs.io/en/latest/) for further description of `uns` and `obs`.).
The first line is `version`, which is required for most of our tooling to work. The schema version is set at
1.0.0 in the example above, but of course for future versions that should be changed.
Next is `contributors` which describes who is adding the dataset to the portal. If you consult the schema, you see that
contributors is a list where each element can have `name`, `email`, and `institution`. So when filled out, the
`contributors` field should look like this:
```
contributors:
- name: Mary B. Scientist
email: mbs@singlecell.edu
institution: Single-Cell University
- name: Robert J. Scientist
email: rjs@usingle.edu
institution: University of Single Cell
```
`title` is the name of the dataset, and is just a string that gets displayed in the portal and cellxgene to identify the
dataset.
`layer_descriptions` is free text descriptions of the different
[layers](https://anndata.readthedocs.io/en/latest/anndata.AnnData.layers.html) of the AnnData file. It should look like
this when complete, depending on what layers are present:
```
layer_descriptions:
X: CPM and logged
raw.X: raw
```
Note that one of the layers needs to be "raw", that is, the AnnData file must contain raw counts.
The two DOI fields are optional but can be included if the dataset is associated with a publication or preprint. Note
that the DOI should be a full url:
```
publication_doi: https://doi.org/10.1073%2Fpnas.83.15.5372
```
Finally, the `organism_ontology_term_id` field is the species of the donor organism from the NCBITaxon ontology. The
value for _Homo sapiens_ is `NCBITaxon:9606`:
```
organism_ontology_term_id: NCBITaxon:9606
```
Note that the schema also requires a human-readable `organism` field, but this doesn't need to be included in the yaml.
When the `cellxgene schema apply` script encounters an ontology field, it looks up the label for the term(s) and inserts it
into the appropriate field.
#### Observation metadata
The next section is `obs`, which is metadata than can vary for each observation (and "observation" usually means cell).
These fields are all ontology fields except for `sex`, which has its own enumerated set of permitted values.
There are two ways to fill in the `obs` fields. The first is useful when there is only one value for all the
observations in the dataset. This is not uncommon, for example all cells often come from the same assay. In that case
just insert the ontology term:
```
assay_ontology_term_id: EFO:0009922
```
The second is for when there is an existing field in the dataset that needs to be mapped to the schema field. For
example, the submitter may have included cell type annotations in a field called `CellType`, and those annotations may
just be free text. This doesn't follow the schema because it needs to be in `cell_type_ontology_term_id` and
`cell_type`, and it needs ontology terms and labels, not just any text. In that case the field can be a dictionary:
```
cell_type_ontology_term_id:
CellType:
t-cell: CL:0000084
b-cell: CL:0000236
```
This will look at the `obs.CellType` field in the dataset, and where it has the value "t-cell", it will insert
`CL:0000084` into `cell_type_ontology_term_id` and its label `T cell` into `cell_type`.
Now there are often situations where there is no valid ontology term for some field. For example, the dataset may have
been produced via an assay not present in `EFO`. Or, a particular cell type may have no entry in `CL`. In that case, a
free text description can be used in the `ontology_term_id` field:
```
assay_ontology_term_id: Sci-Plex
cell_type_ontology_term_id:
CellType:
t-cell: CL:0000084
b-cell: CL:0000236
new cell type: new cell type
```
In these cases, the `cellxgene schema apply` script will leave the ontology field blank and move the free text
description into the label field. So the `assay_ontology_term_id` in the new dataset would be `""` but `assay` would be
`Sci-Plex`.
#### Gene symbol harmonization
The last section describes how gene symbol conversion should be applied to each of the layers. This is similar to the
`layer_descriptions` field above, but there are only three permitted values: `raw`, `log1p`, and `sqrt`:
```
fixup_gene_symbols:
X: log1p
raw.X: raw
```
This tells the script how each each layer was transformed from raw values that can be directly summed. `raw` means that
the layer contains raw counts or some linear tranformation of raw counts. `log1p` means that the layer has `log(X + 1)`
for each the raw `X` values. `sqrt` means `sqrt(X)` (this is not common). For layers produced by Seurat's normalization
or SCTransform functions, the correct choice is usually `log1p`.
### `cellxgene schema validate`
The next `cellxgene schema` subcommand is `cellxgene schema validate`, and it validates that a given h5ad follows a
version of the schema. It accepts two parameters:
1. The h5ad file to check
2. The version of the schema to check against.
If the validation succeeds, the command will have a zero exit code. If it does not, it will have a non-zero exit code
and will print validation failure messages.
+4 -2
View File
@@ -13,6 +13,8 @@ nav:
url: posts/install url: posts/install
- title: Gallery - title: Gallery
url: posts/gallery url: posts/gallery
- title: Cellxgene data portal
url: https://cellxgene.cziscience.com/
- title: Demo datasets - title: Demo datasets
url: posts/demo-data url: posts/demo-data
- title: Preparing your data - title: Preparing your data
@@ -31,7 +33,7 @@ nav:
url: posts/roadmap url: posts/roadmap
- title: Contributing (ideas or code) - title: Contributing (ideas or code)
url: posts/contribute url: posts/contribute
- title: Extensions
url: posts/extensions
- title: Contact & finding help - title: Contact & finding help
url: posts/contact url: posts/contact
- title: cellxgene.cziscience.com
url: posts/cellxgene_cziscience_com
@@ -0,0 +1,446 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 -->
<title>cellxgene.cziscience.com | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" />
<meta property="og:title" content="cellxgene.cziscience.com" />
<meta property="og:locale" content="en_US" />
<meta name="description" content="An interactive explorer for single-cell transcriptomics data" />
<meta property="og:description" content="An interactive explorer for single-cell transcriptomics data" />
<link rel="canonical" href="https://chanzuckerberg.github.io/cellxgene/deprecated/cellxgene_cziscience_com.html" />
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/deprecated/cellxgene_cziscience_com.html" />
<meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json">
{"@type":"WebPage","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"cellxgene.cziscience.com","description":"An interactive explorer for single-cell transcriptomics data","url":"https://chanzuckerberg.github.io/cellxgene/deprecated/cellxgene_cziscience_com.html","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=f70dffced52a32aada1a22504c841e97e941401a">
<!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div class="wrapper">
<header>
<img src="/cellxgene/cellxgene-logo.png" alt="cellxgene" />
<p>An interactive explorer for single-cell transcriptomics data</p>
<p>
<a href="/cellxgene/" class="btn">Quick start</a><br>
<a href="/cellxgene/posts/install" class="btn">Installation</a><br>
<a href="/cellxgene/posts/gallery" class="btn">Gallery</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
<a href="https://cellxgene.cziscience.com/" class="btn">All other datasets</a><br>
<a href="/cellxgene/posts/prepare" class="btn">Preparing your data</a><br>
<a href="/cellxgene/posts/launch" class="btn">Launching cellxgene</a><br>
<a href="/cellxgene/posts/hosted" class="btn">Hosting cellxgene</a><br>
<a href="/cellxgene/posts/annotations" class="btn">Annotating data</a><br>
<a href="/cellxgene/posts/methods" class="btn">Methods</a><br>
<a href="/cellxgene/posts/troubleshooting" class="btn">Troubleshooting</a><br>
<a href="/cellxgene/posts/roadmap" class="btn">Roadmap</a><br>
<a href="/cellxgene/posts/contribute" class="btn">Contributing (ideas or code)</a><br>
<a href="/cellxgene/posts/extensions" class="btn">Extensions</a><br>
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p>
</header>
<section>
<h1 id="cellxgeneczisciencecom">cellxgene.cziscience.com</h1>
<p>Chan Zuckerberg has an online repository of public single-cell datasets for exploration with cellxgene.</p>
<p>If you have a public dataset which you would like hosted for visualization on this site,
with a link to embed on your own site, please drop us a note at <a href="mailto:cellxgene@chanzuckerberg.com">cellxgene@chanzuckerberg.com</a>.</p>
<table class="fixed-layout">
<thead style="width: 100%">
<tr>
<th>cellxgene link</th>
<th>More Information</th>
</tr>
</thead>
<tbody style="width: 100%">
<tr>
<td><a href="https://cellxgene.cziscience.com/d/krasnow_lab_human_lung_cell_atlas_10x-1.cxg/" target="_blank">Krasnow Lab Human Lung Cell Atlas, 10X</a></td>
<td>
<a href="http://cmgm-new.stanford.edu/krasnow/">Krasnow Lab</a>,
<a href="https://github.com/krasnowlab/hlca">HLCA website</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/krasnow_lab_human_lung_cell_atlas_smartseq2-2.cxg/" target="_blank">Krasnow Lab Human Lung Cell Atlas, Smart-seq2</a></td>
<td>
<a href="http://cmgm-new.stanford.edu/krasnow/">Krasnow Lab</a>,
<a href="https://github.com/krasnowlab/hlca">HLCA website</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/human_cell_landscape-3.cxg/" target="_blank">Human Cell Landscape</a></td>
<td>
<a href="https://person.zju.edu.cn/en/ggj">Guo Lab</a>,
<a href="http://bis.zju.edu.cn/HCL/">HCL website</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/human_fetal_liver_single_cell_transcriptome-13.cxg/" target="_blank">Human fetal liver single cell transcriptome data</a></td>
<td>
<a href="https://www.ebi.ac.uk/arrayexpress/experiments/E-MTAB-7407/">E-MTAB-7407</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/cell_atlas_of_thymic_development-14.cxg/" target="_blank">A cell atlas of human thymic development defines T cell repertoire formation</a></td>
<td>
<a href="https://www.ebi.ac.uk/arrayexpress/experiments/E-MTAB-8581/">E-MTAB-8581</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/cellular_census_of_human_lungs_alveoli_and_parenchyma-15.cxg/" target="_blank">A cellular census of human lungs identifies novel cell states in health and in asthma - parenchyma</a></td>
<td>
<a href="https://asthma.cellgeni.sanger.ac.uk/">asthma.cellgeni.sanger.ac.uk</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/cellular_census_of_human_lungs_nasal-16.cxg/" target="_blank">A cellular census of human lungs identifies novel cell states in health and in asthma - nasal</a></td>
<td>
<a href="https://asthma.cellgeni.sanger.ac.uk/">asthma.cellgeni.sanger.ac.uk</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/cellular_census_of_human_lungs_bronchi-17.cxg/" target="_blank">A cellular census of human lungs identifies novel cell states in health and in asthma - bronchi</a></td>
<td>
<a href="https://asthma.cellgeni.sanger.ac.uk/">asthma.cellgeni.sanger.ac.uk</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/ischaemic_sensitivity_of_human_tissue_by_single_cell_RNA_seq_lung-18.cxg/" target="_blank">Ischaemic sensitivity of human tissue by single cell RNA seq - lung</a></td>
<td>
<a href="https://data.humancellatlas.org/explore/projects/c4077b3c-5c98-4d26-a614-246d12c2e5d7">HCA</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/ischaemic_sensitivity_of_human_tissue_by_single_cell_RNA_seq_spleen-19.cxg/" target="_blank">Ischaemic sensitivity of human tissue by single cell RNA seq - spleen</a></td>
<td>
<a href="https://data.humancellatlas.org/explore/projects/c4077b3c-5c98-4d26-a614-246d12c2e5d7">HCA</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/ischaemic_sensitivity_of_human_tissue_by_single_cell_RNA_seq_oesophagus-20.cxg/" target="_blank">Ischaemic sensitivity of human tissue by single cell RNA seq - oesophagus</a></td>
<td>
<a href="https://data.humancellatlas.org/explore/projects/c4077b3c-5c98-4d26-a614-246d12c2e5d7">HCA</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/spatio_temporal_immune_zonation_of_the_human_kidney-21.cxg/" target="_blank">Spatio-temporal immune zonation of the human kidney</a></td>
<td>
<a href="https://www.kidneycellatlas.org/">www.kidneycellatlas.org</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/fetal_maternal_interface_10x-22.cxg/" target="_blank">Reconstructing the human first trimester fetal-maternal interface using single cell transcriptomics - 10x</a></td>
<td>
<a href="https://www.ebi.ac.uk/arrayexpress/experiments/E-MTAB-6701/">E-MTAB-6701</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/fetal_maternal_interface_smartseq2-23.cxg/" target="_blank">Reconstructing the human first trimester fetal-maternal interface using single cell transcriptomics - SmartSeq2</a></td>
<td>
<a href="https://www.ebi.ac.uk/arrayexpress/experiments/E-MTAB-6701/">E-MTAB-6701</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/gut_cell_atlas-24.cxg/" target="_blank">Gut Cell Atlas</a></td>
<td>
<a href="https://www.gutcellatlas.org/">www.gutcellatlas.org</a>,
<a href="https://www.covid19cellatlas.org/">covid19cellatlas.org</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_atlas_of_peripheral_immune_response_to_SARS_CoV_2_infection-25.cxg/" target="_blank">A single-cell atlas of the peripheral immune response to severe COVID-19</a></td>
<td>
<a href="https://blishlab.sites.stanford.edu/">Blish Lab</a>,
<a href="https://www.medrxiv.org/content/10.1101/2020.04.17.20069930v1">medRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Atlas_of_Healthy_and_SHIV_Infected_Non_Human_Primate_Lung_and_Ileum_ACE2+_Cells_ileum-12.cxg/" target="_blank">Atlas of Healthy and SHIV-Infected Non-Human Primate Lung and Ileum ACE2+ Cells - Ileum</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP807/atlas-of-healthy-and-shiv-infected-non-human-primate-lung-and-ileum-ace2-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Atlas_of_Healthy_and_SHIV_Infected_Non_Human_Primate_Lung_and_Ileum_ACE2+_Cells_lung-11.cxg/" target="_blank">Atlas of Healthy and SHIV-Infected Non-Human Primate Lung and Ileum ACE2+ Cells - Lung</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP807/atlas-of-healthy-and-shiv-infected-non-human-primate-lung-and-ileum-ace2-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_epithelial-10.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - epithelial cells</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_scraping-9.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - nasal scrapings</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_surgical-8.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - surgical</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Allergic_inflammatory_memory_in_human_respiratory_epithelial_progenitor_cells_nasalsss-26.cxg/" target="_blank">Allergic inflammatory memory in human respiratory epithelial progenitor cells - nasal SSS</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP253/allergic-inflammatory-memory-in-human-respiratory-epithelial-progenitor-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/ACE2_and_TMPRSS2_expression_in_human_non_inflamed_terminal_ileum_epithelial-7.cxg/" target="_blank">ACE2 and TMPRSS2 expression in human non-inflamed terminal ileum - epithelial cells</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP812/ace2-and-tmprss2-expression-in-human-non-inflamed-terminal-ileum?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/ACE2_and_TMPRSS2_expression_in_human_non_inflamed_terminal_ileum-6.cxg/" target="_blank">ACE2 and TMPRSS2 expression in human non-inflamed terminal ileum</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP812/ace2-and-tmprss2-expression-in-human-non-inflamed-terminal-ileum?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Human_Lung_HIV_TB_Co_infection_ACE2+_Cells-5.cxg/" target="_blank">Human Lung HIV-TB Co-infection ACE2+ Cells</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP814/human-lung-hiv-tb-co-infection-ace2-cells?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Epithelial_Cells_in_NHP_mTB_Granuloma_and_Uninvolved_Lung-4.cxg/" target="_blank">Epithelial Cells in NHP mTB Granuloma and Uninvolved Lung</a></td>
<td>
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP806/epithelial-cells-in-nhp-mtb-granuloma-and-uninvolved-lung?scpbr=the-alexandria-project">Single Cell Portal</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_EC-49.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_SFG-50.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Superior Frontal Gyrus</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_EC_astrocytes-51.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Astrocytes in EC</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_EC_excitatoryNeurons-52.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Excitatory Neurons in EC</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_EC_inhibitoryNeurons-53.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Inhibitory Neurons in EC</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_EC_microglia-54.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Microglia in EC</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_SFG_astrocytes-55.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Astrocytes in SFG</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_SFG_excitatoryNeurons-56.cxg/
" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Excitatory Neurons in SFG</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_SFG_inhibitoryNeurons-57.cxg/" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Inhibitory Neurons in SFG</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/kampmann_lab_human_AD_snRNAseq_SFG_microglia-58.cxg/" target="_blank">Selective Neuronal Vulnerability in Alzheimer's Disease: Microglia in SFG</a></td>
<td>
<a href="https://kampmannlab.ucsf.edu/">Kampmann Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.04.04.025825v2">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_gene_expression_profiling_of_SARS_CoV_2_infected_human_cell_lines_H1299-27.cxg/" target="_blank">Single-cell gene expression profiling of SARS-CoV-2 infected human cell lines - H1299</a></td>
<td>
<a href="https://www.mdc-berlin.de/landthaler#t-single-cellsars-cov-2">Landthaler Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.05.05.079194v1">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_gene_expression_profiling_of_SARS_CoV_2_infected_human_cell_lines_Calu_3-28.cxg/" target="_blank">Single-cell gene expression profiling of SARS-CoV-2 infected human cell lines - Calu-3</a></td>
<td>
<a href="https://www.mdc-berlin.de/landthaler#t-single-cellsars-cov-2">Landthaler Lab</a>,
<a href="https://www.biorxiv.org/content/10.1101/2020.05.05.079194v1">BioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_drug_screening_a549-42.cxg/" target="_blank">Single-cell drug screening - A549</a></td>
<td>
<a href="https://github.com/cole-trapnell-lab/sci-plex">Trapnell Lab Github</a>,
<a href="https://science.sciencemag.org/content/367/6473/45">Science</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_drug_screening_k562-43.cxg/" target="_blank">Single-cell drug screening - K562</a></td>
<td>
<a href="https://github.com/cole-trapnell-lab/sci-plex">Trapnell Lab Github</a>,
<a href="https://science.sciencemag.org/content/367/6473/45">Science</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_drug_screening_mcf7-44.cxg/" target="_blank">Single-cell drug screening - MCF7</a></td>
<td>
<a href="https://github.com/cole-trapnell-lab/sci-plex">Trapnell Lab Github</a>,
<a href="https://science.sciencemag.org/content/367/6473/45">Science</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.prod.single-cell.czi.technology/d/Molecular_atlas_of_cell_types_and_zonation_in_the_brain_vasculature-48.cxg/" target="_blank">A molecular atlas of cell types and zonation in the brain vasculature</a></td>
<td>
<a href="http://betsholtzlab.org/VascularSingleCells/database.html">Betsholtz Lab</a>,
<a href="https://www.nature.com/articles/nature25739">Nature</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_soma_transcriptomics_AT8-45.cxg/" target="_blank">Single Soma Transcriptomics - AT8</a></td>
<td>
<a href="https://www.biorxiv.org/content/10.1101/2020.05.11.088591v1">bioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_soma_transcriptomics_MAP2-46.cxg/" target="_blank">Single Soma Transcriptomics - MAP2</a></td>
<td>
<a href="https://www.biorxiv.org/content/10.1101/2020.05.11.088591v1">bioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_soma_transcriptomics_MAP2AT8-47.cxg/" target="_blank">Single Soma Transcriptomics - MAP2AT8</a></td>
<td>
<a href="https://www.biorxiv.org/content/10.1101/2020.05.11.088591v1">bioRxiv preprint</a>
</td>
</tr>
<tr>
<td><a href="https://cellxgene.cziscience.com/d/Single_cell_longitudinal_analysis_of_SARS_CoV_2_infection_in_human_bronchial_epithelial_cells-29.cxg/" target="_blank">Single-cell longitudinal analysis of SARS-CoV-2 infection in human bronchial epithelial cells</a></td>
<td>
<a href="https://www.biorxiv.org/content/10.1101/2020.05.06.081695v2">bioRxiv preprint</a>
</td>
</tr>
</tbody>
</table>
</section>
<footer>
<p>This project is maintained by <a href="https://github.com/chanzuckerberg">chanzuckerberg</a></p>
</footer>
</div>
<script src="/cellxgene/assets/js/scale.fix.js"></script>
</body>
</html>
+10 -6
View File
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebSite","headline":"Index","url":"https://chanzuckerberg.github.io/cellxgene/","name":"cellxgene","@context":"https://schema.org"}</script> {"@type":"WebSite","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Index","description":"An interactive explorer for single-cell transcriptomics data","url":"https://chanzuckerberg.github.io/cellxgene/","name":"cellxgene","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=f70dffced52a32aada1a22504c841e97e941401a">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -82,13 +86,13 @@
<a href="/cellxgene/posts/extensions" class="btn">Extensions</a><br>
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>annotations | cellxgene</title> <title>annotations | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="annotations" /> <meta property="og:title" content="annotations" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="Creating annotations" /> <meta name="description" content="Creating annotations" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/annotations.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/annotations.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Creating annotations","@type":"WebPage","headline":"annotations","url":"https://chanzuckerberg.github.io/cellxgene/posts/annotations.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/annotations.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"annotations","description":"Creating annotations","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Contact | cellxgene</title> <title>Contact | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="Contact" /> <meta property="og:title" content="Contact" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="Contact" /> <meta name="description" content="Contact" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contact.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contact.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Contact","@type":"WebPage","headline":"Contact","url":"https://chanzuckerberg.github.io/cellxgene/posts/contact.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/contact.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Contact","description":"Contact","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn"><b>Contact & finding help</b></a><br> <a href="/cellxgene/posts/contact" class="btn"><b>Contact & finding help</b></a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Code of conduct | cellxgene</title> <title>Code of conduct | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="Code of conduct" /> <meta property="og:title" content="Code of conduct" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="An interactive explorer for single-cell transcriptomics data" /> <meta name="description" content="An interactive explorer for single-cell transcriptomics data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contribute.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/contribute.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","headline":"Code of conduct","url":"https://chanzuckerberg.github.io/cellxgene/posts/contribute.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/contribute.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Code of conduct","description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>demo-data | cellxgene</title> <title>demo-data | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="demo-data" /> <meta property="og:title" content="demo-data" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="Demo datasets" /> <meta name="description" content="Demo datasets" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Demo datasets","@type":"WebPage","headline":"demo-data","url":"https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/demo-data.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"demo-data","description":"Demo datasets","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn"><b>Demo datasets</b></a><br> <a href="/cellxgene/posts/demo-data" class="btn"><b>Demo datasets</b></a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Extensions | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" />
<meta property="og:title" content="Extensions" />
<meta property="og:locale" content="en_US" />
<meta name="description" content="An interactive explorer for single-cell transcriptomics data" />
<meta property="og:description" content="An interactive explorer for single-cell transcriptomics data" />
<link rel="canonical" href="https://chanzuckerberg.github.io/cellxgene/posts/extensions.html" />
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/extensions.html" />
<meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json">
{"@type":"WebPage","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Extensions","description":"An interactive explorer for single-cell transcriptomics data","url":"https://chanzuckerberg.github.io/cellxgene/posts/extensions.html","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=f70dffced52a32aada1a22504c841e97e941401a">
<!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div class="wrapper">
<header>
<img src="/cellxgene/cellxgene-logo.png" alt="cellxgene" />
<p>An interactive explorer for single-cell transcriptomics data</p>
<p>
<a href="/cellxgene/" class="btn">Quick start</a><br>
<a href="/cellxgene/posts/install" class="btn">Installation</a><br>
<a href="/cellxgene/posts/gallery" class="btn">Gallery</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
<a href="https://cellxgene.cziscience.com/" class="btn">All other datasets</a><br>
<a href="/cellxgene/posts/prepare" class="btn">Preparing your data</a><br>
<a href="/cellxgene/posts/launch" class="btn">Launching cellxgene</a><br>
<a href="/cellxgene/posts/hosted" class="btn">Hosting cellxgene</a><br>
<a href="/cellxgene/posts/annotations" class="btn">Annotating data</a><br>
<a href="/cellxgene/posts/methods" class="btn">Methods</a><br>
<a href="/cellxgene/posts/troubleshooting" class="btn">Troubleshooting</a><br>
<a href="/cellxgene/posts/roadmap" class="btn">Roadmap</a><br>
<a href="/cellxgene/posts/contribute" class="btn">Contributing (ideas or code)</a><br>
<a href="/cellxgene/posts/extensions" class="btn"><b>Extensions</b></a><br>
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p>
</header>
<section>
<h1 id="extensions">Extensions</h1>
<p>This project was started with the sole goal of empowering the scientific community to explore and understand their data.
As such, we encourage other scientific tool builders in academia or industry to adopt the patterns, tools, and code from
this project. All code is freely available for reuse under the <a href="https://opensource.org/licenses/MIT">MIT license</a>.</p>
<p>Before extending cellxgene, we encourage you to reach out to us with ideas or questions. It might be possible that an
extension could be directly contributed, which would make it available for a wider audience, or that it’s on our
<a href="/cellxgene/posts/roadmap.html">roadmap</a> and under active development.</p>
<p>Please note that cellxgene does not have public APIs. Our development may break extensions. We will document changes to the code base but it is advised that extensions pin the version of cellxgene they develop against.</p>
<h2 id="example-reuse--extensions">Example Reuse &amp; extensions</h2>
<h4 id="cellxgene-gateway">cellxgene Gateway</h4>
<p><a href="https://github.com/Novartis/cellxgene-gateway">cellxgene Gateway</a> allows you to use with multiple datasets. It
displays an index of available h5ad (anndata) files. When a user clicks on a file name, it launches a Cellxgene Server
instance that loads that particular data file and once it is available proxies requests to that server.</p>
<h4 id="cellxgene-vip-visualization-in-plugin">cellxgene-VIP (Visualization in Plugin)</h4>
<p><a href="https://github.com/interactivereport/cellxgene_VIP">cellxgene-VIP</a> enables cellxgene to generate violin, stacked violin, stacked bar, heatmap, volcano, embedding, dot, track, density, 2D density, sankey and dual-gene plot in high-resolution SVG/PNG format. It also performs differential gene expression analysis and provides a Command Line Interface (CLI) for advanced users to perform analysis using python and R.</p>
<h4 id="galaxy">Galaxy</h4>
<p><a href="https://galaxyproject.org">Galaxy</a> is an open source, collaborative, web-based platform for data intensive biomedical research.
Galaxy provides various tools for <a href="https://singlecell.usegalaxy.eu/">single-cell data analysis</a> and also infrastructure to the
<a href="https://humancellatlas.usegalaxy.eu">Galaxy Human Cell Atlas project</a>. cellxgene can be <a href="https://usegalaxy.eu/root?tool_id=interactive_tool_cellxgene">
accessed within Galaxy</a> to view analyzed datasets. See also the relevant <a href="https://doi.org/10.1093/gigascience/giaa102">publication</a>
</p>
<h4 id="single-cell-portal">Single Cell Portal</h4>
<p>The <a href="https://singlecell.broadinstitute.org/single_cell">Single Cell Portal</a> is a data hosting and visualization service. cellxgene can be embedded as an additional view to complement the visualizations provided by the.
<a href="https://singlecell.broadinstitute.org/single_cell/study/SCP807/atlas-of-healthy-and-shiv-infected-non-human-primate-lung-and-ileum-ace2-cells">Example</a>.</p>
<h4 id="fastgenomics">FASTGenomics</h4>
<p><a href="https://beta.fastgenomics.org/">FASTGenomics</a> is a collaborative research platform that offers easy-to-use data management and reproducible analytics to drive single-cell research forward. Many of the publicly available datasets in FASTGenomics - as well as your private datasets - can be interactively explored with cellxgene.
See also this <a href="https://beta.fastgenomics.org/datasets/detail-dataset-952687f71ef34322a850553c4a24e82e#Cellxgene">example</a> for data from <a href="https://beta.fastgenomics.org/p/schulte-schrepping_covid19">Schulte-Schrepping et al. (Cell, 2020)</a>.
Note that it is not necessary to create an account, anonymous login is permitted.</p>
</section>
<footer>
<p>This project is maintained by <a href="https://github.com/chanzuckerberg">chanzuckerberg</a></p>
</footer>
</div>
<script src="/cellxgene/assets/js/scale.fix.js"></script>
</body>
</html>
+39
View File
@@ -0,0 +1,39 @@
# Extensions
This project was started with the sole goal of empowering the scientific community to explore and understand their data.
As such, we encourage other scientific tool builders in academia or industry to adopt the patterns, tools, and code from
this project. All code is freely available for reuse under the [MIT license](https://opensource.org/licenses/MIT).
Before extending cellxgene, we encourage you to reach out to us with ideas or questions. It might be possible that an
extension could be directly contributed, which would make it available for a wider audience, or that it's on our
[roadmap](./roadmap.md) and under active development.
Please note that cellxgene does not have public APIs. Our development may break extensions. We will document changes to the code base but it is advised that extensions pin the version of cellxgene they develop against.
## Example Reuse & extensions
#### cellxgene Gateway
[cellxgene Gateway](https://github.com/Novartis/cellxgene-gateway) allows you to use with multiple datasets. It
displays an index of available h5ad (anndata) files. When a user clicks on a file name, it launches a Cellxgene Server
instance that loads that particular data file and once it is available proxies requests to that server.
#### cellxgene-VIP (Visualization in Plugin)
[cellxgene-VIP](https://github.com/interactivereport/cellxgene_VIP) enables cellxgene to generate violin, stacked violin, stacked bar, heatmap, volcano, embedding, dot, track, density, 2D density, sankey and dual-gene plot in high-resolution SVG/PNG format. It also performs differential gene expression analysis and provides a Command Line Interface (CLI) for advanced users to perform analysis using python and R.
#### Galaxy
[Galaxy](https://singlecell.usegalaxy.eu/) is an open source, web-based platform for data intensive biomedical research. cellxgene can be accessed within Galaxy to view analyzed datasets.
See also the relevant [publication](https://www.biorxiv.org/content/10.1101/2020.06.06.137570v1.full.pdf)
#### Single Cell Portal
The [Single Cell Portal](https://singlecell.broadinstitute.org/single_cell) is a data hosting and visualization service. cellxgene can be embedded as an additional view to complement the visualizations provided by the.
[Example](https://singlecell.broadinstitute.org/single_cell/study/SCP807/atlas-of-healthy-and-shiv-infected-non-human-primate-lung-and-ileum-ace2-cells).
#### FASTGenomics
[FASTGenomics](https://beta.fastgenomics.org/) is a collaborative research platform that offers easy-to-use data management and reproducible analytics to drive single-cell research forward. Many of the publicly available datasets in FASTGenomics - as well as your private datasets - can be interactively explored with cellxgene.
See also this [example](https://beta.fastgenomics.org/datasets/detail-dataset-952687f71ef34322a850553c4a24e82e#Cellxgene) for data from [Schulte-Schrepping et al. (Cell, 2020)](https://beta.fastgenomics.org/p/schulte-schrepping_covid19).
Note that it is not necessary to create an account, anonymous login is permitted.
+8 -8
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Gallery | cellxgene</title> <title>Gallery | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="Gallery" /> <meta property="og:title" content="Gallery" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="An interactive explorer for single-cell transcriptomics data" /> <meta name="description" content="An interactive explorer for single-cell transcriptomics data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/gallery.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/gallery.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","headline":"Gallery","url":"https://chanzuckerberg.github.io/cellxgene/posts/gallery.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/gallery.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Gallery","description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
@@ -130,7 +130,7 @@ Check out the cool data that our users are using cellxgene to explore!</p>
<h3 id="melanoma"><a href="https://melanoma.cellgeni.sanger.ac.uk/">Melanoma</a></h3> <h3 id="melanoma"><a href="https://melanoma.cellgeni.sanger.ac.uk/">Melanoma</a></h3>
<h3 id="czis-own-cellxgene-site"><a href="cellxgene_cziscience_com">CZI’s own cellxgene site</a></h3> <h3 id="czis-own-cellxgene-site"><a href="https://cellxgene.cziscience.com/">CZI’s own cellxgene site</a></h3>
<p><em>Want us to link to your dataset here? <a href="contact">Just send us a note!</a></em></p> <p><em>Want us to link to your dataset here? <a href="contact">Just send us a note!</a></em></p>
+28 -27
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Hosting cellxgene on the web | cellxgene</title> <title>Hosting cellxgene on the web | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="Hosting cellxgene on the web" /> <meta property="og:title" content="Hosting cellxgene on the web" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="An interactive explorer for single-cell transcriptomics data" /> <meta name="description" content="An interactive explorer for single-cell transcriptomics data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/hosted.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/hosted.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","headline":"Hosting cellxgene on the web","url":"https://chanzuckerberg.github.io/cellxgene/posts/hosted.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/hosted.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Hosting cellxgene on the web","description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
@@ -139,35 +139,36 @@
<h1 id="deploying-cellxgene-with-heroku">Deploying cellxgene with Heroku</h1> <h1 id="deploying-cellxgene-with-heroku">Deploying cellxgene with Heroku</h1>
<h2 id="quickstart">Quickstart</h2> <h2 id="heroku-support">Heroku Support</h2>
<p>Clicking on the following button will forward you to Heroku to begin the deployment process:</p> <p>The cellxgene team has decided to end our support for our experimental deploy to Heroku button as we move towards providing a supported method of hosted cellxgene.</p>
<p><a href="https://heroku.com/deploy?template=https://github.com/chanzuckerberg/cellxgene"> <p>While we no longer directly support Heroku, it is still possible to create a Heroku app via <a href="https://github.com/chanzuckerberg/cellxgene/blob/main/Dockerfile">our provided Dockerfile here</a> and <a href="https://devcenter.heroku.com/articles/build-docker-images-heroku-yml">Heroku’s documentation</a>.</p>
<img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy" />
</a></p>
<p>If not already logged in to Heroku, there you will be prompted to log in or sign up for an account.</p> <p>You may have to tweak the <code class="language-plaintext highlighter-rouge">Dockerfile</code> like so:</p>
<p>Once logged in you will be sent to the setup page. Here you can set some of the basic settings for the app:</p> <pre><code class="language-Dockerfile">FROM ubuntu:bionic
<h3 id="default-settings">Default settings</h3> ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
<ul> RUN apt-get update &amp;&amp; \
<li><code class="language-plaintext highlighter-rouge">App name</code>: the unique name for your deployment</li> apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev python3-requests &amp;&amp; \
<li>This will also serve as the default URL (e.g. https://cellxgene.herokapp.com/)</li> pip3 install cellxgene
<li><code class="language-plaintext highlighter-rouge">App owner</code>: Who will own this app. Either you personally or an organization/team</li>
<li><code class="language-plaintext highlighter-rouge">Region</code>: Location of the server where the app will be deployed (EU or US)</li>
</ul>
<h3 id="configuration">Configuration</h3> # ENTRYPOINT ["cellxgene"] # Heroku doesn't work well with ENTRYPOINT
</code></pre>
<ul> <p>and provide a <code class="language-plaintext highlighter-rouge">heroku.yml</code> file similar to this:</p>
<li><code class="language-plaintext highlighter-rouge">DATASET</code>: A <em>publicly</em> accessible URL pointing to a .h5ad file to view</li>
<li>This defaults to pbm3k.h5ad</li>
</ul>
<p>After filling out the settings and pressing the <code class="language-plaintext highlighter-rouge">Deploy app</code> button Heroku will begin building your deployment. This process will take a few minutes, but once completed you will have a personal free hosted version of cellxgene!</p> <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">build</span><span class="pi">:</span>
<span class="na">docker</span><span class="pi">:</span>
<span class="na">web</span><span class="pi">:</span> <span class="s">Dockerfile</span>
<span class="na">run</span><span class="pi">:</span>
<span class="na">web</span><span class="pi">:</span>
<span class="na">command</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">cellxgene launch --host 0.0.0.0 --port $PORT $DATASET</span> <span class="c1"># the DATATSET config var must be defined in your dashboard settings.</span>
</code></pre></div></div>
<h2 id="what-is-heroku">What is Heroku?</h2> <h2 id="what-is-heroku">What is Heroku?</h2>
+23 -16
View File
@@ -38,31 +38,38 @@ If you know of other solutions, drop us a note and we'll add to this list.
# Deploying cellxgene with Heroku # Deploying cellxgene with Heroku
## Quickstart ## Heroku Support
Clicking on the following button will forward you to Heroku to begin the deployment process: The cellxgene team has decided to end our support for our experimental deploy to Heroku button as we move towards providing a supported method of hosted cellxgene.
<a href="https://heroku.com/deploy?template=https://github.com/chanzuckerberg/cellxgene"> While we no longer directly support Heroku, it is still possible to create a Heroku app via [our provided Dockerfile here](https://github.com/chanzuckerberg/cellxgene/blob/main/Dockerfile) and [Heroku's documentation](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml).
<img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
</a>
If not already logged in to Heroku, there you will be prompted to log in or sign up for an account. You may have to tweak the `Dockerfile` like so:
Once logged in you will be sent to the setup page. Here you can set some of the basic settings for the app: ```Dockerfile
FROM ubuntu:bionic
### Default settings ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
- `App name`: the unique name for your deployment RUN apt-get update && \
- This will also serve as the default URL (e.g. https://cellxgene.herokapp.com/) apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev python3-requests && \
- `App owner`: Who will own this app. Either you personally or an organization/team pip3 install cellxgene
- `Region`: Location of the server where the app will be deployed (EU or US)
### Configuration # ENTRYPOINT ["cellxgene"] # Heroku doesn't work well with ENTRYPOINT
```
- `DATASET`: A _publicly_ accessible URL pointing to a .h5ad file to view and provide a `heroku.yml` file similar to this:
- This defaults to pbm3k.h5ad
After filling out the settings and pressing the `Deploy app` button Heroku will begin building your deployment. This process will take a few minutes, but once completed you will have a personal free hosted version of cellxgene! ```yml
build:
docker:
web: Dockerfile
run:
web:
command:
- cellxgene launch --host 0.0.0.0 --port $PORT $DATASET # the DATATSET config var must be defined in your dashboard settings.
```
## What is Heroku? ## What is Heroku?
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Install | cellxgene</title> <title>Install | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="Install" /> <meta property="og:title" content="Install" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="An interactive explorer for single-cell transcriptomics data" /> <meta name="description" content="An interactive explorer for single-cell transcriptomics data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/install.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/install.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","headline":"Install","url":"https://chanzuckerberg.github.io/cellxgene/posts/install.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/install.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Install","description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>demo-data | cellxgene</title> <title>demo-data | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="demo-data" /> <meta property="og:title" content="demo-data" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="Demo datasets" /> <meta name="description" content="Demo datasets" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/launch.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/launch.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Demo datasets","@type":"WebPage","headline":"demo-data","url":"https://chanzuckerberg.github.io/cellxgene/posts/launch.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/launch.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"demo-data","description":"Demo datasets","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Methods | cellxgene</title> <title>Methods | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="Methods" /> <meta property="og:title" content="Methods" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="An interactive explorer for single-cell transcriptomics data" /> <meta name="description" content="An interactive explorer for single-cell transcriptomics data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/methods.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/methods.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","headline":"Methods","url":"https://chanzuckerberg.github.io/cellxgene/posts/methods.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/methods.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Methods","description":"An interactive explorer for single-cell transcriptomics data","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>prepare | cellxgene</title> <title>prepare | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="prepare" /> <meta property="og:title" content="prepare" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="Preparing your data" /> <meta name="description" content="Preparing your data" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/prepare.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/prepare.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Preparing your data","@type":"WebPage","headline":"prepare","url":"https://chanzuckerberg.github.io/cellxgene/posts/prepare.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/prepare.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"prepare","description":"Preparing your data","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>roadmap | cellxgene</title> <title>roadmap | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="roadmap" /> <meta property="og:title" content="roadmap" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="Roadmap" /> <meta name="description" content="Roadmap" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Roadmap","@type":"WebPage","headline":"roadmap","url":"https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/roadmap.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"roadmap","description":"Roadmap","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+7 -7
View File
@@ -7,7 +7,7 @@
<!-- Begin Jekyll SEO tag v2.6.1 --> <!-- Begin Jekyll SEO tag v2.6.1 -->
<title>Troubleshooting | cellxgene</title> <title>Troubleshooting | cellxgene</title>
<meta name="generator" content="Jekyll v3.8.7" /> <meta name="generator" content="Jekyll v3.9.0" />
<meta property="og:title" content="Troubleshooting" /> <meta property="og:title" content="Troubleshooting" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta name="description" content="Troubleshooting" /> <meta name="description" content="Troubleshooting" />
@@ -16,10 +16,10 @@
<meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html" /> <meta property="og:url" content="https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html" />
<meta property="og:site_name" content="cellxgene" /> <meta property="og:site_name" content="cellxgene" />
<script type="application/ld+json"> <script type="application/ld+json">
{"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"description":"Troubleshooting","@type":"WebPage","headline":"Troubleshooting","url":"https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html","@context":"https://schema.org"}</script> {"url":"https://chanzuckerberg.github.io/cellxgene/posts/troubleshooting.html","publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://chanzuckerberg.github.io/cellxgene/cellxgene-logo.png"}},"headline":"Troubleshooting","description":"Troubleshooting","@type":"WebPage","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag --> <!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=6bda27f5542fb7f469425e1cd99f2f37268b095f"> <link rel="stylesheet" href="/cellxgene/assets/css/style.css?v=3718e894edc8a8f6e7776946695ab37c5c96ec9f">
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]--> <![endif]-->
@@ -46,6 +46,10 @@
<a href="https://cellxgene.cziscience.com/" class="btn">Cellxgene data portal</a><br>
<a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br> <a href="/cellxgene/posts/demo-data" class="btn">Demo datasets</a><br>
@@ -85,10 +89,6 @@
<a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br> <a href="/cellxgene/posts/contact" class="btn">Contact & finding help</a><br>
<a href="/cellxgene/posts/cellxgene_cziscience_com" class="btn">cellxgene.cziscience.com</a><br>
<a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a> <a href="https://github.com/chanzuckerberg/cellxgene" class="btn" target="_blank">Code</a>
</p> </p>
+39
View File
@@ -0,0 +1,39 @@
# Extensions
This project was started with the sole goal of empowering the scientific community to explore and understand their data.
As such, we encourage other scientific tool builders in academia or industry to adopt the patterns, tools, and code from
this project. All code is freely available for reuse under the [MIT license](https://opensource.org/licenses/MIT).
Before extending cellxgene, we encourage you to reach out to us with ideas or questions. It might be possible that an
extension could be directly contributed, which would make it available for a wider audience, or that it's on our
[roadmap](./roadmap.md) and under active development.
Please note that cellxgene does not have public APIs. Our development may break extensions. We will document changes to the code base but it is advised that extensions pin the version of cellxgene they develop against.
## Example Reuse & extensions
#### cellxgene Gateway
[cellxgene Gateway](https://github.com/Novartis/cellxgene-gateway) allows you to use with multiple datasets. It
displays an index of available h5ad (anndata) files. When a user clicks on a file name, it launches a Cellxgene Server
instance that loads that particular data file and once it is available proxies requests to that server.
#### cellxgene-VIP (Visualization in Plugin)
[cellxgene-VIP](https://github.com/interactivereport/cellxgene_VIP) enables cellxgene to generate violin, stacked violin, stacked bar, heatmap, volcano, embedding, dot, track, density, 2D density, sankey and dual-gene plot in high-resolution SVG/PNG format. It also performs differential gene expression analysis and provides a Command Line Interface (CLI) for advanced users to perform analysis using python and R.
#### Galaxy
[Galaxy](https://singlecell.usegalaxy.eu/) is an open source, web-based platform for data intensive biomedical research. cellxgene can be accessed within Galaxy to view analyzed datasets.
See also the relevant [publication](https://www.biorxiv.org/content/10.1101/2020.06.06.137570v1.full.pdf)
#### Single Cell Portal
The [Single Cell Portal](https://singlecell.broadinstitute.org/single_cell) is a data hosting and visualization service. cellxgene can be embedded as an additional view to complement the visualizations provided by the.
[Example](https://singlecell.broadinstitute.org/single_cell/study/SCP807/atlas-of-healthy-and-shiv-infected-non-human-primate-lung-and-ileum-ace2-cells).
#### FASTGenomics
[FASTGenomics](https://beta.fastgenomics.org/) is a collaborative research platform that offers easy-to-use data management and reproducible analytics to drive single-cell research forward. Many of the publicly available datasets in FASTGenomics - as well as your private datasets - can be interactively explored with cellxgene.
See also this [example](https://beta.fastgenomics.org/datasets/detail-dataset-952687f71ef34322a850553c4a24e82e#Cellxgene) for data from [Schulte-Schrepping et al. (Cell, 2020)](https://beta.fastgenomics.org/p/schulte-schrepping_covid19).
Note that it is not necessary to create an account, anonymous login is permitted.
+1 -1
View File
@@ -39,6 +39,6 @@ Check out the cool data that our users are using cellxgene to explore!
### [Melanoma](https://melanoma.cellgeni.sanger.ac.uk/) ### [Melanoma](https://melanoma.cellgeni.sanger.ac.uk/)
### [CZI's own cellxgene site](cellxgene_cziscience_com) ### [CZI's own cellxgene site](https://cellxgene.cziscience.com/)
_Want us to link to your dataset here? [Just send us a note!](contact)_ _Want us to link to your dataset here? [Just send us a note!](contact)_
+23 -16
View File
@@ -38,31 +38,38 @@ If you know of other solutions, drop us a note and we'll add to this list.
# Deploying cellxgene with Heroku # Deploying cellxgene with Heroku
## Quickstart ## Heroku Support
Clicking on the following button will forward you to Heroku to begin the deployment process: The cellxgene team has decided to end our support for our experimental deploy to Heroku button as we move towards providing a supported method of hosted cellxgene.
<a href="https://heroku.com/deploy?template=https://github.com/chanzuckerberg/cellxgene"> While we no longer directly support Heroku, it is still possible to create a Heroku app via [our provided Dockerfile here](https://github.com/chanzuckerberg/cellxgene/blob/main/Dockerfile) and [Heroku's documentation](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml).
<img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
</a>
If not already logged in to Heroku, there you will be prompted to log in or sign up for an account. You may have to tweak the `Dockerfile` like so:
Once logged in you will be sent to the setup page. Here you can set some of the basic settings for the app: ```Dockerfile
FROM ubuntu:bionic
### Default settings ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
- `App name`: the unique name for your deployment RUN apt-get update && \
- This will also serve as the default URL (e.g. https://cellxgene.herokapp.com/) apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev python3-requests && \
- `App owner`: Who will own this app. Either you personally or an organization/team pip3 install cellxgene
- `Region`: Location of the server where the app will be deployed (EU or US)
### Configuration # ENTRYPOINT ["cellxgene"] # Heroku doesn't work well with ENTRYPOINT
```
- `DATASET`: A _publicly_ accessible URL pointing to a .h5ad file to view and provide a `heroku.yml` file similar to this:
- This defaults to pbm3k.h5ad
After filling out the settings and pressing the `Deploy app` button Heroku will begin building your deployment. This process will take a few minutes, but once completed you will have a personal free hosted version of cellxgene! ```yml
build:
docker:
web: Dockerfile
run:
web:
command:
- cellxgene launch --host 0.0.0.0 --port $PORT $DATASET # the DATATSET config var must be defined in your dashboard settings.
```
## What is Heroku? ## What is Heroku?
-7
View File
@@ -1,7 +0,0 @@
FROM python:3.7
WORKDIR /usr/src/app
RUN pip3 install cellxgene
expose 5005
-58
View File
@@ -1,58 +0,0 @@
# cellxgene cloud deployment with Heroku
## Quickstart
Clicking on the following button will forward you to Heroku to begin the deployment process:
<a href="https://heroku.com/deploy?template=https://github.com/chanzuckerberg/cellxgene/tree/main">
<img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
</a>
If not already logged in to Heroku, there you will be prompted to log in or sign up for an account.
Once logged in you will be sent to the setup page. Here you can set some of the basic settings for the app:
#### Default settings
- `App name`: the unique name for your deployment
- This will also serve as the default URL (e.g. https://cellxgene.herokapp.com/)
- `App owner`: Who will own this app. Either you personally or an organization/team
- `Region`: Location of the server where the app will be deployed (EU or US)
#### Configuration
- `DATASET`: A _publicly_ accessible URL pointing to a .h5ad file to view
- This defaults to pbm3k.h5ad
After filling out the settings and pressing the `Deploy app` button Heroku will begin building your deployment. This process will take a few minutes, but once completed you will have a personal free hosted version of cellxgene!
## What is Heroku?
Heroku is a quick and easy way to host applications on the cloud.
A Heroku deployment of cellxgene means that the app is not running on your local machine. Instead, the app is installed, configured, and ran on the Heroku servers (read: cloud).
On Heroku's servers, applications run on a [dyno](https://www.heroku.com/dynos) which are Heroku's implementation and abstraction of containers.
Heroku is one of many options available for hosting instances of cellxgene on the web.
Some other options include: Amazon Web Services, Google Cloud Platform, Digital Ocean, and Microsoft Azure.
## Why use Heroku to deploy cellxgene?
What Heroku enables is a quick, non-technical method of setting up a cellxgene instance. No command line knowledge needed. This also allows machines to access the instance via the internet, so sharing a visualized dataset is as simple as sharing a link.
Because cellxgene currently heavily relies on its Python backend for providing the viewer with the necessary data and tooling, it is currently not possible to host cellxgene as a static webpage.
This is a good option if you want to quickly deploy an instance of cellxgene to the web. Heroku deployments are free for small datasets up to around 250MBs in size. See below regarding larger datasets.
## When should I not deploy with Heroku?
- The default free dyno offered by Heroku is limited in memory to 512 MBs
- The amount of memory needed for the dyno is roughly the same size as the h5ad file
- Heroku offers tiered paid dynos. More can be found [here](https://www.heroku.com/pricing)
- Note that this can get _very_ expensive for larger datasets (\$25+ a month)
- On the free dyno, after 30 minutes of inactivity, Heroku will put your app into a hibernation mode. On the next access, Heroku will need time to boot the dyno back online.
- Having multiple simultaneous users requires more memory. This means that the free container size is easily overwhelmed by multiple users, even with small datasets; this can be addressed by purchasing a larger container size
- For this facilitated Heroku deployment to work, your dataset must be hosted on a publicly accessible URL
- By default, Heroku publically shares your instance to anyone with the URL.
- There are many ways of securing your instance. One quick and simple way is by installing [wwwhisper](https://elements.heroku.com/addons/wwwhisper), a Heroku addon
-5
View File
@@ -1,5 +0,0 @@
build:
docker:
web: experiments/heroku/Dockerfile
run:
web: cellxgene launch $DATASET --host 0.0.0.0 --port $PORT
+8
View File
@@ -39,3 +39,11 @@ create-test-db:
clean-test-db: clean-test-db:
-docker stop test_db -docker stop test_db
-docker rm test_db -docker rm test_db
.PHONY: test-annotations-performance
test-annotations-performance:
python test/performance/performance_test_annotations_backend.py
.PHONY: test-annotations-scale
test-annotations-scale:
locust -f test/performance/scale_test_annotations.py --headless -u 30 -r 10 --host https://api.cellxgene.dev.single-cell.czi.technology/cellxgene/e/ --run-time 5m 2>&1 | tee locust_dev_stats.txt
+2 -2
View File
@@ -1,8 +1,8 @@
from server.common.utils import import_plugins
import logging import logging
import sys import sys
from server.common.utils.utils import import_plugins
__version__ = "0.16.0" __version__ = "0.16.7"
display_version = "cellxgene v" + __version__ display_version = "cellxgene v" + __version__
try: try:
+112 -33
View File
@@ -1,22 +1,32 @@
import datetime import datetime
import logging import logging
from functools import wraps
from http import HTTPStatus
from urllib.parse import urlparse
import hashlib
import os
from flask import Flask, redirect, current_app, make_response, render_template, abort from flask import (
from flask import Blueprint, request Flask,
redirect,
current_app,
make_response,
render_template,
abort,
Blueprint,
request,
send_from_directory,
)
from flask_restful import Api, Resource from flask_restful import Api, Resource
from server_timing import Timing as ServerTiming from server_timing import Timing as ServerTiming
from http import HTTPStatus
import server.common.rest as common_rest import server.common.rest as common_rest
from server.common.errors import DatasetAccessError, RequestException
from server.common.utils import path_join, Float32JSONEncoder
from server.common.data_locator import DataLocator from server.common.data_locator import DataLocator
from server.common.errors import DatasetAccessError, RequestException
from server.common.health import health_check from server.common.health import health_check
from server.common.utils.utils import path_join, Float32JSONEncoder
from server.data_common.matrix_loader import MatrixDataLoader from server.data_common.matrix_loader import MatrixDataLoader
from functools import wraps
webbp = Blueprint("webapp", "server.common.web", template_folder="templates") webbp = Blueprint("webapp", "server.common.web", template_folder="templates")
ONE_WEEK = 7 * 24 * 60 * 60 ONE_WEEK = 7 * 24 * 60 * 60
@@ -86,23 +96,15 @@ def dataset_index(url_dataroot=None, dataset=None):
cache_manager = current_app.matrix_data_cache_manager cache_manager = current_app.matrix_data_cache_manager
with cache_manager.data_adaptor(url_dataroot, location, app_config) as data_adaptor: with cache_manager.data_adaptor(url_dataroot, location, app_config) as data_adaptor:
data_adaptor.set_uri_path(f"{url_dataroot}/{dataset}") data_adaptor.set_uri_path(f"{url_dataroot}/{dataset}")
dataset_title = app_config.get_title(data_adaptor) args = {"SCRIPTS": scripts, "INLINE_SCRIPTS": inline_scripts}
return render_template( return render_template("index.html", **args)
"index.html", datasetTitle=dataset_title, SCRIPTS=scripts, INLINE_SCRIPTS=inline_scripts
)
except DatasetAccessError as e: except DatasetAccessError as e:
return common_rest.abort_and_log( return common_rest.abort_and_log(
e.status_code, f"Invalid dataset {dataset}: {e.message}", loglevel=logging.INFO, include_exc_info=True e.status_code, f"Invalid dataset {dataset}: {e.message}", loglevel=logging.INFO, include_exc_info=True
) )
@webbp.route("/health", methods=["GET"])
@cache_control_always(no_store=True)
def health():
config = current_app.app_config
return health_check(config)
@webbp.errorhandler(RequestException) @webbp.errorhandler(RequestException)
def handle_request_exception(error): def handle_request_exception(error):
return common_rest.abort_and_log(error.status_code, error.message, loglevel=logging.INFO, include_exc_info=True) return common_rest.abort_and_log(error.status_code, error.message, loglevel=logging.INFO, include_exc_info=True)
@@ -181,9 +183,9 @@ def dataroot_test_index():
data += f"<p>Logged in as {auth.get_user_id()} / {auth.get_user_name()} / {auth.get_user_email()}</p>" data += f"<p>Logged in as {auth.get_user_id()} / {auth.get_user_name()} / {auth.get_user_email()}</p>"
if auth.requires_client_login(): if auth.requires_client_login():
if server_config.auth.is_user_authenticated(): if server_config.auth.is_user_authenticated():
data += "<p><a href='/logout'>Logout</a></p>" data += f"<p><a href='{auth.get_logout_url(None)}'>Logout</a></p>"
else: else:
data += "<p><a href='/login'>Login</a></p>" data += f"<p><a href='{auth.get_login_url(None)}'>Login</a></p>"
datasets = [] datasets = []
for dataroot_dict in server_config.multi_dataset__dataroot.values(): for dataroot_dict in server_config.multi_dataset__dataroot.values():
@@ -221,6 +223,13 @@ def dataroot_index():
return redirect(config.server_config.multi_dataset__index) return redirect(config.server_config.multi_dataset__index)
class HealthAPI(Resource):
@cache_control(no_store=True)
def get(self):
config = current_app.app_config
return health_check(config)
class DatasetResource(Resource): class DatasetResource(Resource):
"""Base class for all Resources that act on datasets.""" """Base class for all Resources that act on datasets."""
@@ -230,7 +239,8 @@ class DatasetResource(Resource):
class SchemaAPI(DatasetResource): class SchemaAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) # TODO @mdunitz separate dataset schema and user schema
@cache_control(no_store=True)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
return common_rest.schema_get(data_adaptor) return common_rest.schema_get(data_adaptor)
@@ -243,8 +253,15 @@ class ConfigAPI(DatasetResource):
return common_rest.config_get(current_app.app_config, data_adaptor) return common_rest.config_get(current_app.app_config, data_adaptor)
class UserInfoAPI(DatasetResource):
@cache_control_always(no_store=True)
@rest_get_data_adaptor
def get(self, data_adaptor):
return common_rest.userinfo_get(current_app.app_config, data_adaptor)
class AnnotationsObsAPI(DatasetResource): class AnnotationsObsAPI(DatasetResource):
@cache_control(public=True, max_age=ONE_WEEK) @cache_control(public=True, no_store=True)
@rest_get_data_adaptor @rest_get_data_adaptor
def get(self, data_adaptor): def get(self, data_adaptor):
return common_rest.annotations_obs_get(request, data_adaptor) return common_rest.annotations_obs_get(request, data_adaptor)
@@ -301,8 +318,18 @@ class LayoutObsAPI(DatasetResource):
return common_rest.layout_obs_put(request, data_adaptor) return common_rest.layout_obs_put(request, data_adaptor)
def get_api_resources(bp_api, url_dataroot=None): def get_api_base_resources(bp_base):
api = Api(bp_api) """Add resources that are accessed from the api_base_url"""
api = Api(bp_base)
# Diagnostics routes
api.add_resource(HealthAPI, "/health")
return api
def get_api_dataroot_resources(bp_dataroot, url_dataroot=None):
"""Add resources that refer to a dataset"""
api = Api(bp_dataroot)
def add_resource(resource, url): def add_resource(resource, url):
"""convenience function to make the outer function less verbose""" """convenience function to make the outer function less verbose"""
@@ -311,6 +338,7 @@ def get_api_resources(bp_api, url_dataroot=None):
# Initialization routes # Initialization routes
add_resource(SchemaAPI, "/schema") add_resource(SchemaAPI, "/schema")
add_resource(ConfigAPI, "/config") add_resource(ConfigAPI, "/config")
add_resource(UserInfoAPI, "/userinfo")
# Data routes # Data routes
add_resource(AnnotationsObsAPI, "/annotations/obs") add_resource(AnnotationsObsAPI, "/annotations/obs")
add_resource(AnnotationsVarAPI, "/annotations/var") add_resource(AnnotationsVarAPI, "/annotations/var")
@@ -323,6 +351,25 @@ def get_api_resources(bp_api, url_dataroot=None):
return api return api
def handle_api_base_url(app, app_config):
"""If an api_base_url is provided, then an inline script is generated to
handle the new API prefix"""
api_base_url = app_config.server_config.get_api_base_url()
if not api_base_url:
return
sha256 = hashlib.sha256(api_base_url.encode()).hexdigest()
script_name = f"api_base_url-{sha256}.js"
script_path = os.path.join(app.root_path, "../common/web/templates", script_name)
with open(script_path, "w") as fout:
fout.write("window.CELLXGENE.API.prefix = `" + api_base_url + "${location.pathname}api/`;\n")
dataset_configs = [app_config.default_dataset_config] + list(app_config.dataroot_config.values())
for dataset_config in dataset_configs:
inline_scripts = dataset_config.app__inline_scripts
inline_scripts.append(script_name)
class Server: class Server:
@staticmethod @staticmethod
def _before_adding_routes(app, app_config): def _before_adding_routes(app, app_config):
@@ -330,7 +377,8 @@ class Server:
pass pass
def __init__(self, app_config): def __init__(self, app_config):
self.app = Flask(__name__, static_folder="../common/web/static") self.app = Flask(__name__, static_folder=None)
handle_api_base_url(self.app, app_config)
self._before_adding_routes(self.app, app_config) self._before_adding_routes(self.app, app_config)
self.app.json_encoder = Float32JSONEncoder self.app.json_encoder = Float32JSONEncoder
server_config = app_config.server_config server_config = app_config.server_config
@@ -347,28 +395,59 @@ class Server:
self.app.register_blueprint(webbp) self.app.register_blueprint(webbp)
api_version = "/api/v0.2" api_version = "/api/v0.2"
api_base_url = server_config.get_api_base_url()
api_path = "/"
if api_base_url:
parse = urlparse(api_base_url)
api_path = parse.path
bp_base = Blueprint("bp_base", __name__, url_prefix=api_path)
base_resources = get_api_base_resources(bp_base)
self.app.register_blueprint(base_resources.blueprint)
if app_config.is_multi_dataset(): if app_config.is_multi_dataset():
# NOTE: These routes only allow the dataset to be in the directory # NOTE: These routes only allow the dataset to be in the directory
# of the dataroot, and not a subdirectory. We may want to change # of the dataroot, and not a subdirectory. We may want to change
# the route format at some point # the route format at some point
for dataroot_dict in server_config.multi_dataset__dataroot.values(): for dataroot_dict in server_config.multi_dataset__dataroot.values():
url_dataroot = dataroot_dict["base_url"] url_dataroot = dataroot_dict["base_url"]
bp_api = Blueprint( bp_dataroot = Blueprint(
f"api_dataset_{url_dataroot}", __name__, url_prefix=f"/{url_dataroot}/<dataset>" + api_version f"api_dataset_{url_dataroot}",
__name__,
url_prefix=f"{api_path}/{url_dataroot}/<dataset>" + api_version,
) )
resources = get_api_resources(bp_api, url_dataroot) dataroot_resources = get_api_dataroot_resources(bp_dataroot, url_dataroot)
self.app.register_blueprint(resources.blueprint) self.app.register_blueprint(dataroot_resources.blueprint)
self.app.add_url_rule( self.app.add_url_rule(
f"/{url_dataroot}/<dataset>/", f"/{url_dataroot}/<dataset>",
f"dataset_index_{url_dataroot}", f"dataset_index_{url_dataroot}",
lambda dataset, url_dataroot=url_dataroot: dataset_index(url_dataroot, dataset), lambda dataset, url_dataroot=url_dataroot: dataset_index(url_dataroot, dataset),
methods=["GET"], methods=["GET"],
) )
self.app.add_url_rule(
f"/{url_dataroot}/<dataset>/",
f"dataset_index_{url_dataroot}/",
lambda dataset, url_dataroot=url_dataroot: dataset_index(url_dataroot, dataset),
methods=["GET"],
)
self.app.add_url_rule(
f"/{url_dataroot}/<dataset>/static/<path:filename>",
f"static_assets_{url_dataroot}",
view_func=lambda dataset, filename: send_from_directory("../common/web/static", filename),
methods=["GET"],
)
else: else:
bp_api = Blueprint("api", __name__, url_prefix=api_version) bp_api = Blueprint("api", __name__, url_prefix=f"{api_path}{api_version}")
resources = get_api_resources(bp_api) resources = get_api_dataroot_resources(bp_api)
self.app.register_blueprint(resources.blueprint) self.app.register_blueprint(resources.blueprint)
self.app.add_url_rule(
"/static/<path:filename>",
"static_assets",
view_func=lambda filename: send_from_directory("../common/web/static", filename),
methods=["GET"],
)
self.app.matrix_data_cache_manager = server_config.matrix_data_cache_manager self.app.matrix_data_cache_manager = server_config.matrix_data_cache_manager
self.app.app_config = app_config self.app.app_config = app_config
-1
View File
@@ -1,4 +1,3 @@
# import the built in auth types so they can be registered # import the built in auth types so they can be registered
import server.auth.auth_none # noqa: F401 import server.auth.auth_none # noqa: F401

Some files were not shown because too many files have changed in this diff Show More