Compare commits

...
Author SHA1 Message Date
Roy Zheng 96cb19c89b chore: CCIE-4984 conform to open sourcing guidelines 2025-08-11 22:52:38 +00:00
Justin Kiggins eb1dc8944f Update README.md (#2725)
CodeQL Scan / Analyze (javascript) (push) Has been cancelled
CodeQL Scan / Analyze (python) (push) Has been cancelled
Compatibility Tests / docker-build (push) Has been cancelled
Compatibility Tests / cxg:latest os: py:3.12 anndata:==0.10.9 (push) Has been cancelled
Compatibility Tests / cxg:latest os:macos-13 py:3.10 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:macos-13 py:3.11 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:macos-13 py:3.12 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:macos-latest py:3.10 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:macos-latest py:3.11 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:macos-latest py:3.12 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:ubuntu-latest py:3.10 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:ubuntu-latest py:3.11 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:latest os:ubuntu-latest py:3.12 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:macos-13 py:3.10 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:macos-13 py:3.11 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:macos-13 py:3.12 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:macos-latest py:3.10 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:macos-latest py:3.11 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:macos-latest py:3.12 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:ubuntu-latest py:3.10 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:ubuntu-latest py:3.11 anndata:latest (push) Has been cancelled
Compatibility Tests / cxg:main os:ubuntu-latest py:3.12 anndata:latest (push) Has been cancelled
Close inactive pull requests / close-issues (push) Has been cancelled
Trademark
2025-05-14 12:03:19 -07:00
Ronen fd8b47b78e chore: Release 1.3.0 (#2700)
* Bump version: 1.2.0 → 1.3.0-rc.0

* Bump version: 1.3.0-rc.0 → 1.3.0
2024-09-12 16:05:14 -04:00
Ronen 487bd13ff8 chore: add support for python 3.12 (#2694) 2024-09-12 14:16:46 -04:00
Ronen eb743efd9a fix: webpack upgrade (#2691) 2024-09-09 13:34:44 -04:00
Ronen 67d152e108 fix: mlflow critical upgrade (#2690) 2024-09-09 12:29:09 -04:00
Timmy Huang c425d2e0b0 fix: underscore snakecase notation to hyphenated snakecase for diffexp-may-be-slow (#2687) 2024-09-05 10:09:13 -07:00
Timmy Huangandkaloster 7bf5add6ef chore: Fix compatibility tests (#2685)
* chore: Fix compatibility tests

* DEBUGGGG

* fix: update deps, fix unit tests

* fix: FE deps

* chore: update compatibility matrix

---------

Co-authored-by: kaloster <rkalo@contractor.chanzuckerberg.com>
2024-09-05 09:26:40 -07:00
dependabot[bot]andTimmy Huang 4281a8f816 chore(deps-dev): bump follow-redirects from 1.15.1 to 1.15.6 in /client (#2661)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.1 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.1...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
2024-04-18 14:54:49 +00:00
Emanuele Bezzi 53e9edfec1 docs: change link to the CZI science community Slack (#2662) 2024-03-19 10:19:28 -07:00
atarashansky 74fbec35fe chore: Release version 1.2.0 (#2652)
* Bump version: 1.1.2 → 1.2.0-rc.0

* Bump version: 1.2.0-rc.0 → 1.2.0
2023-12-19 14:13:08 -08:00
atarashansky 5d4c782f3a chore: lower pinned requirements for backend server (#2651)
* chore: lower pinned requirements for backend server

* update one requirement
2023-12-19 13:39:47 -08:00
atarashansky 6505f6cbf5 chore: upgrade backend dependencies (#2641)
chore: upgrade backend dependencies (#2641)
2023-11-29 14:16:39 -08:00
Severiano BadajozandSeve Badajoz 4bb9a2b834 chore: update dependencies (#2636)
* chore: update dependencies

* fix babel plugins and lint errors

* switch out obselete browser plugin

* add babel config for jest

* revert some babel/jest package bumps

* npm install

* tmp remove werkzeug pin

---------

Co-authored-by: Seve Badajoz <severiano.badajoz@chanzuckerberg.com>
2023-10-20 21:08:09 -04:00
maniarathi 2d7aab3a9a fix: override default so that issues are not marked as stale (#2627) 2023-08-23 21:22:17 -04:00
maniarathi f749733d54 feat: Add a Github Action that automatically closes stale PRs. (#2626) 2023-08-22 22:08:22 -04:00
atarashanskyandatarashansky ffcf6eb5d8 chore: Release version 1.1.2 (#2611)
* Bump version: 1.1.1 → 1.1.2-rc.0

* fix: release candidate 1.1.2

* Bump version: 1.1.2-rc.0 → 1.1.2

* promote

---------

Co-authored-by: atarashansky <atarashansky@CZIMACOS3990.hsd1.ma.comcast.net>
2023-04-26 15:01:57 -04:00
atarashanskyandatarashansky c209a9bca7 fix: pin flask requirement to be less than 2.3 (#2609)
Co-authored-by: atarashansky <atarashansky@CZIMACOS3990.hsd1.ma.comcast.net>
2023-04-26 13:58:40 -04:00
Kuni Katsuya 85319d45a4 chore: tech issue template 2023-02-06 15:46:54 -08:00
Andrew Tolopko 514f1627ea Update license copyright year (#2600) 2023-02-03 16:01:53 -05:00
Severiano Badajoz 127de34a20 feat: Add PR Linting (#2560) 2022-10-24 18:08:25 +00:00
Andrew Tolopko 54ab9d79b6 docs: fix test pypi install command (#2515)
fix test pypi install command in release_process.md
2022-09-29 19:30:11 +00:00
Andrew Tolopko f48d06fb90 Bump version: 1.1.0 → 1.1.1 (#2572) 2022-09-21 15:33:28 -04:00
Severiano BadajozandSeve Badajoz 84563291a0 fix: fix static file serving in webpack (#2574)
Co-authored-by: Seve Badajoz <severiano.badajoz@chanzuckerberg.com>
2022-09-21 18:03:03 +00:00
Andrew Tolopko ddb601c103 feat: annotate command improvements (#2568)
* Replace --input-h5ad-file with a positional argument, for consistency with other CLI commands
* Replace --update-h5ad-file with --overwrite, for consistency with `prepare` command.
* Fix/clarify various help descriptions
* Fix final output message when input file is overwritten
* Fix annotate top-level help description
2022-09-15 11:55:58 -04:00
ashin-czi 450261f109 chore: updating Annotate favicon and logo icon (#2556)
* chore: replacing html icon with png icon and updating favicon
2022-08-25 16:01:03 +00:00
ashin-czi c0f4ad6dfa docs: updating readme and display version for rebranding (#2550)
* docs: updating readme and display version for rebranding

- CELLxGENE -> CELLxGENE Annotate
- reverting __init__.py display_version back to "cellxgene"
- changing page titles to "CELLxGENE | Annotate"
2022-08-25 15:41:26 +00:00
atarashanskyandatarashansky ff5da77372 fix unit tests (#2562)
Co-authored-by: atarashansky <atarashansky@CZIMACOS3990.local>
2022-08-25 08:02:58 -07:00
a9044b01df release version 1.1.0 (#2548)
* Bump version: 1.0.1 → 1.1.0-rc.0

* bump version to v1.1.0

* Bump version: 1.1.0-rc.0 → 1.1.0-rc.1

* bump version to v1.1.0 rc1

rc0 was previously used by an aborted release attempt

* Bump version: 1.1.0-rc.1 → 1.1.0

Co-authored-by: Seve Badajoz <severiano.badajoz@chanzuckerberg.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2022-08-23 10:02:23 -07:00
ashin-czi 11b948369f fix: reverting back to old icon 2022-08-18 14:49:59 -04:00
ashin-czi a2dc0c25b1 chore: updating Annotate favicon 2022-08-11 13:30:19 -04:00
Andrew Tolopko a9ef01a6f9 fix test (#2549)
address issues building mlflow model in GHA test env
2022-07-29 11:17:04 -04:00
Andrew Tolopko 03d9e8e6aa fix unit test
avoid performing pip installs for mlflow-based tests
2022-07-29 09:57:55 -04:00
Andrew Tolopko 30e19e47c6 feat: add cli annotation subcommand (#2539)
add `cellxgene annotate` subcommand for invoking MLflow model to generate new `obs` annotations, initially intended for cell type annotations.
2022-07-29 05:15:54 -07:00
Emanuele Bezzi d2b20129f7 use fsspec.download for S3 artifacts + additional logging (#2536) 2022-07-29 11:05:36 +00:00
Rohan Agarwal 6c86216f6b Update actions/setup-python version (#2528) 2022-07-28 21:17:46 +00:00
Andrew Tolopko 4df50a7677 fix: s3 user annotations (#2541)
do _not_ attempt to create the user annotations directory if an s3 location is specified
2022-07-28 20:00:15 +00:00
Andrew Tolopko 69a6d52240 fix embedding selection (#2543)
reverted code to previous implementation that was presumably changed due to a "destructuring assignment" lint error; explicitly ignoring error now
2022-07-28 15:49:33 -04:00
Andrew Tolopko 06da05eb9f skip failing tests to unblock adding of new features (#2545)
skipped tests will be reinstated in future issues
2022-07-28 15:11:36 -04:00
Severiano BadajozandSeve Badajoz d753441acc chore(webpack): remove script-ext-html-webpack-plugin (#2534)
Co-authored-by: Seve Badajoz <severiano.badajoz@chanzuckerberg.com>
2022-07-12 13:42:20 -04:00
8b4c1e418e chore: replace optimize-css-assets-webpack-plugin with css-minimizer-webpack-plugin (#2525)
* Replace optimize-css-assets-webpack-plugin with css-minimizer-webpack-plugin

* Fix lint issues in JS

* remove favicons

* update snapshots

* update annotations snapshots

Co-authored-by: Trent Smith <1429913+Bento007@users.noreply.github.com>
Co-authored-by: Seve Badajoz <sbadajoz@chanzuckerberg.com>
2022-06-07 13:13:17 -07:00
Madison Dunitz f2bd6ebce1 remove unnecessary postgres req (#2524) 2022-05-27 12:15:17 -07:00
Bruce Martin de44739f8b add fsspec support to gene set and cell annotations (#2512) 2022-04-20 18:18:47 -07:00
Severiano Badajoz 002c9a5c7f release version 1.0.1 (#2497)
* Bump version: 1.0.0 → 1.0.1-rc.0

* Bump version: 1.0.1-rc.0 → 1.0.1
2022-02-28 15:18:21 -08:00
dependabot[bot] e6a0351079 chore(deps): bump nanoid from 3.1.25 to 3.3.1 in /client (#2488)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.25 to 3.3.1.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.25...3.3.1)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-25 09:57:22 -08:00
dependabot[bot] 38fdc649f5 chore(deps): bump axios from 0.21.1 to 0.21.4 in /client (#2491)
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-24 14:20:17 -08:00
dependabot[bot] 69a5d5e837 chore(deps): bump tmpl from 1.0.4 to 1.0.5 in /client (#2490)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-24 14:05:26 -08:00
dependabot[bot] b854c58eea chore(deps): bump simple-get from 3.1.0 to 3.1.1 in /client (#2489)
Bumps [simple-get](https://github.com/feross/simple-get) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/feross/simple-get/releases)
- [Commits](https://github.com/feross/simple-get/compare/v3.1.0...v3.1.1)

---
updated-dependencies:
- dependency-name: simple-get
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-24 13:46:07 -08:00
dependabot[bot] 2d650ba50d chore(deps): bump follow-redirects from 1.14.2 to 1.14.8 in /client (#2484)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.2 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.2...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-24 11:57:08 -08:00
Ben MR 8bac98f25c Fix float16 support [#2379] (#2483)
* Fix float16 support [#2379]

Convert to float32 on startup unless backed, in which case error. scipy does not support complex slicing from float16 data so this is the easiest fix for now.

* minor msg change
2022-02-10 15:20:47 -08:00
maniarathi ceb0cc6f27 Update license to 2022. (#2478) 2022-01-14 14:07:35 -05:00
Severiano Badajoz 853976b716 release version 1.0.0 (#2466)
* Bump version: 0.19.0 → 1.0.0-rc.0

* Bump version: 1.0.0-rc.0 → 1.0.0
2021-11-02 14:53:26 -07:00
Emanuele Bezzi a344f292a9 Restore logo and favicon in docs/ (#2464) 2021-10-14 21:13:04 +00:00
Emanuele Bezzi 15d2ce62b8 Migrate to the new documentation website (#2456) 2021-10-14 20:54:54 +00:00
Colin Megill 8e011314e9 Remove log (#2463) 2021-10-14 18:02:54 +00:00
Ben MR c81731d32c [FE: fix #2371] fix for when labels are null (#2462)
Categoricals can have nulls
2021-10-05 12:17:34 -07:00
Colin Megill 3b091d888d Update diffexpButtons.js (#2461) 2021-10-04 15:35:55 -07:00
Timmy Huang 79aeedc6c3 refactor: 2235 remove cookie banner (#2459) 2021-10-04 11:26:01 -07:00
Colin Megill e6bacf043b typo fix (#2460) 2021-10-01 18:28:47 -07:00
Colin Megill be020deaa6 Update infoMenu.js (#2455)
Fixes #2452
2021-09-29 13:00:55 -07:00
Emanuele Bezzi 4034256d9c Create CNAME file to point docs to the new website (#2453)
Add a CNAME file that will redirect the current github pages (hosted at https://chanzuckerberg.github.io/cellxgene/) to the new website.
2021-09-29 13:28:49 -04:00
Ben MR 5163e35e1f fix #2446 update release docs and commands (#2448)
* update release docs and commands

* update doc on release-test install
2021-09-29 15:06:35 +00:00
Ben MR f4986c9c7d Release version 0.19.0 (#2444)
* Bump version: 0.18.0 → 0.19.0-rc.0

* release can 0.19.0

* Bump version: 0.19.0-rc.0 → 0.19.0-rc.1

* bump rc

* Bump version: 0.19.0-rc.1 → 0.19.0

* release final
2021-09-28 11:10:24 -07:00
Colin Megill a0011b0872 cursor pointer on mit license and version (#2447) 2021-09-27 09:47:34 -07:00
Colin Megill 77fff0bd9c Remove pointer on gene (#2445)
* Remove grab

Fixes #2438

* snapshot

* pointer

* snapshot
2021-09-24 12:21:32 -07:00
Ben MR 6a4884d97c Re-add bumpversion to makefile (#2443) 2021-09-23 22:08:00 +00:00
Colin Megill 15714e9924 Remove grab (#2439)
* Remove grab

Fixes #2438

* snapshot
2021-09-23 10:30:55 -07:00
Severiano Badajoz 5eb0ecc997 chore: update frontend packages (#2442)
* run audit

* migrate husky
2021-09-22 10:22:44 -07:00
Andrew Tolopko e2772d1da9 #2410 reenable scheduled compatibility test (#2440)
re-enable scheduled compat matrix tests on main branch; was temporarily commented out for testing on branch
2021-09-21 17:27:45 -04:00
Colin Megill 7f068cd576 Reverse gene list order (#2432)
Fixes #2420
2021-09-21 11:53:50 -07:00
jacobrheath e4961a579e feat: removing sastisfaction gh action for gh app (#2441) 2021-09-21 10:09:30 -07:00
Andrew Tolopko c0263f6ce5 #2410 update compatibility test matrix (#2431)
Github Actions Workflow updates compatibility matrix:
- Added MacOS Catalina and Big Sur to test compat matrix
- Added Python 3.9 to test compat matrix, but avoid running 3.9 for matrix jobs that do not have `tables` pypi build available for the given env
- Maintains running tests on both cellxgene main branch and latest pypi release.
- Add explicit matrix exclusions for matrix combinations that will never pass (see comments).
- Numerous refactorings to the workflow config to simplify matrix. Basically a rewrite.
- The anndata pkg is now tested at a pinned release and at latest release, but no longer using `master` branch version. To limit cross-product explosion of matrix jobs, the pinned anndata version is only tested on py3.8 and cellxgene latest release.
- Run unit and smoke tests in a single job, to improve speed, reduce workflow complexity and the number of jobs. Also fixes the redundant testing of unit tests. Within each job, the unit and smoke tests are run in separate steps for ease of troubleshooting. 
- Fixed termination of backend server to allow both smoke tests to run within a single job (both attempt to use 5005 port, sequentially, but first server was not being terminated).
- Replaced `continue-on-error: true` with `fail-fast: false`, which allows all matrix jobs to run independently, while also ensuring the that entire workflow is flagged as failed if any matrix job fails
-  The `smoke-test-annotations` fail intermittently and have been disabled. Fix will be addressed in story: https://app.zenhub.com/workspaces/single-cell-5e2a191dad828d52cc78b028/issues/chanzuckerberg/cellxgene/2433
2021-09-21 08:32:19 -04:00
Madison Dunitz 3ebbb0ccbf move common code into server, update tests and makefile (#2425)
* move common code into server, update tests and makefile

remove backend directory, refactor

update smoke tests
2021-09-20 18:50:06 -07:00
Ben MR 97caa5bcaa Upgrade min versions of dependencies [zh2426] (#2436)
* Update dependencies

Flask to 2.0
Scipy etc. bumped to latest version not supporting 3.5
Others bumped to latest where possible

* py min v

* relax py version

* revert reqs changes
2021-09-20 17:30:30 -07:00
Severiano Badajoz f49c3d8fe7 feat: remove info drawer and reintroduce singleton categories (#2421)
* refactor: drop non-session auth from frontend

* remove dataset drawer and reintroduce singleton values
2021-09-20 16:58:55 +00:00
Ben MR ef2ab07ca0 Clean up dead/hosted code [zh2310] (#2430)
* Clean up dead/hosted code

* Remove schema conversion tool and related
* Remove cxg references
* Remove locust

* missed a spot

* Remove aws secret manager

* Merge branch 'main' into brodgers/2310/code-cleanup-v1

* cleanup merge
2021-09-17 20:41:12 +00:00
Ben MR 69e159916e [zh2311] Remove auth (#2427)
* remove auth

* Remove all auth code

zh2311

* lint

* remove auth from e2e tests conf
2021-09-17 18:47:52 +00:00
Severiano Badajoz a239d8636d fix: more accurate validation on geneset name (#2429) 2021-09-15 16:40:34 -07:00
Severiano Badajoz 0634160c0c Fix unit tests (#2428)
* only create toaster if we're on the browser

* drop/fix references to window

* add back fe unit tests
2021-09-15 00:43:59 +00:00
Severiano Badajoz 01013bcf04 refactor: drop non-session auth from frontend (#2419) 2021-09-14 15:55:46 -07:00
Madison Dunitz f9c744327a Dunitz/2308-remove hosted (#2399)
* remove code for and references to czi-hosted

* revert client change
2021-09-02 13:18:35 -04:00
Severiano Badajoz 780852fd49 docs: release 0.18.0 (#2413)
* Bump version: 0.17.0 → 0.18.0-rc.0

* Bump version: 0.18.0-rc.0 → 0.18.0
2021-08-31 15:52:30 -07:00
Colin Megill de03129061 Spinner on color by geneset when closed (#2412)
* take 2

* take 2.1

* bruce test

* remove component did mount

* working
2021-08-27 12:15:47 -07:00
Severiano Badajoz 2715dba703 fix: update/remove deprecated packages (#2411)
* replace deprecated packages

* more updates
2021-08-26 15:18:43 -07:00
Bruce Martin 154d099fef X float16 support (#2406)
* float16 support

* fix type checks

* PR review comments

* add tests for custom json encoder; rename and comment for posterity

* lint

* typos
2021-08-25 11:12:49 -07:00
eaae6df5e3 TS Revert (1) (#2402)
* revert all commits to before Typescript migration

* update compat workflow to match latest deps (#2335)

* update compat workflow to match latest deps

* attempt to debug

* attempt to debug

* remove debugging code

* typo

* update deps to match desktop (#2340)

* fix: don't run lint with `--fix` on push tests (#2273)

* fix: don't run lint with `--fix` on push tests

* npx

Co-authored-by: maniarathi <mani.arathi@gmail.com>
Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>

* rename X_approx_distribution to X_approximate_distribution (#2337)

* Correctly handle non-finite numbers in heuristic determination of X distribution (#2342)

* handle non-finites explicitly

* improve and test edge case handling for distribution estimation

* revert debugging changes

* code readability

* clean up type inferencing (#2332)

* unit tests for 64 bit conversion

* clean up type handling

* type inference tests

* more type inference fixes

* use schema to determine user intent for data typing

* stop using deprecated API

* fbs type encoding test

* add missing test

* add more tests

* correctly infer X type for CXG adaptor

* lint

* fix typo

* ts migration

* cleanup from PR review

* lint

* PR review changes

* remove unused packages from client (#2359)

* remove unused packages from client

* add missing peer dep

* fix: disable FE auth testing on compatibility tests (#2377)

* update: release process (#2277)

Co-authored-by: maniarathi <mani.arathi@gmail.com>

* fix: remove spaces in param setup (#2380)

* delete deploy workflow (#2396)

* undo reformatting which now does not pass lint

* fix snapshots which changed due to npm dep changes

* add missing quoting to snapshot

* another snapshot typo fix

* TS Revert (2) - replay PR #2347 and #2354 (#2403)

* replay edits from PR 2347

* TS Revert (3) - replay edits in PR #2327 (#2404)

* replay edits in PR 2327

* TS Revert (4) - replay PR #2355 (#2405)

* replay edits in PR 2355

* add additional babel config

* reformat with new prettier config

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
Co-authored-by: maniarathi <mani.arathi@gmail.com>
Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
2021-08-23 15:01:36 -07:00
Madison Dunitz 295590a7c6 delete deploy workflow (#2396) 2021-08-18 17:13:04 -05:00
Mim HastieandTimmy Huang b814489328 Added typings to annoMatrix dir. (#2365) (#2371)
* Added typings to annoMatrix directory. (#2365)

* Added review point updates. (#2365)

* Fixed viewOf defect. Added review point updates. (#2365)

* Updated test to use Field key interpolation. (#2365)

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>

* Updated test to use Field key interpolation. (#2365)

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>

* Updated test to use Field key interpolation. (#2365)

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>

* Updated test to use Field key interpolation. (#2365)

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>

* Linting. (#2365)

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>

* Simplified while condition in middleware GC. (#2365)

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>

* Reverted embedding.ts prettier to resolve conflict. (#2365)

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
2021-08-18 13:18:54 -07:00
Severiano BadajozandTimmy Huang 08b03ace60 chore: type general utils (#2381)
* type camera

* type reducer store

* type actionhelpers

* type catchErrorsWrap callsite

* missed camera member var

* type nameCreators

* type makeContinousDimensionName callsite

* type promise limit

* type quantile

* type range

* introduce TypedArray + NumericArray

* type range

* cleanup test

* fix call sites

* type plimit call site

* finish typing camera

* use our TypedArray

* type scientific and sigFig utils and callsites

* simple typings

* type catLabelSort

* type callsite

* type

* callsites

* type camera methods

* swap back to strings, set defaults accordingly

* partially type centroid

* explicit tuple and undefined check

* fix references to this

* call constructor with new and casting

* Revert "introduce TypedArray + NumericArray"

This reverts commit cf21538717.

* explicit tuple

* generics and import fixes

* add unsigned 8 clamped arrray

* back to literals

* use arraytypes

* fix return state

* type more actions

* Update client/src/util/actionHelpers.ts

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>

* properly type dispatch

* properly type thunk

* use new dispatch

* remove nullish coallescer

* use AppDispatch

* generic jsonrequest

* use dispatch again

* lint

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
2021-08-18 00:15:53 +00:00
Bruce Martin 45cecad76a TS typing for Dataframe (#2382)
* initial TS typing

* first cut at Dataframe TS typing

* more Dataframe typing

* comments

* more Dataframe cleanup

* PR review fixes and improvements
2021-08-17 10:43:40 -07:00
Severiano Badajoz 3fdf5cac9d fix: remove spaces in param setup (#2380) 2021-08-13 17:01:10 +00:00
Severiano Badajozandmaniarathi 3c3a794986 update: release process (#2277)
Co-authored-by: maniarathi <mani.arathi@gmail.com>
2021-08-13 00:25:58 +00:00
Bruce Martin 4b417cb5a5 undoable TS typing (#2374)
* type undoable-related TS

* style change to type declaration
2021-08-12 17:14:09 -07:00
Severiano Badajoz 925b785b1f fix: disable FE auth testing on compatibility tests (#2377) 2021-08-12 18:34:04 +00:00
Bruce Martin 660dff256c add array type foundations (#2376) 2021-08-12 07:17:05 -07:00
Timmy Huang 59c475b821 chore: extract schema types (#2375)
* chore: extract schema types

* address comments
2021-08-12 03:12:21 +00:00
Timmy Huang fc60b2acef fix: thuang-fix-tsconfig-path (#2372)
Thanks so much for the quick review, Bruce!!
2021-08-11 01:38:33 +00:00
Timmy Huang 26de334274 chore: add schema types (#2369) 2021-08-04 13:16:18 -07:00
Timmy Huang 95ce39f2e9 chore: Add global type file (#2363) 2021-08-03 21:52:10 +00:00
Bruce Martin 03bb904f24 remove unused packages from client (#2359)
* remove unused packages from client

* add missing peer dep
2021-07-30 20:00:00 -07:00
Colin Megill 01d34580b9 genesets e2e tests, undo/redo (#2327)
* undo redo create

* edit undo redo

* all tests pass, add, edit

* description

* remove RER1

* remove rer1

* remove from hosted
2021-07-30 16:52:49 -07:00
Timmy Huangandbkmartinjr 5ab96ed360 disable formatting rules for eslint and add prettier in lint-staged (#2355)
* disable formatting rules for eslint and add prettier in lint-staged

* update npm modules

* set plugin-proposal-private-methods to loose

* update snapshots due to popover package update

* add missing quotes

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
2021-07-30 12:27:47 -07:00
Bruce Martin 97fb98b4eb API update for tests (#2354) 2021-07-29 20:27:29 -07:00
Bruce Martin 0e7daea737 temp fixes for TS lint (#2352) 2021-07-29 18:31:38 -07:00
Bruce Martin 8136387127 Clean up max-category front-end limit (#2347)
* remove topN category truncation from component rendering layer

* clean up category item limit implementation

* name change for clarity

* fix snapshot

* comments
2021-07-29 16:05:10 -07:00
Mim HastieandTimmy Huang 27575b8d86 Added @typescript-eslint/recommended config with suppressions (#2345)
* Disabled @blueprintjs/classes-constants. #2288.

* thuang-eslint-bp-off (#2344)

* Disabled @blueprintjs/classes-constants on webpack dev and shared. #2288.

* Added TS recommended, suppress lint errors codemod.

* Added per-error/warning ignore for tests.

* Added per-error/warning ignore for configuration.

* Added per-error/warning ignore for src. Removed suppress package.

* Minor linting.

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
2021-07-28 16:06:34 -07:00
Bruce Martin 32f60a1547 clean up type inferencing (#2332)
* unit tests for 64 bit conversion

* clean up type handling

* type inference tests

* more type inference fixes

* use schema to determine user intent for data typing

* stop using deprecated API

* fbs type encoding test

* add missing test

* add more tests

* correctly infer X type for CXG adaptor

* lint

* fix typo

* ts migration

* cleanup from PR review

* lint

* PR review changes
2021-07-28 15:10:12 -07:00
Bruce Martin 1140676106 Correctly handle non-finite numbers in heuristic determination of X distribution (#2342)
* handle non-finites explicitly

* improve and test edge case handling for distribution estimation

* revert debugging changes

* code readability
2021-07-28 14:34:29 -07:00
Bruce Martin 0b1ab02a60 rename X_approx_distribution to X_approximate_distribution (#2337) 2021-07-27 13:43:04 -07:00
1998c0ad63 fix: don't run lint with --fix on push tests (#2273)
* fix: don't run lint with `--fix` on push tests

* npx

Co-authored-by: maniarathi <mani.arathi@gmail.com>
Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
2021-07-27 16:43:01 +00:00
Bruce Martin 2b072e6271 update deps to match desktop (#2340) 2021-07-27 08:29:00 -07:00
Bruce Martin a1c46170b9 update compat workflow to match latest deps (#2335)
* update compat workflow to match latest deps

* attempt to debug

* attempt to debug

* remove debugging code

* typo
2021-07-26 14:32:46 -07:00
934cc5c69b TS migration. #2288. (#2328)
* Added TS. Updated build and linting config. Added types.

* [ts-migrate][.] Rename files from JS/JSX to TS/TSX

Co-authored-by: ts-migrate <>

* [ts-migrate][.] Run TS Migrate

Co-authored-by: ts-migrate <>

* Corrected files mangled by ts-migrate.

* Updated lint config, minor linting.

* Re-enabled Husky.

* Updated tests and config.

* Reverted webpack devtool config.

* Removed obsolete snapshots.

* Added annotations snap.

* Updated tsconfig includes wrt linting.

* Removed ts-migrate.

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
2021-07-26 20:18:17 +00:00
jacobrheath 7328cbdbd5 feat[sastisfaction]: adding non-blocking security linting to cellxgene (#2210)
* adding sastisfaction

* Update sastisfaction.yml

* Update sastisfaction.yml
2021-07-26 12:24:43 -07:00
Bruce Martin 1ea2b7fe80 fix for incorrect stats computation in diff exp t-test (#2318)
* 2211 fixes

* lint

* lint

* add missing test and bug found by test

* change terminology for count distribution

* update scanpy requirement

* update scanpy requirement
2021-07-23 11:36:26 -07:00
Severiano Badajoz 1ebde2213d fix: set count to 15 for testing (#2324) 2021-07-21 22:56:22 +00:00
Severiano Badajoz bbf1950624 fix: decrease the topN count explicitly on hosted (#2320)
* fix: decrease the topN count explicitly on hosted

* lint
2021-07-21 18:12:10 +00:00
Bruce Martin 3d7490e0a9 gene expression perf work (#2305)
* gene expression perf work

* lint
2021-07-16 12:56:42 -07:00
Bruce Martin 0667ad0274 remove experimental reembedding support (#2301)
* remove experimental reembedding support

* lint

* lint

* add prepare requirements to requirements-dev

* oops, revert accidental deletion of import

* more test modifications

* remove obsolete unit tests
2021-07-15 13:55:26 -07:00
Bruce Martin e334fbe96e remove experimental ontology support (#2300)
* remove experimental ontology support

* lint

* remove ontologies from unit tests

* additional test changes
2021-07-14 07:23:38 -07:00
maniarathi 45a8984223 Update license to be 2021. (#2285) 2021-07-12 10:00:17 -07:00
Severiano Badajoz 07816c2f93 docs: release 0.17.0 (#2278)
* Bump version: 0.16.7 → 0.17.0-rc.0

* Bump version: 0.17.0-rc.0 → 0.17.0
2021-07-07 15:07:37 -07:00
Severiano Badajoz 90da04b6c7 fix: update server version to 0.16.7 (#2276) 2021-07-06 23:25:34 +00:00
Severiano Badajoz 873b3a2f1f fix: point to local server (#2274) 2021-07-06 16:07:17 -07:00
Severiano Badajoz 5f976cc4fc fix: convert sparse numpy matrix to ndarray (#2271) 2021-07-06 21:04:50 +00:00
signechambers1 f55c726e2a Adding gene sets documentation to cellxgene docs (#2259)
* Create gene_sets.md

* Add files via upload

* Update gene_sets.md

user guide updates

* Update gene_sets.md

Updates to multi-user

* Update gene_sets_example.csv

* Update gene_sets.md
2021-07-06 10:30:54 -10:00
signechambers1andSeve Badajoz 56fcbae672 Remove safari from supported browsers (#2272)
* Update README.md

* Update package.json

* Update obsoleteHTMLTemplate.html

* remove unneeded polyfills

* explicit deny safari

* remove from webpack and update lock

Co-authored-by: Seve Badajoz <sbadajoz@chanzuckerberg.com>
2021-07-06 10:09:51 -10:00
face1b3033 genesets e2e tests (#2241)
* __test: create geneset

* example dataset test geneset

* delete geneset test

* edit __test

* gene crud

* Update client/Makefile

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>

* copy gene sets separately

* make fix

* ignore test files locally

* csv update

* updated csvs

* fix unit tests for gene set load routes

* add missing fix to czi_hosted unit test

* pin tiledb version, for czi_hosted backend, to <0.9

* Revert tiledb pin to be less than 0.9. Broken tests have been updated in main branch.

* newline, gitignore

* color by and subset

* diffexp sets equal

* add diff exp test class

* fix data class

* diffexp snapshot

* snapshot

* snap3

* snapshot parentInnerhtml

* remove snap

* updated anno snaps

* add test class to gene list div

* new snapshots

* kick off

* Revert "kick off"

This reverts commit 743f551d55.

* remove import

* eol

* revert changes to csv re: gene tests

* global name

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
Co-authored-by: Arathi Mani <arathi.mani@chanzuckerberg.com>
Co-authored-by: maniarathi <mani.arathi@gmail.com>
2021-07-01 21:29:06 -04:00
Severiano Badajoz b714c18e75 feat: frontend geneset validation (#2258)
* add geneset name validation

* validate genes before addition

* display error messages
2021-07-01 22:09:50 +00:00
Severiano Badajoz b8b1d0dd9e fix: reset gene set colorScale on gene set deletion (#2264) 2021-06-30 16:46:44 -07:00
Madison Dunitz 5007d307a2 Dunitz/czi hosted test server (#2254)
update hosted unit tests to use wsgi server instead of cellxgene packages
2021-06-30 15:07:54 -07:00
Severiano Badajoz 82de4178d9 feat: add quick gene lookup functionality (#2250)
* add/remove gene functionality back with geneset style gene

* styling and expansion

* memo gene list to prevent re render
2021-06-23 13:59:10 -04:00
maniarathi 023ae10822 Update unit tests for CXG conversion to check for actual content rather than file names alone which have changed with the recent 0.9 release of tiledb's python package. (#2249)
* Update unit tests for CXG conversion to check for actual content rather than file names alone which have changed with the recent 0.9 release of tiledb's python package.

* Some cleanup

* Undo a bad line
2021-06-19 09:37:05 -07:00
28b526b3fc feat: diffexp returns two genesets (#2230)
* feat: return two lists for diffexp (#2221)

* sp

* split out derive sort order, tests passing

* sp

* return diff exp results in two lists

* update

* copy implementation over to desktop

* add tests for two lists

* small fixes to complete backend implementation

* accept new diffexp response

* map diff exp response to genesets

* delete )

* name diffexp genesets with population names

* take constants out of state and allow width prop to override

* shorten mini-histo properly truncate and resize depending on expansion

* prepend new genesets

* rename data within diffexp action

* backend

* move diffexp ttest to common code module, update tests

* update for unit tests

* reference actual var

Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
Co-authored-by: Madison Dunitz <dunitzm@gmail.com>
2021-06-08 21:02:19 +00:00
Severiano Badajoz 7ed53c0f5b fix: add placeholder test so suite doesn't fail (#2233)
* comment out test suite

* actually just put in placeholdertest
2021-05-26 12:41:16 -07:00
Severiano Badajoz a4050f10e1 feat(geneset): save diff exp result as geneset (#2216)
* update package lock

* allow falsey vals

* remove old diff exp handling

* save diff exp results as geneset

* delete test

* check for undefined or null

* use global geneset description for diffexp

* remove diffexp special code, no longer showing adjpval + logfoldchange

* remove differential map to state

* remove clear from FSM, since we no longer support those actions

* restore controlHelpers test with todo
2021-05-25 10:51:31 -07:00
Madison Dunitz 6f6634a4d9 add action to deploy on on push to canary branch (#2196)
* add action to deploy on on push to canary branch
2021-05-17 16:15:48 -07:00
Severiano Badajoz 265ccf3682 fix: use env specific secret (#2204) 2021-05-13 14:28:16 -07:00
maniarathi 59ec3afbb9 Pin versions of flatbuffer and Flask to be less than 2.0 (#2199) 2021-05-12 15:13:10 -07:00
Madison Dunitz 4666f1f044 add rule for static assest without dataset id (#2194) 2021-05-07 21:17:31 -07:00
Colin Megillandbkmartinjr d04dba225f Filename dialogue includes gene sets (#2185)
* change filenames

* gene-sets to anno dialogue

* logging, lambda

* celllabels

* cell dash labels

* space

* fix 2182 - annotation file name change on the backend

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
2021-05-07 17:32:11 -04:00
Colin Megill 096d8ff1d2 Geneset remove toggle (#2184)
* no toggle, histo at top

* set mean expression, truncation

* gene set
2021-05-05 19:51:05 -04:00
Colin Megill 25b308c532 conditional colon (#2181) 2021-05-04 13:43:02 -04:00
Bruce Martin f2e9aecebe hosted gene sets routes, plus a few bug fixes (#2155)
* first cut at hosted gs routes

* lint

* update tests to match csv parser changes

* update tests to new API

* update gene set name validation rules to match requirements

* add path mapping from dataset to geneset

* add test cases for geneset GET route

* fix test assertion

* remove debugging code

* update gene set uri mapping function

* fix error message

* allow extra user-specified headers in gene set csv file

* clarify comment
2021-04-27 13:58:58 -07:00
maniarathi ebeb1c8818 Fix command to EB to use the artifact.zip file during deployment instead of the source code in the directory. (#2174) 2021-04-23 11:07:57 -07:00
maniarathi b60d20eb2f Fix phony (#2173) 2021-04-22 18:00:00 -07:00
Colin Megill fd2a7a53ab Color by gene set mean expression (#2157)
* colorby histo

* color graph by mean expression

* move var index after returns

* add genesets as an argument

* varindex

* undo redo for mean expression

* destructure

* ternary

* Revert "destructure"

This reverts commit 2d9432c1c7.

* color by mean for diffexp
2021-04-22 13:58:30 -04:00
Bruce Martin 860547ced1 update requirements for server (#2172) 2021-04-21 17:12:16 -07:00
Colin Megillandbkmartinjr 876ceb4d8b Create / edit geneset description (#2139)
* geneset description add

* edit geneset description

* default state for desc

* remove log

* naming, todo

* check for both dup name and desc

* fixes

* do not store gene set modal state in history stack

* Update createGenesetDialogue.js

* Update editGenesetNameDialogue.js

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
2021-04-21 19:50:29 -04:00
Bruce Martin 99a795a688 Updating front-end dependencies (#2167)
* update to webpack 5

* update babel

* update eslint

* update cheerio

* update npm min to v7

* revert engine change

* generate package lock with npm v6 (lockfileVersion 1)

* add region to test setup

* update blueprint popover2

* tabindex changes due to blueprint popover2 revision

* update snapshots

* update lodash and pako

* fix typo

* fix lodash refactoring

* more lodash refactoring

* update babel and blueprintjs

* update jest support packages

* update puppeteer

* update regl

* update react-icons and react-helmet

* update react and react-dom
2021-04-21 07:23:31 -07:00
maniarathiandTimmy Huang e2ce9a90ca Remove updates coming from cookie banner since updates have already been executed. (#2161)
Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
2021-04-19 08:42:34 -07:00
Madison Dunitz 63cf82c60e Dunitz/scale test update (#2159) 2021-04-18 13:33:04 -05:00
Colin Megill c23b25d4e8 geneset description (#2141) 2021-04-12 16:31:21 -04:00
Severiano Badajoz f36d889455 fix: explicitly depend on favicons (#2143) 2021-04-08 15:55:00 -07:00
Severiano Badajoz 4510c8c8a4 Fetch passwords from secretsmanager (#2138)
* add aws secretsmanagerclient

* create custom globalsetup

* consume secret password and enable tests

* update npm
2021-04-05 14:16:10 -07:00
Colin MegillandAmbrose J Carr 6ecdfa4940 Create design_principles.md (#1903)
* Create design_principles.md

* Update design_principles.md

* Update design_principles.md

* Update design_principles.md

* Update design_principles.md

* Update design_principles.md

* Update dev_docs/design_principles.md

Co-authored-by: Ambrose J Carr <ambrosejcarr@users.noreply.github.com>

* Update dev_docs/design_principles.md

Co-authored-by: Ambrose J Carr <ambrosejcarr@users.noreply.github.com>

Co-authored-by: Ambrose J Carr <ambrosejcarr@users.noreply.github.com>
2021-04-01 16:50:09 -04:00
Colin Megill b446bf7144 Handle empty gene set (#2128)
* handle empty gene set case

* merge error, remove conflict markers

* paren
2021-04-01 13:45:47 -04:00
Colin Megill 20bfa4cc97 prepopulate input (#2129) 2021-04-01 06:21:59 -04:00
Bruce Martin 2fa19c756c propagate diffexp state through component tree (#2135) 2021-03-31 12:23:38 -07:00
Bruce Martin ae30b66123 gene set summary progress (#2127)
* revert removal of cache control headers

* checkpoint work on revising summary route

* add summary query support to annoMatrix

* summarize route cleanup

* add mising file

* clean up summarize route

* add summary histogram

* update deps

* lint

* more lint

* lint

* manage crossfiler during gene set state changes

* remove obsolete debugging code

* correctly perform async watch in histogram

* better error handling
2021-03-30 14:43:53 -07:00
Bruce Martin bfb9e1edcc increase default diffexp gene count to 50 (#2130)
* increase default diffexp gene count

* try tiledb version fix
2021-03-30 13:10:09 -07:00
Madison Dunitz ae23c9e5b9 white space change to run tests (#2132)
* update tiledb reqs
2021-03-30 14:34:39 -05:00
Bruce Martin b494dd31f4 revert removal of cache control headers (#2118)
* revert removal of cache control headers

* always generate cache header for health route
2021-03-26 08:49:44 -07:00
Madison Dunitz 78c9d24ed4 Refactor czi_hosted and server into backend directory, pull common code into backend/common, refactor tests (#2102)
* move local_server -> backend/server server-> backend/czi_hosted, pull common code into backend/common update imports, tests and make commands
2021-03-26 00:27:07 -05:00
e6e358ddc8 Gene sets UI, right sidebar refactor (#2097)
* prototyping

* render histos on open gene set

* prototyping

* render histos on open gene set

* factor out add genes to own component

* remove unused import

* mock reducer

* color by geneset stub

* menus and buttons

* geneset dialogue stub

* remove heatmap mock

* componetize histogram

* reenable add genes

* re-add isuserdefined

* test data

* remove have fetched

* add isExpanded state to gene, and pass to histogram

* expand button

* toggleable

* mini

* bump number of genes to 50

* don't clear diffexp on subset

* move create category to top

* render diffexp as geneset

* geneset show mean expression

* gene set reducer

* add geneset UI reducer

* wire e2e gene set loading prototype

* fix sniffing bug

* fix typo

* add gene modals

* client/src/actions/

* add autosave

* rename data-dir cli param

* add geneset, add gene, delete set

* prototype: remove csv upload placeholder

* handle delete gene from set

* prepopulate geneset with genes from modal

* add geneset: rename action

* icons, language consistency

* chevron after

* handle empty string case on genes for create geneset

* edit geneset

* fix language on create

* copy correction

* add popper2
upgrade react popper
upgrade react popper
adding popover2 package

* truncate uses tooltip2

* gene set button text typo

* remove logging

* moving server over

* remove test imports

* don't try to destructure map, use array.from

* fix add gene map datastructure error

* Revert "fix add gene map datastructure error"

This reverts commit b0eed45952.

* name --> genesetName, genes --> geneSymbols

* add gene to geneset, temporary format

* handle empty case, clear form input

* lint -- genesets wasn't passed via props

* userinfo

* move genes string to object conversion to action

* remove tmp gene description

* emptystring default for description

* remove empty string

* remove top level package json

* remove package lock as well

* remove flag for feature toggle

* remove comments in geneset

* comment cleanup

* remove comment

* revert diffexp genes to 10

* color by gene set

* disable color by gene set

* Gene menus are now inline, remove dead prototype code

* remove todo, magic number to variable

* remove jshint in rightsidebar

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>

* remove unused geneset validation code

* tmp format pending geneset description

* move magic number into variable

* reorganize genesetsUI reducer pending tests

* rewire edit given new action name

* add basic validation and feedback for geneset name uniqueness

* mv annoDialog

* mv label, repair paths

* Update client/src/components/brushableHistogram/header.js

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>

* add imports for icon in histo

* update jest snapshots given blueprint/tooltip2 usage of index -1

* ensure no empty paragraph

* intent from blueprint

* remove remainder of jshint references

* do not push undo when autosave fires

* fix autosave bugs

* remove todos

* clamp to util

* scient to util

* revert clearing diffexp

* rename value to be more specific stacked bar

* clean up logging and commetns

* remove gene entry tests pending rewrite

* tab index -1

* update jest snapshot, blueprint tooltip 2

* caret margin

* snapshot update

* ensure histogram is centered

* add geneset actions to config

* comment maybeScientific

* comment clamp

* comment ui reducer

* remove prototype code

* remove error log

* remove references to bl.ocks

* componetize parseBulkGeneString

* catch case where geneset rename same name

* genesetui reducer tests

* add geneset ui to index reducer config

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2021-03-24 16:33:26 -04:00
Timmy Huang 5335c39184 Prod (#2123)
* thuang-126-CZIF-policy-updates (#2111)

* thuang-130-CZIF-policy-change-for-real (#2119)
2021-03-24 10:34:36 -07:00
Bruce Martin a89362c1ad update gene set name validation to match latest requirements (#2117) 2021-03-22 14:54:12 -07:00
Timmy Huang 8d932fb47d thuang-126-CZIF-policy-updates (#2111) (#2112) 2021-03-17 16:07:42 -07:00
Madison Dunitz 920d71e6b7 pin numba req (#2110)
* pin numba req

* make req more flexible
2021-03-16 11:45:27 -05:00
Bruce Martin 31e0326ded gene sets summary route (#2099)
* gene sets summary route

* lint

* clarify return type

* style
2021-03-10 16:02:05 -08:00
Severiano Badajoz 1d3d9237e7 temp disable auth testing (#2092)
* disable auth testing

* revert some deletion
2021-03-04 13:56:48 -08:00
Bruce Martin c037f4eaa6 rename "geneset" to "gene set" in CLI (#2088)
* remove dead code

* rename geneset to gene_set
2021-03-02 15:36:01 -08:00
Bruce Martin b00496198d wire up geneset reducer (#2082)
* first cut at GET /genesets route

* update existing tests to match code changes

* more GET /genesets and initial tests

* add missing test fixture

* geneset validation accepts OTA format

* genesets route: better error handling, more tests

* lint

* genesets reducer and initial load

* fix lint

* add autosave support for genesets

* remove debug logging

* fix typo

* fix another typo

* update smoke test config for genesets

* smoke test fixes

* more fiddling with smoke tests
2021-03-02 12:12:58 -08:00
maniarathi b3aadf6632 Release version 0.16.7 (#2081) 2021-02-27 22:10:53 -08:00
Bruce Martin f3a3820ffa genesets route for local server (#2079)
* first cut at GET /genesets route

* update existing tests to match code changes

* more GET /genesets and initial tests

* add missing test fixture

* geneset validation accepts OTA format

* genesets route: better error handling, more tests

* lint
2021-02-26 17:53:07 -08:00
09466a5c32 fix: server/requirements-dev.txt to reduce vulnerabilities (#2055)
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-RSA-1038401

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com>
2021-02-25 12:24:06 -08:00
Bruce Martin de571ce0b3 fix local_server unit-test target (#2078) 2021-02-23 16:49:43 -08:00
Marcus Kinsella fb61bd6e9c Split out the local backend (#2052)
This splits the backend into two parts: the local backend for desktop cellxgene and the AWS backend for hosted cellxgene. The local backend is in local_server while the hosted remains in server. The general idea is to copy everything from server to local_server, pull unneeded stuff out of local_server, and keep server as-is for this PR. Not touching server means all the infra and deployment code will continue working just as it did before so we can make those changes incrementally.
2021-02-18 12:58:22 -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
512 changed files with 40843 additions and 38329 deletions
+12 -1
View File
@@ -1,5 +1,16 @@
[bumpversion]
current_version = 0.16.0
current_version = 1.3.0
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]
search = version="{current_version}"
+23
View File
@@ -0,0 +1,23 @@
---
name: Tech Issue
about: Engineering-specific technical work that is not product-specific. Engineering team "owns" these issues.
title: ""
labels: tech
assignees: ""
---
## Motivation
Why is this work important to engineers?
## Definition of Done
What should the end result look like? What will have been changed?
## Tasks
Detail the specific tasks that can be used to accomplish the desired changes.
If detailed steps cannot be provided at this time, please file a [Tech Proposal](https://docs.google.com/document/d/1o2vuvl-kXwRJN1nBoPzJS_MAQgDGYnjmPZWa4qRDi-I/edit#heading=h.7dvzhm7gqc3v) instead.
- [ ]
- [ ]
+23
View File
@@ -0,0 +1,23 @@
name: Close inactive pull requests
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: -1 # Do not mark any issues as stale
days-before-pr-stale: 14
days-before-pr-close: 3
stale-pr-message: "This PR has not seen any activity in the past 2 weeks; if no one comments or reviews it in the next 3 days, this PR will be closed."
close-pr-message: "This PR was closed because it has been inactive for 17 days, 3 days since being marked as stale. Please re-open if you still need this to be addressed."
stale-pr-label: "stale"
close-pr-label: "autoclosed"
exempt-draft-pr: true
repo-token: ${{ secrets.GITHUB_TOKEN }}
+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
+82 -88
View File
@@ -2,7 +2,7 @@ name: Compatibility Tests
on:
schedule:
- cron: '0 8 7 * 2'
- cron: "0 8 7 * 2"
push:
branches:
- main
@@ -14,105 +14,99 @@ jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build docker image
run: docker build .
cellxgene-main-with-python-and-anndata-versions:
name: python versions x anndata versions
runs-on: ubuntu-latest
matrix-compatibility-test:
name: cxg:${{ matrix.cellxgene_build }} os:${{ matrix.os }} py:${{ matrix.python-version }} anndata:${{ matrix.anndata_version || 'latest' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8]
anndata-version: [0.6.22.post1, 0.7.1]
test-suite: [smoke-test, smoke-test-annotations]
os: [ubuntu-latest, macos-latest, macos-13]
python-version: ["3.10", "3.11", "3.12"]
cellxgene_build: [main, latest]
# add anndata pinned version test for subset of matrix configurations,
# in order to reduce matrix cross-product explosion
include:
- python-version: 3.12
cellxgene_build: latest
# TODO: dynamically use the literal version in requirements.txt,
# to avoid having to update this in manually in the future
# TODO: Do not bother running this if anndata latest version
# matches this pinned version, to avoid a redundant test
anndata_version: "==0.10.9"
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
# 1. only install the dev requirements on top of what is in the cellxgene pip package
sudo apt-get update && sudo apt-get install -y libhdf5-serial-dev
sed -i 's/-r requirements.txt//' server/requirements-dev.txt
pip install -r server/requirements-dev.txt
# 2. install cellxgene
make pydist install-dist
# 3. install anndata
pip install anndata==${{ matrix.anndata-version }}
# workaround for anndata 0.6.22.post1 bug
[[ "0.6.22.post1" = "${{ matrix.anndata-version }}" ]] && pip install h5py==2.9.0 || true
- name: Tests
run: make unit-test ${{ matrix.test-suite }}
cellxgene-release-with-anndata-master:
name: cellxgene release with anndata master
runs-on: ubuntu-latest
strategy:
matrix:
test-suite: [smoke-test, smoke-test-annotations]
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v1
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: 3.7
- name: Checkout
uses: actions/checkout@v2
python-version: ${{ matrix.python-version }}
- name: Cache env vars
run: echo "PIP_CACHE=`python -m pip cache dir`" >> $GITHUB_ENV
- name: Cache env vars (MacOS)
if: startsWith(matrix.os, 'macos')
run: echo "BREW_CACHE=`brew --cache`" >> $GITHUB_ENV
# FIXME: Only working for Linux
- name: Python cache
uses: actions/cache@v4
with:
path: cellxgene
- name: Checkout tools repo
uses: actions/checkout@v2
path: ${{ env.PIP_CACHE }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Node cache
uses: actions/cache@v4
with:
repository: theislab/anndata
path: anndata
- name: Install dependencies
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Brew cache (MacOS)
if: startsWith(matrix.os, 'macos')
uses: actions/cache@v4
with:
path: ${{ env.BREW_CACHE }}
key: ${{ runner.os }}-brew-
- name: Install dependencies (Ubuntu Linux)
if: startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libhdf5-serial-dev
- name: Install dependencies (MacOS)
if: startsWith(matrix.os, 'macos')
run: brew install hdf5
- name: Install cellxgene from `main` branch
if: matrix.cellxgene_build == 'main'
run: |
cd cellxgene
# 1. only install the dev requirements on top of what is in the cellxgene pip package
make dev-env-client
sed -i 's/-r requirements.txt//' server/requirements-dev.txt
pip install -r server/requirements-dev.txt
# 2. install cellxgene
pip install --upgrade cellxgene
# 3. install anndata
cd ../anndata && pip install -e .
- name: Tests
run: cd cellxgene && make unit-test ${{ matrix.test-suite }}
cellxgene-main-with-anndata-master:
name: cellxgene main with anndata master
runs-on: ubuntu-latest
strategy:
matrix:
test-suite: [smoke-test, smoke-test-annotations]
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Checkout
uses: actions/checkout@v2
with:
path: cellxgene
- name: Checkout tools repo
uses: actions/checkout@v2
with:
repository: theislab/anndata
path: anndata
- name: Install dependencies
make pydist install-dist
- name: Install cellxgene from latest release (pypi.org)
if: matrix.cellxgene_build == 'latest'
run: |
cd cellxgene
sed -i -E 's/^anndata[>=]=[0-9]+.[0-9]+.[0-9]+$/anndata/g' server/requirements.txt
make pydist install-dist dev-env
cd ../anndata
pip install -e .
- name: Tests
run: cd cellxgene && make unit-test ${{ matrix.test-suite }}
pip install --upgrade cellxgene
# install the additional dev requirements on top of what is in the
# cellxgene pip package, which are needed for testing, but otherwise
# keep same pip pkg versions as in the cxg release
sed -i'' -e 's/-r requirements.txt//' server/requirements-dev.txt
pip install -r server/requirements-dev.txt
pip install --force-reinstall numpy==2.0.1 numba>=0.60.0 pandas flatbuffers==2.0.7
- name: Install anndata version per matrix variable
run: pip install anndata${{ matrix.anndata_version }}
- name: Install node
run: make dev-env-client
# Run different types of test separately, to facilitate troubleshooting
- name: Unit Tests - client
run: make unit-test-client
- name: Unit Tests - server
run: make unit-test-server
- name: Smoke Tests
run: make smoke-test
# FIXME: Fails intermittently. See https://app.zenhub.com/workspaces/single-cell-5e2a191dad828d52cc78b028/issues/chanzuckerberg/cellxgene/2415
# - name: Smoke Tests with Annotations
# run: make smoke-test-annotations
-13
View File
@@ -1,13 +0,0 @@
name: Deploy via single cell infra repo
on:
push:
branches: main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: repository dispatch
run: |
curl -XPOST -u czi-sci-single-cell-eng:${{secrets.SCI_GITHUB_TOKEN}} -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/chanzuckerberg/single-cell-infra/dispatches --data '{"event_type": "cellxgene-hook"}'
@@ -0,0 +1,19 @@
name: "Lint PR commit message"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3.4.1
with:
validateSingleCommit: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+58 -50
View File
@@ -14,15 +14,15 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- run: |
git fetch --depth=1 origin +${{github.base_ref}}
- name: Set up Python 3.7
uses: actions/setup-python@v1
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.7
python-version: 3.12
- name: Node cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -31,33 +31,36 @@ jobs:
- name: Install dependencies
run: |
pip install flake8
pip install black
cd client
npm install
- name: Lint with flake8
- name: Format with black and lint with flake8
run: |
make lint-server
- name: Lint src with eslint
working-directory: ./client
run: |
make lint
npx eslint src __tests__
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v1
- uses: actions/checkout@v4
- name: Set up Python 3.12 (pyenv) # pyenv needed for mlflow in cli annotate tests
uses: gabrielfalcao/pyenv-action@v9
with:
python-version: 3.7
default: 3.12
command: pip install -U pip # upgrade pip after installing python
- run: pip install virtualenv # virtualenv needed for mlflow in cli annotate tests
- name: Python cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Node cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -67,64 +70,69 @@ jobs:
run: make pydist install-dist dev-env-server
- name: Unit tests
run: |
make unit-test
bash <(curl -s https://codecov.io/bash) -y .codecov.yml -k server -cF backend,python,unitTest
make unit-test-server unit-test-client
bash <(curl -s https://codecov.io/bash) -y .codecov.yml -k server -cF server,python,unitTest
cd client && ./node_modules/codecov/bin/codecov --yml=../.codecov.yml --root=../ --gcov-root=../ -C -F frontend,javascript,unitTest
smoke-tests:
runs-on: macos-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v1
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.7
python-version: 3.12
- name: Python cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Node cache
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: make pydist install-dist
run: |
pip install setuptools
make pydist install-dist
- name: Smoke tests (without annotations feature)
run: |
cd client && make smoke-test
./node_modules/codecov/bin/codecov --yml=../.codecov.yml --root=../ --gcov-root=../ -C -F frontend,javascript,smokeTest
smoke-tests-annotations:
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: Python cache
uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Node cache
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: make pydist install-dist
- name: Smoke tests (with annotations feature)
run: |
cd client && make smoke-test-annotations
./node_modules/codecov/bin/codecov --yml=../.codecov.yml --root=../ --gcov-root=../ -C -F frontend,javascript,smokeTestAnnotations
# TODO: reinstate: https://github.com/chanzuckerberg/cellxgene/issues/2544
# smoke-tests-annotations:
# runs-on: ubuntu-latest
# timeout-minutes: 20
# steps:
# - uses: actions/checkout@v2
# - name: Set up Python 3.9
# uses: actions/setup-python@v4
# with:
# python-version: 3.9
# - name: Python cache
# uses: actions/cache@v1
# with:
# path: ~/.cache/pip
# key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
# restore-keys: |
# ${{ runner.os }}-pip-
# - name: Node cache
# uses: actions/cache@v1
# with:
# path: ~/.npm
# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
# restore-keys: |
# ${{ runner.os }}-node-
# - name: Install dependencies
# run: make pydist install-dist
# - name: Smoke tests (with annotations feature)
# run: |
# cd client && make smoke-test-annotations
# ./node_modules/codecov/bin/codecov --yml=../.codecov.yml --root=../ --gcov-root=../ -C -F frontend,javascript,smokeTestAnnotations
+4 -1
View File
@@ -15,7 +15,7 @@ dist/
*.egg-info
# Environments
venv/
venv*/
cellxgene/
# client build
@@ -54,3 +54,6 @@ client/.eslintcache
# E2E Testing
ignoreE2E*
# annotate subcmd
.models_cache
+1 -1
View File
@@ -1,3 +1,3 @@
We warmly welcome contributions from the community!
Whether you want to contribute ideas, requests, documentation, or code, you can get started by visiting our [contribution guide](https://chanzuckerberg.github.io/cellxgene/posts/contribute).
Whether you want to contribute ideas, requests, documentation, or code, you can get started by visiting our [contribution guide](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md).
+2 -1
View File
@@ -4,7 +4,8 @@ ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
RUN apt-get update && \
apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev 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
ENTRYPOINT ["cellxgene"]
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2017-2023 Chan Zuckerberg Initiative
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
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
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
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.
+2 -2
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2013
Copyright (c) 2017-2023 Chan Zuckerberg Initiative
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
@@ -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
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
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.
+3
View File
@@ -3,3 +3,6 @@ recursive-include server/common/web/static *
include server/requirements.txt
include server/requirements-prepare.txt
include server/requirements-annotate.txt
include server/converters/schema/hgnc_complete_set.txt.gz
include server/converters/schema/schema_definitions/*
+68 -33
View File
@@ -16,8 +16,13 @@ clean: clean-lite clean-server clean-client
clean-lite:
rm -rf $(CLEANFILES)
clean-%:
cd $(*) && $(MAKE) clean
.PHONY: clean-client
clean-client:
cd client && $(MAKE) clean
.PHONY: clean-server
clean-server:
cd server && $(MAKE) clean
# BUILDING PACKAGE
@@ -28,20 +33,20 @@ build-client:
.PHONY: build
build: clean build-client
git ls-files server/ | grep -v 'server/test/' | cpio -pdm $(BUILDDIR)
git ls-files server/ | cpio -pdm $(BUILDDIR)
cp -r client/build/ $(CLIENTBUILD)
$(call copy_client_assets,$(CLIENTBUILD),$(SERVERBUILD))
cp MANIFEST.in README.md setup.cfg setup.py $(BUILDDIR)
# If you are actively developing in the server folder use this, dirties the source tree
.PHONY: build-for-server-dev
build-for-server-dev: clean-server build-client
$(call copy_client_assets,client/build,server)
build-for-server-dev: clean-server build-client copy-client-assets
.PHONY: copy-client-assets
copy-client-assets:
$(call copy_client_assets,client/build,server)
# TESTING
.PHONY: test
test: unit-test smoke-test
@@ -49,8 +54,22 @@ test: unit-test smoke-test
.PHONY: unit-test
unit-test: unit-test-server unit-test-client
unit-test-%:
cd $(*) && $(MAKE) unit-test
.PHONY: test-server
test-server: unit-test-server smoke-test
.PHONY: unit-test-client
unit-test-client:
cd client && $(MAKE) unit-test
.PHONY: unit-test-server
unit-test-server:
PYTHONWARNINGS=ignore:ResourceWarning coverage run \
--source=server \
--omit=.coverage,venv \
-m unittest discover \
--start-directory test/unit \
--verbose; test_result=$$?; \
exit $$test_result \
.PHONY: smoke-test
smoke-test:
@@ -60,14 +79,9 @@ smoke-test:
smoke-test-annotations:
cd client && $(MAKE) smoke-test-annotations
.PHONY: test-db
test-db:
cd server && $(MAKE) test-db
# FORMATTING CODE
.PHOHY: fmt
.PHONY: fmt
fmt: fmt-client fmt-py
.PHONY: fmt-client
@@ -81,9 +95,10 @@ fmt-py:
.PHONY: lint
lint: lint-server lint-client
.PHONY: lint-server
lint-server:
flake8 server
lint-server: fmt-py
flake8 server --per-file-ignores='test/fixtures/dataset_config_outline.py:F821 test/fixtures/server_config_outline.py:F821 test/performance/scale_test_annotations.py:E501'
.PHONY: lint-client
lint-client:
@@ -96,31 +111,46 @@ pydist: build
cd $(BUILDDIR); python setup.py sdist -d ../dist
@echo "done"
# RELEASE HELPERS
# create new version to commit to main
.PHONY: release-stage-1
release-stage-1: dev-env bump clean-lite gen-package-lock
# Set PART=[major, minor, patch] as param to make bump.
# This will create a release candidate. (i.e. 0.16.1 -> 0.16.2-rc.0 for a patch bump)
.PHONY: bump-version
bump-version:
bumpversion --config-file .bumpversion.cfg $(PART)
# Create new version to commit to main
.PHONY: create-release-candidate
create-release-candidate: bump-version clean-lite gen-package-lock
@echo "Version bumped part:$(PART) and client built. Ready to commit and push"
# build dist and release to dev pypi
.PHONY: release-stage-2
release-stage-2: dev-env pydist twine
# Bump the release candidate version if needed (i.e. the previous release candidate had errors).
.PHONY: recreate-release-candidate
recreate-release-candidate: 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: pydist twine
@echo "Dist built and uploaded to test.pypi.org"
@echo "Test the install:"
@echo " make install-release-test"
@echo "Then upload to Pypi prod:"
@echo " make twine-prod"
.PHONY: release-stage-final
release-stage-final: twine-prod
# Build final dist (gets rid of the rc tag) and release final candidate to TestPyPI
.PHONY: release-final-to-test-pypi
release-final-to-test-pypi: 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"
# DANGER: releases directly to prod
# use this if you accidently burned a test release version number,
# use this if you accidentally burned a test release version number,
.PHONY: release-directly-to-prod
release-directly-to-prod: dev-env pydist twine-prod
release-directly-to-prod: pydist twine-prod
@echo "Dist built and uploaded to pypi.org"
@echo "Test the install:"
@echo " make install-release"
@@ -136,10 +166,15 @@ dev-env-client:
dev-env-server:
pip install -r server/requirements-dev.txt
# give PART=[major, minor, part] as param to make bump
.PHONY: bump
bump:
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
twine:
@@ -165,7 +200,7 @@ install-dev: uninstall
# install from test.pypi to test your release
.PHONY: install-release-test
install-release-test: uninstall
pip install --no-cache-dir --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cellxgene
pip install --no-cache-dir --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple cellxgene==$(VERSION)
@echo "Installed cellxgene from test.pypi.org, now run and smoke test"
# install from pypi to test your release
+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 -
+36 -38
View File
@@ -7,27 +7,27 @@ _an interactive explorer for single-cell transcriptomics data_
[![Compatibility Tests](https://github.com/chanzuckerberg/cellxgene/workflows/Compatibility%20Tests/badge.svg)](https://github.com/chanzuckerberg/cellxgene/actions?query=workflow%3A%22Compatibility+Tests%22)
![Code Coverage](https://codecov.io/gh/chanzuckerberg/cellxgene/branch/main/graph/badge.svg)
cellxgene (pronounced "cell-by-gene") is an interactive data explorer for single-cell transcriptomics datasets, such as those coming from the [Human Cell Atlas](https://humancellatlas.org). Leveraging modern web development techniques to enable fast visualizations of at least 1 million cells, we hope to enable biologists and computational researchers to explore their data.
CZ CELLxGENE Annotate (pronounced "cell-by-gene") is an interactive data explorer for single-cell datasets, such as those coming from the [Human Cell Atlas](https://humancellatlas.org). Leveraging modern web development techniques to enable fast visualizations of at least 1 million cells, we hope to enable biologists and computational researchers to explore their data.
Whether you need to visualize one thousand cells or one million, cellxgene helps you gain insight into your single-cell data.
Whether you need to visualize one thousand cells or one million, CELLxGENE Annotate helps you gain insight into your single-cell data.
<img src="https://github.com/chanzuckerberg/cellxgene/raw/main/docs/images/crossfilter.gif" width="350" height="200" hspace="30"><img src="https://github.com/chanzuckerberg/cellxgene/raw/main/docs/images/category-breakdown.gif" width="350" height="200" hspace="30">
# Getting started
### The comprehensive guide to cellxgene
### The comprehensive guide to CZ CELLxGENE Annotate
[The cellxgene documentation is your one-stop-shop for information about cellxgene](https://chanzuckerberg.github.io/cellxgene/)! You may be particularly interested in:
[The CZ CELLxGENE Annotate documentation is your one-stop-shop for information about CELLxGENE Annotate](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/README.md)! You may be particularly interested in:
- Seeing [what cellxgene can do](https://chanzuckerberg.github.io/cellxgene/posts/gallery)
- Learning more about cellxgene [installation](https://chanzuckerberg.github.io/cellxgene/posts/install) and [usage](https://chanzuckerberg.github.io/cellxgene/posts/launch)
- [Preparing your own data](https://chanzuckerberg.github.io/cellxgene/posts/prepare) for use in cellxgene
- Checking out [our roadmap](https://chanzuckerberg.github.io/cellxgene/posts/roadmap) for future development
- [Contributing](https://chanzuckerberg.github.io/cellxgene/posts/contribute) to cellxgene
- Seeing [what Annotate can do](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/explore-data/explorer-tutorials.md)
- Learning more about Annotate [installation](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/install.md) and [usage](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/quick-start.md#quick-start-1)
- [Preparing your own data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/data-reqs.md) for use in Annotate
- Checking out [our roadmap](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/roadmap.md) for future development
- [Contributing](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md) to Annotate
### Quick start
To install cellxgene you need Python 3.6+. We recommend [installing cellxgene into a conda or virtual environment.](https://chanzuckerberg.github.io/cellxgene/posts/install)
To install CELLxGENE Annotate you need Python 3.10+. We recommend [installing Annotate into a conda or virtual environment.](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/install.md)
Install the package.
@@ -35,69 +35,67 @@ Install the package.
pip install cellxgene
```
Launch cellxgene with an example [anndata](https://anndata.readthedocs.io/en/latest/) file
Launch Annotate with an example [anndata](https://anndata.readthedocs.io/en/latest/) file
```bash
cellxgene launch https://cellxgene-example-data.czi.technology/pbmc3k.h5ad
```
To explore more datasets already formatted for cellxgene, check out the [Demo data](https://chanzuckerberg.github.io/cellxgene/posts/demo-data) or
see [Preparing your data](https://chanzuckerberg.github.io/cellxgene/posts/prepare) to learn more about formatting your own
data for cellxgene.
To explore more datasets already formatted for Annotate, check out the [Demo data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/quick-start.md#example-datasets) or
see [Preparing your data](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/desktop/data-reqs.md) to learn more about formatting your own
data for CELLxGENE Annotate.
### Supported browsers
cellxgene currently supports the following browsers:
CELLxGENE Annotate currently supports the following browsers:
- Google Chrome 61+
- Edge 15+
- Firefox 60+
- Safari 10.1+
Please [file an issue](https://github.com/chanzuckerberg/cellxgene/issues/new/choose) if you would like us to add support for an unsupported browser.
### Finding help
We'd love to hear from you!
For questions, suggestions, or accolades, [join the `#cellxgene-users` channel on the CZI Science Slack](https://join-cellxgene-users.herokuapp.com/) and say "hi!".
For questions, suggestions, or accolades, join the `#cellxgene-users` channel on the [CZI Science Community Slack](https://czi.co/science-slack) and say "hi!".
For any errors, [report bugs on Github](https://github.com/chanzuckerberg/cellxgene/issues).
# Developing with cellxgene
# Developing with CZ CELLxGENE Annotate
### 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://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md) and don't hesitate to open an issue or send a pull request to improve CELLxGENE Annotate. 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.
### 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 Annotate, 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](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/roadmap.md) and under active development.
See the [CELLxGENE extensions](https://github.com/chanzuckerberg/cellxgene-documentation/blob/main/community-extensions.md) section of our documentation for examples of community use and CELLxGENE extensions.
### Trademarks
CZ CELLXGENE, CZ CELLXGENE DISCOVER, and CZ CELLXGENE ANNOTATE are trademarks of the Chan Zuckerberg Initiative. All rights reserved.
Use, reuse, modification, and re-distribution of the source code in this repository is subject to the terms of the applicable open source [license](LICENSE.txt). However, that license does not grant permission to use the trademarks without separate, express permission from the Chan Zuckerberg Initiative.
### Security
If you believe you have found a security issue, we would appreciate notification. Please send email to <security@chanzuckerberg.com>.
# About
# Inspiration
### Core team
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'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.
We were inspired by Mike Bostock and the [crossfilter](https://github.com/crossfilter) team for the design of our filtering implementation.
+2
View File
@@ -0,0 +1,2 @@
## Reporting Security Issues
If you believe you have found a security issue, please responsibly disclose by contacting us at [security@chanzuckerberg.com](mailto:security@chanzuckerberg.com).
-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"
}
}
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd client
npx --no-install lint-staged --config "./configuration/lint-staged/lint-staged.config.js"
+1
View File
@@ -0,0 +1 @@
18.17.0
+10 -3
View File
@@ -1,7 +1,13 @@
include ../common.mk
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../server/test/fixtures/pbmc3k-annotations.csv)
ANNOTATIONS := $(if $(ANNOTATIONS),$(ANNOTATIONS),../test/fixtures/pbmc3k-annotations.csv)
GENE_SETS := $(if $(GENE_SETS),$(GENE_SETS),../test/fixtures/pbmc3k-genesets.csv)
ANNOTATIONS_FILENAME := $(shell basename $(ANNOTATIONS))
GENE_SETS_FILENAME := $(shell basename $(GENE_SETS))
CXG_CONFIG := $(if $(CXG_CONFIG),$(CXG_CONFIG),./__tests__/e2e/test_config.yaml)
# Packaging
.PHONY: clean
@@ -31,7 +37,7 @@ start-frontend:
.PHONY: smoke-test
smoke-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_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" npm run e2e -- --verbose false'
@@ -40,8 +46,9 @@ smoke-test:
smoke-test-annotations:
$(eval TMP_DIR := $(shell mktemp -d /tmp/cellxgene_XXXXXX))
cp $(ANNOTATIONS) $(TMP_DIR)/ && \
cp $(GENE_SETS) $(TMP_DIR)/ && \
start_server_and_test \
'CXG_OPTIONS="--annotations-file $(TMP_DIR)/$(ANNOTATIONS_FILENAME)" $(MAKE) start-server' \
'CXG_OPTIONS="--annotations-file $(TMP_DIR)/$(ANNOTATIONS_FILENAME) --gene-sets-file $(TMP_DIR)/$(GENE_SETS_FILENAME)" $(MAKE) start-server' \
$(CXG_SERVER_PORT) \
'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" npm run e2e-annotations -- --verbose false'
rm -rf $(TMP_DIR)
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`did launch page launched 1`] = `"<span style=\\"width: 185px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"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 aria-haspopup=\\"true\\" class=\\"bp3-popover2-target\\"><span data-testid=\\"louvain:category-label\\" tabindex=\\"-1\\" aria-label=\\"louvain\\" class=\\"\\" 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><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 aria-haspopup=\\"true\\" class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" aria-hidden=\\"true\\" tabindex=\\"0\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><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>"`;
File diff suppressed because one or more lines are too long
+136 -6
View File
@@ -58,11 +58,9 @@ export async function getAllHistograms(testclass, testIds) {
const allHistograms = await getAllByClass(testclass);
const testIDs = await Promise.all(
allHistograms.map((hist) => {
return page.evaluate((elem) => {
return elem.dataset.testid;
}, hist);
})
allHistograms.map((hist) =>
page.evaluate((elem) => elem.dataset.testid, hist)
)
);
return testIDs.map((id) => id.replace(/^histogram-/, ""));
@@ -173,6 +171,137 @@ export async function createCategory(categoryName) {
await clickOn("submit-category");
}
/*
GENESET
*/
export async function colorByGeneset(genesetName) {
await clickOn(`${genesetName}:colorby-entire-geneset`);
}
export async function colorByGene(gene) {
await clickOn(`colorby-${gene}`);
}
export async function assertColorLegendLabel(label) {
const handle = await waitByID("continuous_legend_color_by_label");
const result = await handle.evaluate((node) =>
node.getAttribute("aria-label")
);
return expect(result).toBe(label);
}
export async function expandGeneset(genesetName) {
const expand = await waitByID(`${genesetName}:geneset-expand`);
const notExpanded = await expand.$(
"[data-testclass='geneset-expand-is-not-expanded']"
);
if (notExpanded) await clickOn(`${genesetName}:geneset-expand`);
}
export async function createGeneset(genesetName) {
await clickOnUntil("open-create-geneset-dialog", async () => {
await expect(page).toMatchElement(getTestId("create-geneset-input"));
});
await typeInto("create-geneset-input", genesetName);
await clickOn("submit-geneset");
await waitByClass("autosave-complete");
}
export async function editGenesetName(genesetName, editText) {
const editButton = `${genesetName}:edit-genesetName-mode`;
const submitButton = `${genesetName}:submit-geneset`;
await clickOnUntil(`${genesetName}:see-actions`, async () => {
await expect(page).toMatchElement(getTestId(editButton));
});
await clickOn(editButton);
await typeInto("rename-geneset-modal", editText);
await clickOn(submitButton);
}
export async function deleteGeneset(genesetName) {
const targetId = `${genesetName}:delete-geneset`;
await clickOnUntil(`${genesetName}:see-actions`, async () => {
await expect(page).toMatchElement(getTestId(targetId));
});
await clickOn(targetId);
await assertGenesetDoesNotExist(genesetName);
await waitByClass("autosave-complete");
}
export async function assertGenesetDoesNotExist(genesetName) {
const result = await isElementPresent(
getTestId(`${genesetName}:geneset-name`)
);
await expect(result).toBe(false);
}
export async function assertGenesetExists(genesetName) {
const handle = await waitByID(`${genesetName}:geneset-name`);
const result = await handle.evaluate((node) =>
node.getAttribute("aria-label")
);
return expect(result).toBe(genesetName);
}
/*
GENE
*/
export async function addGeneToSet(genesetName, geneToAddToSet) {
const submitButton = `${genesetName}:submit-gene`;
await clickOn(`${genesetName}:add-new-gene-to-geneset`);
await typeInto("add-genes", geneToAddToSet);
await clickOn(submitButton);
}
export async function removeGene(geneSymbol) {
const targetId = `delete-from-geneset:${geneSymbol}`;
await clickOn(targetId);
await waitByClass("autosave-complete");
}
export async function assertGeneExistsInGeneset(geneSymbol) {
const handle = await waitByID(`${geneSymbol}:gene-label`);
const result = await handle.evaluate((node) =>
node.getAttribute("aria-label")
);
return expect(result).toBe(geneSymbol);
}
export async function assertGeneDoesNotExist(geneSymbol) {
const result = await isElementPresent(getTestId(`${geneSymbol}:gene-label`));
await expect(result).toBe(false);
}
export async function expandGene(geneSymbol) {
await clickOn(`maximize-${geneSymbol}`);
}
/*
CATEGORY
*/
export async function duplicateCategory(categoryName) {
await clickOn("open-annotation-dialog");
@@ -235,7 +364,7 @@ export async function createLabel(categoryName, labelName) {
* 4. You will see `123` is persisted in the input box
* 5. Expected behavior is to get an empty input box
*/
await page.waitFor(500);
await page.waitForTimeout(500);
await clickOn(`${categoryName}:see-actions`);
@@ -312,4 +441,5 @@ export async function assertCategoryDoesNotExist(categoryName) {
await expect(result).toBe(false);
}
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
+2
View File
@@ -6,3 +6,5 @@ export const appUrlBase =
export const DATASET = "pbmc3k";
export const isDev = jestEnv === ENV_DEFAULT.DEV;
export const isDebug = jestEnv === ENV_DEFAULT.DEBUG;
export const TEST_EMAIL = "user@example.com";
export const TEST_PASSWORD = process.env.TEST_ACCOUNT_PASS ?? "";
+105
View File
@@ -0,0 +1,105 @@
export const diffexpPop1Genes = [
"CD79A",
"HLA-DRB1",
"HLA-DQA1",
"HLA-DPB1",
"HLA-DQB1",
"HLA-DPA1",
"MS4A1",
"LTB",
"CD79B",
"CD37",
"HLA-DMA",
"TCL1A",
"LINC00926",
"HLA-DMB",
"HVCN1",
"EAF2",
"FCRLA",
"IRF8",
"PKIG",
"P2RX5",
"HLA-DOB",
"SPIB",
"BLNK",
"SWAP70",
"PNOC",
"CD19",
"SMIM14",
"CD72",
"KIAA0125",
"IGLL5",
"ARHGAP24",
"COTL1",
"C16orf74",
"BTK",
"SNX29P2",
"ADAM28",
"FCGR2B",
"PLD4",
"PPP1R14A",
"MZB1",
"KIAA0040",
"PHACTR1",
"FCRL2",
"RIC3",
"P2RY10",
"SCPEP1",
"DRAM2",
"RP5-887A10.1",
"CD82",
"GPX1",
];
export const diffexpPop2Genes = [
"NKG7",
"GZMB",
"CTSW",
"PRF1",
"GNLY",
"GZMA",
"CST7",
"FGFBP2",
"SRGN",
"CD247",
"FCGR3A",
"TYROBP",
"FCER1G",
"ID2",
"SPON2",
"CCL4",
"CCL5",
"GZMH",
"GIMAP7",
"CLIC3",
"HOPX",
"XCL2",
"LGALS1",
"IGFBP7",
"AKR1C3",
"IL32",
"EFHD2",
"PRSS23",
"TTC38",
"ZAP70",
"S1PR5",
"SAMD3",
"GIMAP4",
"CCL3",
"ABI3",
"XCL1",
"S100A6",
"UBB",
"GPR56",
"PDIA3",
"S100A11",
"APOBEC3G",
"HAVCR2",
"PLEKHF1",
"LITAF",
"ARPC5L",
"PTGDR",
"PRMT2",
"GSTP1",
"FCRL6",
];
-170
View File
@@ -15,22 +15,16 @@ import {
getOneElementInnerHTML,
getTestId,
goToPage,
typeInto,
waitByID,
} from "./puppeteerUtils";
import {
addGeneToSearch,
bulkAddGenes,
calcDragCoordinates,
clip,
drag,
getAllCategoriesAndCounts,
getAllHistograms,
getCellSetCount,
runDiffExp,
selectCategory,
subset,
} from "./cellxgeneActions";
const data = datasets[DATASET];
@@ -146,47 +140,6 @@ describe("cell selection", () => {
});
});
describe("gene entry", () => {
test("search for single gene", async () => {
await goToPage(appUrlBase);
await addGeneToSearch(data.genes.search);
});
test("bulk add genes", async () => {
await goToPage(appUrlBase);
const testGenes = data.genes.bulkadd;
await bulkAddGenes(testGenes);
const allHistograms = await getAllHistograms(
"histogram-user-gene",
testGenes
);
expect(allHistograms).toEqual(expect.arrayContaining(testGenes));
expect(allHistograms).toHaveLength(testGenes.length);
});
});
describe("differential expression", () => {
test("selects cells, saves them and performs diffexp", async () => {
await goToPage(appUrlBase);
await runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
const allHistograms = await getAllHistograms(
"histogram-diffexp",
data.diffexp["gene-results"]
);
expect(allHistograms).toEqual(
expect.arrayContaining(data.diffexp["gene-results"])
);
expect(allHistograms).toHaveLength(data.diffexp["gene-results"].length);
});
});
describe("subset", () => {
test("subset - cell count matches", async () => {
await goToPage(appUrlBase);
@@ -233,94 +186,6 @@ describe("subset", () => {
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(data.subset.lasso.count);
});
test("undo selection appends the top diff exp genes to user defined genes", async () => {
await goToPage(appUrlBase);
const userDefinedGenes = data.genes.bulkadd;
const diffExpGenes = data.diffexp["gene-results"];
await bulkAddGenes(userDefinedGenes);
const userDefinedHistograms = await getAllHistograms(
"histogram-user-gene",
userDefinedGenes
);
expect(userDefinedHistograms).toEqual(
expect.arrayContaining(userDefinedGenes)
);
await subset({ x1: 0.15, y1: 0.1, x2: 0.98, y2: 0.98 });
await runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
const diffExpHistograms = await getAllHistograms(
"histogram-diffexp",
diffExpGenes
);
expect(diffExpHistograms).toEqual(expect.arrayContaining(diffExpGenes));
await clickOn("reset-subset-button");
const expected = [].concat(userDefinedGenes, diffExpGenes);
const userDefinedHistogramsAfterSubset = await getAllHistograms(
"histogram-user-gene",
expected
);
expect(userDefinedHistogramsAfterSubset).toEqual(
expect.arrayContaining(expected)
);
});
test("subset selection appends the top diff exp genes to user defined genes", async () => {
await goToPage(appUrlBase);
const userDefinedGenes = data.genes.bulkadd;
const diffExpGenes = data.diffexp["gene-results"];
await bulkAddGenes(userDefinedGenes);
const userDefinedHistograms = await getAllHistograms(
"histogram-user-gene",
userDefinedGenes
);
expect(userDefinedHistograms).toEqual(
expect.arrayContaining(userDefinedGenes)
);
await subset({ x1: 0.15, y1: 0.1, x2: 0.98, y2: 0.98 });
await runDiffExp(data.diffexp.cellset1, data.diffexp.cellset2);
const diffExpHistograms = await getAllHistograms(
"histogram-diffexp",
diffExpGenes
);
expect(diffExpHistograms).toEqual(expect.arrayContaining(diffExpGenes));
await subset({ x1: 0.16, y1: 0.11, x2: 0.97, y2: 0.97 });
const expected = [].concat(userDefinedGenes, diffExpGenes);
const userDefinedHistogramsAfterSubset = await getAllHistograms(
"histogram-user-gene",
expected
);
expect(userDefinedHistogramsAfterSubset).toEqual(
expect.arrayContaining(expected)
);
});
});
describe("scatter plot", () => {
test("scatter plot appears", async () => {
await goToPage(appUrlBase);
await bulkAddGenes(Object.values(data.scatter.genes));
await clickOn(`plot-x-${data.scatter.genes.x}`);
await clickOn(`plot-y-${data.scatter.genes.y}`);
await waitByID("scatterplot");
});
});
describe("clipping", () => {
@@ -337,30 +202,6 @@ describe("clipping", () => {
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(data.clip.count);
});
test("clip gene", async () => {
await goToPage(appUrlBase);
await typeInto("gene-search", data.clip.gene);
await page.keyboard.press("Enter");
await page.waitForSelector(`[data-testid='histogram-${data.clip.gene}']`);
await clip(data.clip.min, data.clip.max);
const histBrushableAreaId = `histogram-${data.clip.gene}-plot-brushable-area`;
const coords = await calcDragCoordinates(
histBrushableAreaId,
data.clip["coordinates-as-percent"]
);
await drag(histBrushableAreaId, coords.start, coords.end);
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(data.clip["gene-cell-count"]);
});
});
// interact with UI elements just that they do not break
@@ -378,17 +219,6 @@ describe("ui elements don't error", () => {
}
});
test("color by for gene", async () => {
await goToPage(appUrlBase);
await typeInto("gene-search", data.genes.search);
await page.keyboard.press("Enter");
await page.waitForSelector(
`[data-testid='histogram-${data.genes.search}']`
);
await clickOn(`colorby-${data.genes.search}`);
});
test("pan and zoom", async () => {
await goToPage(appUrlBase);
+248 -8
View File
@@ -12,6 +12,8 @@ import {
getTestId,
getTestClass,
getAllByClass,
clickOnUntil,
getOneElementInnerHTML,
} from "./puppeteerUtils";
import {
@@ -27,6 +29,21 @@ import {
renameLabel,
subset,
duplicateCategory,
createGeneset,
deleteGeneset,
assertGenesetExists,
assertGenesetDoesNotExist,
getCellSetCount,
expandGeneset,
editGenesetName,
addGeneToSet,
assertGeneExistsInGeneset,
removeGene,
assertGeneDoesNotExist,
expandGene,
colorByGeneset,
assertColorLegendLabel,
colorByGene,
} from "./cellxgeneActions";
const data = datasets[DATASET];
@@ -34,12 +51,45 @@ const data = datasets[DATASET];
const perTestCategoryName = "TEST-CATEGORY";
const perTestLabelName = "TEST-LABEL";
// geneset CRUD
const genesetToDeleteName = "geneset_to_delete";
const preExistingGenesetName = "fifth_dataset";
const meanExpressionBrushGenesetName = "second_gene_set";
const meanExpressionBrushCellsSelected = "557";
const subsetMeanExpressionBrushCellsSelected = "452";
// initial text, the text we type in, the result
const editableGenesetName = "geneset_to_edit";
const editText = "_111";
const newGenesetName = "geneset_to_edit_111";
// add gene to set
const geneToAddToSet = "RER1";
const setToAddGeneTo = "fill_this_geneset";
// remove gene from set
const geneToRemove = "SIK1";
const setToRemoveFrom = "empty_this_geneset";
// brush a gene
const geneToBrushAndColorBy = "SIK1";
const brushThisGeneGeneset = "brush_this_gene";
const geneBrushedCellCount = "109";
const subsetGeneBrushedCellCount = "96";
const genesetDescriptionID =
"geneset-description-tooltip-fourth_gene_set: fourth description";
const genesetDescriptionString = "fourth_gene_set: fourth description";
const genesetToCheckForDescription = "fourth_gene_set";
async function setup(config) {
await goToPage(appUrlBase);
// setup the test fixtures
await createCategory(perTestCategoryName);
await createLabel(perTestCategoryName, perTestLabelName);
if (config.categoricalAnno) {
// setup the test fixtures
await createCategory(perTestCategoryName);
await createLabel(perTestCategoryName, perTestLabelName);
}
if (config.withSubset) {
await subset({ x1: 0.1, y1: 0.1, x2: 0.8, y2: 0.8 });
@@ -51,6 +101,200 @@ async function setup(config) {
describe.each([
{ withSubset: true, tag: "subset" },
{ withSubset: false, tag: "whole" },
])("geneSET crud operations and interactions", (config) => {
test("genesets load from csv", async () => {
await setup(config);
await assertGenesetExists(preExistingGenesetName);
});
test("brush on geneset mean", async () => {
await setup(config);
await expandGeneset(meanExpressionBrushGenesetName);
const histBrushableAreaId = `histogram-${meanExpressionBrushGenesetName}-plot-brushable-area`;
const coords = await calcDragCoordinates(histBrushableAreaId, {
x1: 0.25,
y1: 0.5,
x2: 0.55,
y2: 0.5,
});
await drag(histBrushableAreaId, coords.start, coords.end);
const cellCount = await getCellSetCount(1);
if (config.withSubset) {
expect(cellCount).toBe(subsetMeanExpressionBrushCellsSelected);
} else {
expect(cellCount).toBe(meanExpressionBrushCellsSelected);
}
});
test("color by mean expression", async () => {
await setup(config);
await colorByGeneset(meanExpressionBrushGenesetName);
await assertColorLegendLabel(meanExpressionBrushGenesetName);
});
test("diffexp", async () => {
if (config.withSubset) return;
await setup(config);
// set the two cell sets to b cells vs nk cells
await expandCategory(`louvain`);
await clickOn(`louvain:category-select`);
await clickOn(`categorical-value-select-louvain-B cells`);
await clickOn(`cellset-button-1`);
await clickOn(`categorical-value-select-louvain-B cells`);
await clickOn(`categorical-value-select-louvain-NK cells`);
await clickOn(`cellset-button-2`);
// run diffexp
await clickOn(`diffexp-button`);
await waitByClass("pop-1-geneset-expand");
await expect(page).toClick(getTestClass("pop-1-geneset-expand"));
await page.waitForFunction(
(selector) => !document.querySelector(selector),
{},
getTestClass("gene-loading-spinner")
);
let genesHTML = await getOneElementInnerHTML(
getTestClass("gene-set-genes")
);
expect(genesHTML).toMatchSnapshot();
await expect(page).toClick(getTestClass("pop-1-geneset-expand"));
await expect(page).toClick(getTestClass("pop-2-geneset-expand"));
await page.waitForFunction(
(selector) => !document.querySelector(selector),
{},
getTestClass("gene-loading-spinner")
);
genesHTML = await getOneElementInnerHTML(getTestClass("gene-set-genes"));
expect(genesHTML).toMatchSnapshot();
});
test("create a new geneset and undo/redo", async () => {
if (config.withSubset) return;
await setup(config);
const genesetName = `test-geneset-foo-123`;
await assertGenesetDoesNotExist(genesetName);
await createGeneset(genesetName);
/* note: as of June 2021, the aria label is in the truncate component which clones the element */
await assertGenesetExists(genesetName);
await clickOn("undo");
await assertGenesetDoesNotExist(genesetName);
await clickOn("redo");
await assertGenesetExists(genesetName);
});
test("edit geneset name and undo/redo", async () => {
await setup(config);
await editGenesetName(editableGenesetName, editText);
await assertGenesetExists(newGenesetName);
await clickOn("undo");
await assertGenesetExists(editableGenesetName);
await clickOn("redo");
await assertGenesetExists(newGenesetName);
});
test("delete a geneset and undo/redo", async () => {
if (config.withSubset) return;
await setup(config);
await deleteGeneset(genesetToDeleteName);
await clickOn("undo");
await assertGenesetExists(genesetToDeleteName);
await clickOn("redo");
await assertGenesetDoesNotExist(genesetToDeleteName);
});
test("geneset description", async () => {
if (config.withSubset) return;
await setup(config);
await clickOnUntil(
`${genesetToCheckForDescription}:geneset-expand`,
async () => {
expect(page).toMatchElement(getTestId(genesetDescriptionID), {
text: genesetDescriptionString,
});
}
);
});
});
describe.each([
{ withSubset: true, tag: "subset" },
{ withSubset: false, tag: "whole" },
])("GENE crud operations and interactions", (config) => {
test("add a gene to geneset and undo/redo", async () => {
await setup(config);
await addGeneToSet(setToAddGeneTo, geneToAddToSet);
await expandGeneset(setToAddGeneTo);
await assertGeneExistsInGeneset(geneToAddToSet);
await clickOn("undo");
await assertGeneDoesNotExist(geneToAddToSet);
await clickOn("redo");
await assertGeneExistsInGeneset(geneToAddToSet);
});
test("expand gene and brush", async () => {
await setup(config);
await expandGeneset(brushThisGeneGeneset);
await expandGene(geneToBrushAndColorBy);
const histBrushableAreaId = `histogram-${geneToBrushAndColorBy}-plot-brushable-area`;
const coords = await calcDragCoordinates(histBrushableAreaId, {
x1: 0.25,
y1: 0.5,
x2: 0.55,
y2: 0.5,
});
await drag(histBrushableAreaId, coords.start, coords.end);
const cellCount = await getCellSetCount(1);
if (config.withSubset) {
expect(cellCount).toBe(subsetGeneBrushedCellCount);
} else {
expect(cellCount).toBe(geneBrushedCellCount);
}
});
test("color by gene in geneset", async () => {
await setup(config);
await expandGeneset(meanExpressionBrushGenesetName);
await colorByGene(geneToBrushAndColorBy);
await assertColorLegendLabel(geneToBrushAndColorBy);
});
test("delete gene from geneset and undo/redo", async () => {
// We've already deleted the gene
if (config.withSubset) return;
await setup(config);
await expandGeneset(setToRemoveFrom);
await removeGene(geneToRemove);
await assertGeneDoesNotExist(geneToRemove);
await clickOn("undo");
await assertGeneExistsInGeneset(geneToRemove);
await clickOn("redo");
await assertGeneDoesNotExist(geneToRemove);
});
});
describe.each([
{ withSubset: true, tag: "subset", categoricalAnno: true },
{ withSubset: false, tag: "whole", categoricalAnno: true },
])("annotations", (config) => {
test("create a category", async () => {
await setup(config);
@@ -287,11 +531,7 @@ describe.each([
const labels = await getAllByClass("categorical-row");
const result = await Promise.all(
labels.map((label) => {
return page.evaluate((element) => {
return element.outerHTML;
}, label);
})
labels.map((label) => page.evaluate((element) => element.outerHTML, label))
);
expect(result).toMatchSnapshot();
+1 -1
View File
@@ -4,7 +4,7 @@
"testMatch": ["**/__tests__/**/?(*.)(spec|test).js?(x)"],
"setupFiles": ["../setupMissingGlobals.js"],
"setupFilesAfterEnv": ["expect-puppeteer", "./puppeteer.setup.js"],
"globalSetup": "jest-environment-puppeteer/setup",
"globalSetup": "../globalSetup.js",
"globalTeardown": "jest-environment-puppeteer/teardown",
"testEnvironment": "./screenshot_env.js"
}
+4 -4
View File
@@ -13,11 +13,13 @@ import * as ENV_DEFAULT from "../../../environment.default.json";
// a test can take more time to finish, so we don't want
// jest to shut off the test too soon
jest.setTimeout(2 * 60 * 1000);
setDefaultOptions({ timeout: 20 * 1000 });
setDefaultOptions({ timeout: 60 * 1000 });
jest.retryTimes(ENV_DEFAULT.RETRY_ATTEMPTS);
(async () => {
beforeEach(async () => {
await jestPuppeteer.resetBrowser();
const userAgent = await browser.userAgent();
await page.setUserAgent(`${userAgent}bot`);
@@ -53,6 +55,4 @@ jest.retryTimes(ENV_DEFAULT.RETRY_ATTEMPTS);
}
}
});
})().catch((error) => {
console.error("puppeteer.setup.js error", error);
});
+4 -11
View File
@@ -12,7 +12,7 @@ export async function waitByID(testId, props = {}) {
}
export async function waitByClass(testClass, props = {}) {
await page.waitForSelector(`[data-testclass='${testClass}']`, props);
return page.waitForSelector(`[data-testclass='${testClass}']`, props);
}
export async function waitForAllByIds(testIds) {
@@ -32,7 +32,7 @@ export async function typeInto(testId, text) {
const selector = getTestId(testId);
// type ahead can be annoying if you don't pause before you type
await page.click(selector);
await page.waitFor(200);
await page.waitForTimeout(200);
await page.type(selector, text);
}
@@ -42,7 +42,7 @@ export async function clearInputAndTypeInto(testId, text) {
// only works for text without special characters
// type ahead can be annoying if you don't pause before you type
await page.click(selector);
await page.waitFor(200);
await page.waitForTimeout(200);
// select all
await page.click(selector, { clickCount: 3 });
await page.keyboard.press("Backspace");
@@ -72,7 +72,7 @@ export async function clickOnUntil(testId, assert) {
} catch (error) {
retry += 1;
await page.waitFor(WAIT_FOR_MS);
await page.waitForTimeout(WAIT_FOR_MS);
}
}
@@ -100,12 +100,6 @@ export async function getElementCoordinates(testId) {
});
}
async function clickTermsOfService() {
if (!(await isElementPresent(getTestId("tos-cookies-accept")))) return;
await clickOn("tos-cookies-accept");
}
async function nameNewAnnotation() {
if (await isElementPresent(getTestId("annotation-dialog"))) {
await typeInto("new-annotation-name", "ignoreE2E");
@@ -122,7 +116,6 @@ export async function goToPage(url) {
});
await nameNewAnnotation();
await clickTermsOfService();
}
export async function isElementPresent(selector, options) {
+19
View File
@@ -0,0 +1,19 @@
server:
app:
force_https: true
port: 5005
dataset:
presentation:
max_categories: 1000
custom_colors: true
user_annotations:
enable: false
type: local_file_csv
local_file_csv:
directory: null
file: null
embeddings:
names: []
+5
View File
@@ -0,0 +1,5 @@
const { setup } = require("jest-environment-puppeteer");
module.exports = async () => {
await setup();
};
+524
View File
@@ -0,0 +1,524 @@
import genesetsReducer from "../../src/reducers/genesets";
describe("initial reducer state", () => {
test("some other action", () => {
expect(genesetsReducer(undefined, { type: "foo" })).toMatchObject({
initialized: false,
lastTid: undefined,
genesets: new Map(),
});
});
});
describe("geneset: initial load", () => {
test("missing JSON response", () => {
expect(() =>
genesetsReducer(undefined, {
type: "geneset: initial load",
})
).toThrow("missing or malformed JSON response");
});
test("empty geneset", () => {
expect(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
})
).toMatchObject({
initialized: true,
lastTid: 0,
genesets: new Map(),
});
});
test("non-empty geneset", () => {
expect(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 99,
genesets: [
{
geneset_name: "G1",
genes: [{ gene_symbol: "F5" }],
},
{
geneset_name: "G2",
geneset_description: "G2 desc",
genes: [{ gene_symbol: "F6" }],
},
{
geneset_name: "G3",
geneset_description: "G3 desc",
genes: [{ gene_symbol: "F7", gene_description: "gene desc" }],
},
],
},
})
).toMatchObject({
initialized: true,
lastTid: 99,
genesets: new Map([
[
"G1",
{
genesetName: "G1",
genesetDescription: "",
genes: new Map([["F5", { geneSymbol: "F5", geneDescription: "" }]]),
},
],
[
"G2",
{
genesetName: "G2",
genesetDescription: "G2 desc",
genes: new Map([["F6", { geneSymbol: "F6", geneDescription: "" }]]),
},
],
[
"G3",
{
genesetName: "G3",
genesetDescription: "G3 desc",
genes: new Map([
["F7", { geneSymbol: "F7", geneDescription: "gene desc" }],
]),
},
],
]),
});
});
});
describe("geneset: create", () => {
const initialState = genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
});
test("simple create", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "a geneset",
genesetDescription: "",
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"a geneset",
{
genesetName: "a geneset",
genesetDescription: "",
genes: new Map(),
},
],
]),
});
});
test("error - duplicate name", () => {
expect(() => {
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
genesetDescription: "foo",
}),
{
type: "geneset: create",
genesetName: "foo",
genesetDescription: "bar",
}
);
}).toThrow("name already defined");
});
test("error - missing required action values", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: create",
genesetDescription: "foo",
});
}).toThrow();
expect(() => {
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
});
}).toThrow("name or description unspecified");
});
});
describe("geneset: delete", () => {
const initialState = genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
});
test("simple delete", () => {
expect(
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
genesetDescription: "foo",
}),
{
type: "geneset: delete",
genesetName: "foo",
}
)
).toMatchObject({
initialized: true,
lastTid: 0,
genesets: new Map(),
});
});
test("error - missing name", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: delete",
genesetName: "foo",
});
}).toThrow("name does not exist");
});
});
describe("geneset: update", () => {
const initialState = genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
});
test("simple update", () => {
expect(
genesetsReducer(
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo1",
genesetDescription: "foo1",
}),
{
type: "geneset: create",
genesetName: "foo2",
genesetDescription: "foo2",
}
),
{
type: "geneset: update",
genesetName: "foo1",
update: {
genesetName: "bar",
genesetDescription: "bar",
},
}
)
).toMatchObject({
initialized: true,
lastTid: 0,
genesets: new Map([
[
"bar",
{ genesetName: "bar", genesetDescription: "bar", genes: new Map() },
],
[
"foo2",
{ genesetName: "foo2", genesetDescription: "foo2", genes: new Map() },
],
]),
});
});
test("error - unknown name", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: update",
genesetName: "foo",
update: {
genesetName: "foo",
genesetDescription: "bar",
},
});
}).toThrow("name unspecified or does not exist");
});
test("error - duplicate name", () => {
expect(() => {
genesetsReducer(
genesetsReducer(initialState, {
type: "geneset: create",
genesetName: "foo",
genesetDescription: "foo",
}),
{
type: "geneset: update",
genesetName: "foo",
update: {
genesetName: "foo",
genesetDescription: "foo",
},
}
);
}).toThrow("update specified existing name and description");
});
});
describe("geneset: add genes", () => {
const initialState = genesetsReducer(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
}),
{
type: "geneset: create",
genesetName: "test",
genesetDescription: "",
}
);
test("add a gene", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: add genes",
genesetName: "test",
genes: [{ geneSymbol: "F5" }],
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map([["F5", { geneSymbol: "F5", geneDescription: "" }]]),
},
],
]),
});
expect(
genesetsReducer(initialState, {
type: "geneset: add genes",
genesetName: "test",
genes: [
{ geneSymbol: "F5", geneDescription: "desc" },
{ geneSymbol: "SET1", geneDescription: "" },
],
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map([
["F5", { geneSymbol: "F5", geneDescription: "desc" }],
["SET1", { geneSymbol: "SET1", geneDescription: "" }],
]),
},
],
]),
});
});
test("no such geneset error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: add genes",
genesetName: "mumble",
genes: [],
});
}).toThrow("geneset name does not exist");
});
});
describe("geneset: delete genes", () => {
const initialState = genesetsReducer(
genesetsReducer(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
}),
{
type: "geneset: create",
genesetName: "test",
genesetDescription: "",
}
),
{
type: "geneset: add genes",
genesetName: "test",
genes: [{ geneSymbol: "F5" }],
}
);
test("simple", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: delete genes",
genesetName: "test",
geneSymbols: ["F5"],
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map(),
},
],
]),
});
});
test("no such geneset error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: delete genes",
genesetName: "mumble",
geneSymbols: [],
});
}).toThrow("name does not exist");
});
});
describe("geneset: set gene description", () => {
const initialState = genesetsReducer(
genesetsReducer(
genesetsReducer(undefined, {
type: "geneset: initial load",
data: {
tid: 0,
genesets: [],
},
}),
{
type: "geneset: create",
genesetName: "test",
genesetDescription: "",
}
),
{
type: "geneset: add genes",
genesetName: "test",
genes: [{ geneSymbol: "F5" }],
}
);
test("simple set", () => {
expect(
genesetsReducer(initialState, {
type: "geneset: set gene description",
genesetName: "test",
update: {
geneSymbol: "F5",
geneDescription: "mumble",
},
})
).toMatchObject({
...initialState,
genesets: new Map([
[
"test",
{
genesetName: "test",
genesetDescription: "",
genes: new Map([
["F5", { geneSymbol: "F5", geneDescription: "mumble" }],
]),
},
],
]),
});
});
test("no such geneset error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: set gene description",
genesetName: "does not exist",
update: {
geneSymbol: "F5",
geneDescription: "mumble",
},
});
}).toThrow("geneset name does not exist");
});
test("no such gene error", () => {
expect(() => {
genesetsReducer(initialState, {
type: "geneset: set gene description",
genesetName: "test",
update: {
geneSymbol: "NO SUCH GENE",
geneDescription: "mumble",
},
});
}).toThrow("no such gene");
});
});
describe("geneset: set tid", () => {
test("simple set", () => {
expect(
genesetsReducer(undefined, {
type: "geneset: set tid",
tid: 1,
})
).toMatchObject({ lastTid: 1 });
});
test("not a number error", () => {
expect(() => {
genesetsReducer(
{ lastTid: 1 },
{
type: "geneset: set tid",
tid: "0",
}
);
}).toThrow("must be a positive integer");
});
test("decrement error", () => {
expect(() => {
genesetsReducer(
{ lastTid: 1 },
{
type: "geneset: set tid",
tid: 0,
}
);
}).toThrow("may not be decremented");
});
});
@@ -0,0 +1,75 @@
import genesetsUIReducer from "../../src/reducers/genesetsUI";
// Format: GeneSetsUI(state,action)
const initialState = {
createGenesetModeActive: false,
isEditingGenesetName: false,
isAddingGenesToGeneset: false,
};
/* initial */
describe("geneset UI states", () => {
test("initial state, some other action", () => {
expect(
genesetsUIReducer(undefined, {
type: "foo",
})
).toMatchObject(initialState);
});
test("geneset: activate add new geneset mode", () => {
expect(
genesetsUIReducer(initialState, {
type: "geneset: activate add new geneset mode",
})
).toMatchObject({
createGenesetModeActive: true,
isEditingGenesetName: false,
isAddingGenesToGeneset: false,
});
});
test("geneset: disable create geneset mode", () => {
expect(
genesetsUIReducer(undefined, { isEditingGenesetName: false })
).toMatchObject(initialState);
});
test("activate add new genes mode", () => {
expect(
genesetsUIReducer(undefined, {
type: "geneset: activate add new genes mode",
geneset: "a geneset name",
})
).toMatchObject({
createGenesetModeActive: false,
isEditingGenesetName: false,
isAddingGenesToGeneset: "a geneset name",
});
});
test("disable create geneset mode", () => {
expect(
genesetsUIReducer(undefined, {
type: "geneset: disable create geneset mode",
})
).toMatchObject(initialState);
});
test("activate rename geneset mode", () => {
expect(
genesetsUIReducer(undefined, {
type: "geneset: activate rename geneset mode",
data: "a geneset name",
})
).toMatchObject({
createGenesetModeActive: false,
isEditingGenesetName: "a geneset name",
isAddingGenesToGeneset: false,
});
});
test("disable rename geneset mode", () => {
expect(
genesetsUIReducer(undefined, {
type: "geneset: disable rename geneset mode",
})
).toMatchObject(initialState);
});
});
+2 -6
View File
@@ -23,9 +23,7 @@ describe("create", () => {
describe("undo", () => {
test("expected state modifications", () => {
const initialState = { a: 0, b: 1000 };
const reducer = (state) => {
return { a: state.a + 1, b: state.b + 1 };
};
const reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
const undoableReducer = undoable(reducer, ["a"]);
const s1 = undoableReducer(initialState, { type: "test" });
@@ -43,9 +41,7 @@ describe("undo", () => {
describe("redo", () => {
const initialState = { a: 0, b: 1000 };
const reducer = (state) => {
return { a: state.a + 1, b: state.b + 1 };
};
const reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
let UR;
beforeEach(() => {
+5 -9
View File
@@ -11,18 +11,14 @@ describe("rangeEncodeIndices", () => {
test("sorted flag", () => {
expect(rangeEncodeIndices([1, 9, 432], 10, true)).toMatchObject([
1,
9,
432,
1, 9, 432,
]);
expect(rangeEncodeIndices([1, 9, 432], 10, false)).toMatchObject([
1,
9,
432,
1, 9, 432,
]);
expect(
rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, true)
).toMatchObject([[0, 3], [9, 10], 432]);
expect(rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, true)).toMatchObject(
[[0, 3], [9, 10], 432]
);
expect(
rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, false)
).toMatchObject([[0, 3], [9, 10], 432]);
@@ -85,9 +85,11 @@ describe("AnnoMatrix", () => {
fetch.once(serverMocks.responder);
await expect(
annoMatrix.fetch("X", {
field: "var",
column: annoMatrix.schema.annotations.var.index,
value: "TYMP",
where: {
field: "var",
column: annoMatrix.schema.annotations.var.index,
value: "TYMP",
},
})
).resolves.toBeInstanceOf(Dataframe);
@@ -103,14 +105,18 @@ describe("AnnoMatrix", () => {
await expect(
annoMatrix.fetch("X", [
{
field: "var",
column: varIndex,
value: "SUMO3",
where: {
field: "var",
column: varIndex,
value: "SUMO3",
},
},
{
field: "var",
column: varIndex,
value: "TYMP",
where: {
field: "var",
column: varIndex,
value: "TYMP",
},
},
])
).resolves.toBeInstanceOf(Dataframe);
@@ -170,9 +170,11 @@ describe("AnnoMatrixCrossfilter", () => {
const xfltr = await crossfilter.select(
"X",
{
field: "var",
column: varIndex,
value: "TYMP",
where: {
field: "var",
column: varIndex,
value: "TYMP",
},
},
{
mode: "range",
@@ -186,9 +188,11 @@ describe("AnnoMatrixCrossfilter", () => {
expect(xfltr.countSelected()).toEqual(501);
const df = await annoMatrix.fetch("X", {
field: "var",
column: varIndex,
value: "TYMP",
where: {
field: "var",
column: varIndex,
value: "TYMP",
},
});
const values = df.icol(0).asArray();
const selected = xfltr.allSelectedMask();
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,4 @@
export const baseDataURL = "https://a.fake.url/api/v0.2";
window.CELLXGENE = {
API: {
prefix: baseDataURL,
version: "v0.2/",
},
};
export { schema } from "./schema";
export * from "./routes";
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,4 @@
import sha1 from "sha1";
import {
_whereCacheGet,
_whereCacheCreate,
@@ -7,37 +8,82 @@ import {
const schema = {};
describe("whereCache", () => {
test("whereCacheGet - missing cache values", () => {
test("whereCacheGet - where query, missing cache values", () => {
expect(
_whereCacheGet({}, schema, "X", {
field: "var",
column: "foo",
value: "bar",
where: {
field: "var",
column: "foo",
value: "bar",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet({ X: {} }, schema, "X", {
field: "var",
column: "foo",
value: "bar",
_whereCacheGet({}, schema, "X", {
summarize: {
field: "var",
column: "foo",
values: ["bar"],
},
})
).toEqual([undefined]);
expect(
_whereCacheGet({ X: { var: new Map() } }, schema, "X", {
field: "var",
column: "foo",
value: "bar",
_whereCacheGet({ where: { X: {} } }, schema, "X", {
where: {
field: "var",
column: "foo",
value: "bar",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet({ where: { X: { var: new Map() } } }, schema, "X", {
where: {
field: "var",
column: "foo",
value: "bar",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet(
{ X: { var: new Map([["foo", new Map()]]) } },
{ where: { X: { var: new Map([["foo", new Map()]]) } } },
schema,
"X",
{
where: {
field: "var",
column: "foo",
value: "bar",
},
}
)
).toEqual([undefined]);
});
test("whereCacheGet - summarize query, missing cache values", () => {
expect(
_whereCacheGet({}, schema, "X", {
summarize: {
method: "mean",
field: "var",
column: "foo",
value: "bar",
values: ["bar"],
},
})
).toEqual([undefined]);
expect(
_whereCacheGet(
{ summarize: { X: { mean: { var: new Map() } } } },
schema,
"X",
{
summarize: {
method: "mean",
field: "var",
column: "foo",
values: ["bar"],
},
}
)
).toEqual([undefined]);
@@ -45,140 +91,292 @@ describe("whereCache", () => {
test("whereCacheGet - varied lookups", () => {
const whereCache = {
X: {
var: new Map([
[
"foo",
new Map([
["bar", [0]],
["baz", [1, 2]],
where: {
X: {
var: new Map([
[
"foo",
new Map([
["bar", [0]],
["baz", [1, 2]],
]),
],
]),
},
},
summarize: {
X: {
mean: {
var: new Map([
[
"foo",
new Map([
[sha1("bar"), [0]],
[sha1("baz"), [1, 2]],
]),
],
]),
],
]),
},
},
},
};
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "foo",
value: "bar",
where: {
field: "var",
column: "foo",
value: "bar",
},
})
).toEqual([0]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "foo",
value: "baz",
summarize: {
method: "mean",
field: "var",
column: "foo",
values: ["bar"],
},
})
).toEqual([0]);
expect(
_whereCacheGet(whereCache, schema, "X", {
where: {
field: "var",
column: "foo",
value: "baz",
},
})
).toEqual([1, 2]);
expect(
_whereCacheGet(whereCache, schema, "X", {
summarize: {
method: "mean",
field: "var",
column: "foo",
values: ["baz"],
},
})
).toEqual([1, 2]);
expect(_whereCacheGet(whereCache, schema, "Y", {})).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "whoknows",
column: "whatever",
value: "snork",
where: {
field: "whoknows",
column: "whatever",
value: "snork",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "whatever",
value: "snork",
where: {
field: "var",
column: "whatever",
value: "snork",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
field: "var",
column: "foo",
value: "snork",
where: {
field: "var",
column: "foo",
value: "snork",
},
})
).toEqual([undefined]);
});
test("whereCacheCreate", () => {
test("whereCacheCreate, where query", () => {
const query = {
field: "queryField",
column: "queryColumn",
value: "queryValue",
where: {
field: "queryField",
column: "queryColumn",
value: "queryValue",
},
};
const wc = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "queryValue" },
{
where: {
field: "queryField",
column: "queryColumn",
value: "queryValue",
},
},
[0, 1, 2]
);
expect(wc).toBeDefined();
expect(wc).toEqual(
expect.objectContaining({
field: {
queryField: expect.any(Map),
where: {
field: {
queryField: expect.any(Map),
},
},
})
);
expect(wc.field.queryField.has("queryColumn")).toEqual(true);
expect(wc.field.queryField.get("queryColumn")).toBeInstanceOf(Map);
expect(wc.field.queryField.get("queryColumn").has("queryValue")).toEqual(
true
);
expect(wc.where.field.queryField.has("queryColumn")).toEqual(true);
expect(wc.where.field.queryField.get("queryColumn")).toBeInstanceOf(Map);
expect(
wc.where.field.queryField.get("queryColumn").has("queryValue")
).toEqual(true);
expect(_whereCacheGet(wc, schema, "field", query)).toEqual([0, 1, 2]);
});
test("whereCacheMerge", () => {
test("whereCacheCreate, summarize query", () => {
const query = {
summarize: {
method: "method",
field: "queryField",
column: "queryColumn",
values: ["queryValue"],
},
};
const wc = _whereCacheCreate("field", query, [0, 1, 2]);
expect(_whereCacheGet(wc, schema, "field", query)).toEqual([0, 1, 2]);
});
test("whereCacheCreate, unknown query type", () => {
expect(_whereCacheCreate("field", { foobar: true }, [1])).toEqual({});
});
test("whereCacheMerge, where queries", () => {
let wc;
// remember, will mutate dst
const src = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "foo" },
{ where: { field: "queryField", column: "queryColumn", value: "foo" } },
["foo"]
);
const dst1 = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "bar" },
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
["dst1"]
);
wc = _whereCacheMerge(dst1, src);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "foo",
where: {
field: "queryField",
column: "queryColumn",
value: "foo",
},
})
).toEqual(["foo"]);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "bar",
where: {
field: "queryField",
column: "queryColumn",
value: "bar",
},
})
).toEqual(["dst1"]);
const dst2 = _whereCacheCreate(
"field",
{ field: "queryField", column: "queryColumn", value: "bar" },
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
["dst2"]
);
wc = _whereCacheMerge(dst2, dst1, src);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "foo",
where: {
field: "queryField",
column: "queryColumn",
value: "foo",
},
})
).toEqual(["foo"]);
expect(
_whereCacheGet(wc, schema, "field", {
field: "queryField",
column: "queryColumn",
value: "bar",
where: {
field: "queryField",
column: "queryColumn",
value: "bar",
},
})
).toEqual(["dst1"]);
wc = _whereCacheMerge({}, src);
expect(wc).toEqual(src);
wc = _whereCacheMerge({ field: { queryField: new Map() } }, src);
wc = _whereCacheMerge({ where: { field: { queryField: new Map() } } }, src);
expect(wc).toEqual(src);
});
test("whereCacheMerge, mixed queries", () => {
const wc = _whereCacheMerge(
_whereCacheCreate(
"field",
{
where: {
field: "queryField",
column: "queryColumn",
value: "foo",
},
},
["a"]
),
_whereCacheCreate(
"field",
{
summarize: {
method: "mean",
field: "queryField",
column: "queryColumn",
values: ["foo", "bar", "baz"],
},
},
["b"]
)
);
expect(
_whereCacheGet(wc, schema, "field", {
where: {
field: "queryField",
column: "queryColumn",
value: "foo",
},
})
).toEqual(["a"]);
expect(
_whereCacheGet(wc, schema, "field", {
summarize: {
method: "mean",
field: "queryField",
column: "queryColumn",
values: ["foo", "bar", "baz"],
},
})
).toEqual(["b"]);
expect(
_whereCacheGet(wc, schema, "field", {
where: {
field: "queryField",
column: "queryColumn",
value: "does-not-exist",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet(wc, schema, "field", {
summarize: {
method: "no-such-method",
field: "queryField",
column: "queryColumn",
values: ["does-not-exist"],
},
})
).toEqual([undefined]);
});
});
+4 -4
View File
@@ -1,11 +1,11 @@
import _ from "lodash";
import cloneDeep from "lodash.clonedeep";
import calcCentroid from "../../src/util/centroid";
import quantile from "../../src/util/quantile";
import { matrixFBSToDataframe } from "../../src/util/stateManager/matrix";
import * as REST from "./stateManager/sampleResponses";
import { indexEntireSchema } from "../../src/util/stateManager/schemaHelpers";
import { _normalizeCategoricalSchema } from "../../src/annoMatrix/schema";
import { normalizeWritableCategoricalSchema } from "../../src/annoMatrix/normalize";
describe("centroid", () => {
let schema;
@@ -13,11 +13,11 @@ describe("centroid", () => {
let obsLayout;
beforeAll(() => {
schema = indexEntireSchema(_.cloneDeep(REST.schema.schema));
schema = indexEntireSchema(cloneDeep(REST.schema.schema));
obsAnnotations = matrixFBSToDataframe(REST.annotationsObs);
obsLayout = matrixFBSToDataframe(REST.layoutObs);
_normalizeCategoricalSchema(
normalizeWritableCategoricalSchema(
schema.annotations.obsByName.field3,
obsAnnotations.col("field3")
);
@@ -793,9 +793,7 @@ describe("dataframe factories", () => {
[3, 3],
[new Array(3).fill(0), new Array(3).fill(0), new Array(3).fill(0)]
);
const dfB = dfA.mapColumns(() => {
return new Array(3).fill(1);
});
const dfB = dfA.mapColumns(() => new Array(3).fill(1));
expect(dfA).not.toBe(dfB);
expect(dfB.iat(0, 0)).toEqual(1);
expect(dfB.iat(0, 1)).toEqual(1);
@@ -88,16 +88,7 @@ describe("Dataframe column histogram", () => {
expect(df.col(1).histogram(5, [0, 100])).toEqual([5, 1, 0, 0, 2]);
expect(df.col(0).histogram(2, [0, 10])).toEqual([2, 2]);
expect(df.col(0).histogram(10, [0, 100])).toEqual([
3,
2,
1,
0,
0,
0,
0,
0,
0,
2,
3, 2, 1, 0, 0, 0, 0, 0, 0, 2,
]);
});
});
+1 -5
View File
@@ -19,11 +19,7 @@ describe("quantile", () => {
test("multi q", () => {
const arr = new Float32Array([9, 3, 5, 6, 0]);
expect(quantile([0, 0.25, 0.5, 0.75, 1.0], arr)).toMatchObject([
0,
3,
5,
6,
9,
0, 3, 5, 6, 9,
]);
});
});
@@ -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 */
@@ -1,38 +1,9 @@
/*
test controls helpers
*/
import { subsetAndResetGeneLists } from "../../../src/util/stateManager/controlsHelpers";
import * as globals from "../../../src/globals";
// TODO #2227 test: improve test coverage on control helper functions
// (`isSelectableCategoryName()`, `selectableCategoryNames()`, `createCategorySummaryFromDfCol()`, `createCategoricalSelection()`, )
describe("controls helpers", () => {
test("subsetAndResetGeneLists", () => {
const geneList = [];
const genRandGene = () => Math.random().toString(36).substring(2, 6);
// build a unique set of genes
for (let i = 0; i < 150; i += 1) {
let randGene = genRandGene();
while (geneList.includes(randGene)) randGene = genRandGene();
geneList.push(randGene);
}
// insert duplicates
geneList[0] = "dupl";
geneList[20] = "dupl";
const state = {
userDefinedGenes: geneList.slice(0, 20),
diffexpGenes: geneList.slice(20),
};
const [newUserDefinedGenes, newDiffExpGenes] = subsetAndResetGeneLists(
state
);
const expectedNewUserDefinedGenes = [
...geneList.slice(0, 20),
...geneList.slice(21),
].slice(0, globals.maxGenes);
expect(globals.maxUserDefinedGenes).toBeLessThan(globals.maxGenes);
expect(geneList.length).toBeGreaterThan(globals.maxGenes);
expect(newUserDefinedGenes).toHaveLength(globals.maxGenes);
expect(newUserDefinedGenes).toStrictEqual(expectedNewUserDefinedGenes);
expect(newDiffExpGenes).toStrictEqual([]);
});
test("placeholder", () => {});
});
@@ -1,3 +1,7 @@
import every from "lodash.every";
import map from "lodash.map";
import isNumber from "lodash.isnumber";
import zip from "lodash.zip";
import _ from "lodash";
import { flatbuffers } from "flatbuffers";
import { NetEncoding } from "../../../src/util/stateManager/matrix_generated";
@@ -121,10 +125,10 @@ function encodeMatrix(columns, colIndex = undefined) {
*/
const utf8Encoder = new TextEncoder("utf-8");
const builder = new flatbuffers.Builder(1024);
const cols = _.map(columns, (carr) => {
const cols = map(columns, (carr) => {
let uType;
let tarr;
if (_.every(carr, _.isNumber)) {
if (every(carr, isNumber)) {
uType = NetEncoding.TypedArray.Float32Array;
tarr = encodeTypedArray(builder, uType, new Float32Array(carr));
} else {
@@ -167,12 +171,12 @@ function encodeMatrix(columns, colIndex = undefined) {
}
const anAnnotationsObsFBSResponse = (() => {
const columns = _.zip(...anAnnotationsObsJSONResponse.data).slice(1);
const columns = zip(...anAnnotationsObsJSONResponse.data).slice(1);
return encodeMatrix(columns, anAnnotationsObsJSONResponse.names);
})();
const anAnnotationsVarFBSResponse = (() => {
const columns = _.zip(...anAnnotationsVarJSONResponse.data).slice(1);
const columns = zip(...anAnnotationsVarJSONResponse.data).slice(1);
return encodeMatrix(columns, anAnnotationsVarJSONResponse.names);
})();
@@ -1,5 +1,3 @@
// jshint esversion: 6
import BitArray from "../../../src/util/typedCrossfilter/bitArray";
const defaultTestLength = 8;
@@ -1,4 +1,5 @@
import _ from "lodash";
import filter from "lodash.filter";
import zip from "lodash.zip";
import Crossfilter from "../../../src/util/typedCrossfilter";
@@ -251,12 +252,12 @@ describe("ImmutableTypedCrossfilter", () => {
test.each([[[]], [[2]], [[2, 1]], [[9, 82]], [[0, 1]]])("exact: %p", (v) =>
expect(
p.select("quantity", { mode: "exact", values: v }).countSelected()
).toEqual(_.filter(someData, (d) => v.includes(d.quantity)).length)
).toEqual(filter(someData, (d) => v.includes(d.quantity)).length)
);
test("single value exact", () => {
expect(
p.select("quantity", { mode: "exact", values: 2 }).countSelected()
).toEqual(_.filter(someData, (d) => d.quantity === 2).length);
).toEqual(filter(someData, (d) => d.quantity === 2).length);
});
test.each([
[0, 1],
@@ -267,7 +268,7 @@ describe("ImmutableTypedCrossfilter", () => {
expect(
p.select("quantity", { mode: "range", lo, hi }).countSelected()
).toEqual(
_.filter(someData, (d) => d.quantity >= lo && d.quantity < hi).length
filter(someData, (d) => d.quantity >= lo && d.quantity < hi).length
)
);
test("bad mode", () => {
@@ -298,12 +299,12 @@ describe("ImmutableTypedCrossfilter", () => {
])("exact: %p", (v) =>
expect(
p.select("type", { mode: "exact", values: v }).countSelected()
).toEqual(_.filter(someData, (d) => v.includes(d.type)).length)
).toEqual(filter(someData, (d) => v.includes(d.type)).length)
);
test("single value exact", () => {
expect(
p.select("type", { mode: "exact", values: "tab" }).countSelected()
).toEqual(_.filter(someData, (d) => d.type === "tab").length);
).toEqual(filter(someData, (d) => d.type === "tab").length);
});
test("range", () => {
expect(() => p.select("type", { mode: "range", lo: 0, hi: 9 })).toThrow(
@@ -341,7 +342,7 @@ describe("ImmutableTypedCrossfilter", () => {
.select("coords", { mode: "within-rect", minX, minY, maxX, maxY })
.allSelected()
).toEqual(
_.filter(someData, (d) => {
filter(someData, (d) => {
const [x, y] = d.coords;
return minX <= x && x < maxX && minY <= y && y < maxY;
})
@@ -397,7 +398,7 @@ describe("ImmutableTypedCrossfilter", () => {
expect(
p.select("coords", { mode: "within-polygon", polygon }).allSelected()
).toEqual(
_.zip(someData, expected)
zip(someData, expected)
.filter((x) => x[1])
.map((x) => x[0])
);
@@ -1,5 +1,3 @@
// jshint esversion: 6
// const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals");
import PositiveIntervals from "../../../src/util/typedCrossfilter/positiveIntervals";
@@ -153,9 +151,9 @@ describe("intersection", () => {
[1, 2],
[6, 9],
]);
expect(
PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])
).toEqual([[1363, 2638]]);
expect(PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])).toEqual(
[[1363, 2638]]
);
expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([
[1, 2],
]);
+2
View File
@@ -16,6 +16,8 @@ module.exports = {
"@babel/plugin-proposal-function-bind",
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-transform-private-methods", { loose: true }],
["@babel/plugin-transform-private-property-in-object", { loose: true }],
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
+2
View File
@@ -15,6 +15,8 @@ module.exports = {
"@babel/plugin-proposal-function-bind",
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-transform-private-methods", { loose: true }],
["@babel/plugin-transform-private-property-in-object", { loose: true }],
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-transform-react-constant-elements",
"@babel/plugin-transform-runtime",
+21 -12
View File
@@ -1,23 +1,21 @@
module.exports = {
root: true,
parser: "babel-eslint",
extends: [
"airbnb",
"plugin:eslint-comments/recommended",
"plugin:@blueprintjs/recommended",
"plugin:compat/recommended",
"plugin:prettier/recommended",
"prettier/react",
"plugin:jsx-a11y/recommended",
// (thuang) disable eslint formatting rules, so prettier can do its job
// Do not use `plugin:prettier/recommended` per doc below:
// https://prettier.io/docs/en/integrating-with-linters.html
"prettier",
],
settings: {
polyfills: [
"TextDecoder",
"TextEncoder",
"fetch",
"Request",
"Response",
"Headers",
"AbortController",
],
// AbortController is not supported in iOS Safari 10.3, Chrome 61
// Headers is not supported in iOS Safari 10.3
polyfills: ["Headers", "AbortController"],
},
env: { browser: true, commonjs: true, es6: true },
globals: {
@@ -30,6 +28,7 @@ module.exports = {
context: true,
beforeEach: true,
},
parser: "@babel/eslint-parser",
parserOptions: {
ecmaVersion: 2017,
sourceType: "module",
@@ -37,8 +36,12 @@ module.exports = {
jsx: true,
generators: true,
},
babelOptions: {
configFile: "./configuration/babel/babel.prod.js",
},
},
rules: {
"react/jsx-no-target-blank": "off",
"eslint-comments/require-description": ["error"],
"no-magic-numbers": "off",
"no-nested-ternary": "off",
@@ -64,6 +67,12 @@ module.exports = {
"LabeledStatement",
"WithStatement",
],
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: true,
},
],
},
overrides: [
{
@@ -1,3 +1,4 @@
module.exports = {
"*.js": "eslint --fix",
"**/*": "prettier --write --ignore-unknown",
};
@@ -56,13 +56,6 @@
/>
<div>Chrome &gt; 60</div>
</a>
<a href="https://www.apple.com/safari/" aria-label="Download Safari">
<img
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/safari.png"
style="width: 80px; height: 80px;"
/>
<div>Safari ≥ 10.1</div>
</a>
<a href="https://www.mozilla.com/firefox/" aria-label="Download Firefox">
<img
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/firefox.png"
@@ -75,7 +68,7 @@
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/edge.png"
style="width: 80px; height: 80px;"
/>
<div>Edge ≥ 15</div>
<div>Edge ≥ 79</div>
</a>
</div>
</div>
@@ -1,8 +1,6 @@
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { merge } = require("webpack-merge");
@@ -11,6 +9,7 @@ const sharedConfig = require("./webpack.config.shared");
const babelOptions = require("../babel/babel.dev");
const fonts = path.resolve("src/fonts");
const images = path.resolve("src/images");
const nodeModules = path.resolve("node_modules");
const devConfig = {
@@ -30,8 +29,12 @@ const devConfig = {
{
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
loader: "file-loader",
include: [nodeModules, fonts],
query: { name: "static/assets/[name].[ext]" },
include: [nodeModules, fonts, images],
options: {
name: "static/assets/[name].[ext]",
// (thuang): This is needed to make sure @font url path is '/static/assets/'
publicPath: "..",
},
},
],
},
@@ -40,21 +43,6 @@ const devConfig = {
inject: true,
template: path.resolve("index.html"),
}),
new FaviconsWebpackPlugin({
logo: "./favicon.png",
prefix: "static/img/",
favicons: {
icons: {
android: false,
appleIcon: false,
appleStartup: false,
coast: false,
firefox: false,
windows: false,
yandex: false,
},
},
}),
new MiniCssExtractPlugin({
filename: "static/[name].css",
}),
@@ -63,14 +51,16 @@ const devConfig = {
__REACT_DEVTOOLS_GLOBAL_HOOK__: "({ isDisabled: true })",
}),
new webpack.DefinePlugin({
"process.env.CXG_SERVER_PORT": JSON.stringify(
process.env.CXG_SERVER_PORT
),
}),
new ScriptExtHtmlWebpackPlugin({
async: "obsolete",
// webpack 5 no longer polyfills NodeJS modules, so fake the one we need
"process.env": JSON.stringify({
NODE_ENV: process.env.NODE_ENV || "development",
CXG_SERVER_PORT: process.env.CXG_SERVER_PORT || "5005",
}),
}),
],
infrastructureLogging: {
level: "warn",
},
};
module.exports = merge(sharedConfig, devConfig);
@@ -1,10 +1,9 @@
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const CleanCss = require("clean-css");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { merge } = require("webpack-merge");
@@ -15,6 +14,7 @@ const CspHashPlugin = require("./cspHashPlugin");
const sharedConfig = require("./webpack.config.shared");
const fonts = path.resolve("src/fonts");
const images = path.resolve("src/images");
const nodeModules = path.resolve("node_modules");
const prodConfig = {
@@ -28,8 +28,8 @@ const prodConfig = {
minimize: true,
minimizer: [
new TerserJSPlugin({}),
new OptimizeCSSAssetsPlugin({
cssProcessor: CleanCss,
new CssMinimizerPlugin({
minify: CssMinimizerPlugin.cleanCssMinify,
}),
],
},
@@ -44,8 +44,12 @@ const prodConfig = {
{
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
loader: "file-loader",
include: [nodeModules, fonts],
query: { name: "static/assets/[name]-[contenthash].[ext]" },
include: [nodeModules, fonts, images],
options: {
name: "static/assets/[name]-[contenthash].[ext]",
// (thuang): This is needed to make sure @font url path is '../static/assets/'
publicPath: "..",
},
},
],
},
@@ -61,27 +65,18 @@ const prodConfig = {
protectWebpackAssets: false,
cleanAfterEveryBuildPatterns: ["main.js", "main.css"],
}),
new FaviconsWebpackPlugin({
logo: "./favicon.png",
prefix: "static/assets/",
favicons: {
icons: {
android: false,
appleIcon: false,
appleStartup: false,
coast: false,
firefox: false,
windows: false,
yandex: false,
},
},
}),
new MiniCssExtractPlugin({
filename: "static/[name]-[contenthash].css",
}),
new CspHashPlugin({
filename: "csp-hashes.json",
}),
new webpack.DefinePlugin({
// webpack 5 no longer polyfills NodeJS modules, so fake the one we need
"process.env": JSON.stringify({
NODE_ENV: "production",
}),
}),
],
performance: {
maxEntrypointSize: 2000000,
@@ -1,13 +1,12 @@
const path = require("path");
const fs = require("fs");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ObsoleteWebpackPlugin = require("obsolete-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
const ObsoleteWebpackPlugin = require("webpack-obsolete-plugin");
const src = path.resolve("src");
const nodeModules = path.resolve("node_modules");
const publicPath = "/";
const publicPath = "";
const rawObsoleteHTMLTemplate = fs.readFileSync(
`${__dirname}/obsoleteHTMLTemplate.html`,
@@ -20,7 +19,6 @@ module.exports = {
entry: [
"core-js",
"regenerator-runtime/runtime",
"fastestsmallesttextencoderdecoder",
"whatwg-fetch",
"abort-controller/polyfill",
"./src/index",
@@ -41,7 +39,7 @@ module.exports = {
loader: "css-loader",
options: {
modules: {
localIdentName: "[name]__[local]___[hash:base64:5]",
localIdentName: "[name]__[local]___[contenthash:base64:5]",
},
importLoaders: 1,
},
@@ -67,8 +65,5 @@ module.exports = {
template: obsoleteHTMLTemplate,
promptOnNonTargetBrowser: false,
}),
new ScriptExtHtmlWebpackPlugin({
async: "obsolete",
}),
],
};
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>cell&times;gene</title>
<title>CELL&times;GENE | Annotate</title>
<style>
html,
body,
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>cell&times;gene</title>
<title>CELL&times;GENE | Annotate</title>
<style>
html,
body,
+1
View File
@@ -14,6 +14,7 @@ const DEFAULT_LAUNCH_CONFIG = {
headless: !isHeadful,
args: ["--ignore-certificate-errors", "--ignore-ssl-errors"],
ignoreHTTPSErrors: true,
timeout: 90000,
defaultViewport: {
width: 1280,
height: 960,
+26355 -14271
View File
File diff suppressed because it is too large Load Diff
+82 -67
View File
@@ -1,6 +1,6 @@
{
"name": "cellxgene",
"version": "0.16.0",
"version": "1.3.0",
"license": "MIT",
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
"repository": "https://github.com/chanzuckerberg/cellxgene",
@@ -10,15 +10,16 @@
"dev": "npm run build -- configuration/webpack/webpack.config.dev.js",
"e2e": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.js",
"e2e-annotations": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2eAnnotations.test.js",
"e2e-prod": "CXG_URL_BASE='https://cellxgene.cziscience.com/d/pbmc3k.cxg/' jest --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.js",
"fmt": "eslint --fix src __tests__",
"lint": "eslint --fix src __tests__",
"prod": "npm run build -- configuration/webpack/webpack.config.prod.js",
"test": "jest --testPathIgnorePatterns e2e"
"test": "jest --testPathIgnorePatterns e2e",
"prepare": "cd .. && husky install client/.husky"
},
"engineStrict": true,
"engines": {
"npm": ">=3.0.0"
"npm": ">=9.6.7",
"node": "^18.17.0"
},
"eslintConfig": {
"extends": "./configuration/eslint/eslint.js"
@@ -31,111 +32,118 @@
},
"browserslist": [
"Chrome > 60",
"Safari >= 10.1",
"iOS >= 10.3",
"Firefox >= 60",
"Edge >= 15",
"not Explorer > 0"
"Edge >= 79",
"not Explorer > 0",
"not Safari > 0"
],
"dependencies": {
"@blueprintjs/core": "^3.30.0",
"@babel/eslint-parser": "^7.15.0",
"@blueprintjs/core": "^3.44.0",
"@blueprintjs/icons": "^3.19.0",
"@blueprintjs/select": "^3.13.5",
"@blueprintjs/popover2": "^0.11.2",
"@blueprintjs/select": "^3.16.0",
"abort-controller": "^3.0.0",
"core-js": "^3.6.5",
"core-js": "^3.16.3",
"d3": "^4.10.0",
"d3-scale-chromatic": "^1.5.0",
"fastestsmallesttextencoderdecoder": "^1.0.22",
"flatbuffers": "^1.11.0",
"fuzzysort": "^1.1.4",
"gl-mat4": "^1.2.0",
"gl-matrix": "^3.3.0",
"gl-vec3": "^1.1.3",
"is-number": "^7.0.0",
"lodash": "^4.17.19",
"lodash.clonedeep": "^4.5.0",
"lodash.difference": "^4.5.0",
"lodash.foreach": "^4.5.0",
"lodash.pull": "^4.1.0",
"lodash.sortby": "^4.7.0",
"lodash.uniq": "^4.5.0",
"memoize-one": "^5.1.1",
"react": "^16.13.1",
"pako": "^2.0.3",
"react": "^17.0.2",
"react-async": "^10.0.1",
"react-dom": "^16.13.1",
"react-dom": "^17.0.2",
"react-flip-toolkit": "^7.0.12",
"react-helmet": "^5.2.1",
"react-icons": "^3.10.0",
"react-helmet": "^6.1.0",
"react-icons": "^4.2.0",
"react-redux": "^7.2.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.7",
"regl": "^1.6.1",
"script-ext-html-webpack-plugin": "^2.1.4",
"regl": "^2.1.0",
"sha1": "^1.1.1",
"tinyqueue": "^2.0.3",
"webpack-merge": "^5.0.9",
"whatwg-fetch": "^3.2.0"
},
"devDependencies": {
"@babel/core": "^7.10.5",
"@babel/core": "^7.25.2",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-proposal-export-namespace-from": "^7.10.4",
"@babel/plugin-proposal-function-bind": "^7.10.5",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/plugin-transform-react-constant-elements": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/register": "^7.10.5",
"@babel/runtime": "^7.10.5",
"@sentry/webpack-plugin": "^1.12.0",
"babel-eslint": "^10.1.0",
"@babel/plugin-transform-private-property-in-object": "^7.22.11",
"@babel/plugin-transform-react-constant-elements": "^7.13.13",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.22.20",
"@babel/preset-react": "^7.13.13",
"@babel/register": "^7.13.16",
"@babel/runtime": "^7.13.16",
"@blueprintjs/eslint-plugin": "^0.3.0",
"@sentry/webpack-plugin": "^1.15.0",
"babel-jest": "^26.1.0",
"babel-loader": "^8.1.0",
"babel-preset-modern-browsers": "^14.2.1",
"babel-preset-modern-browsers": "^15.0.2",
"chalk": "^4.1.0",
"cheerio": "^1.0.0-rc.3",
"clean-css": "^4.2.3",
"clean-webpack-plugin": "^3.0.0",
"cheerio": "^1.0.0-rc.6",
"clean-css": "^5.1.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"codecov": "^3.7.1",
"connect-history-api-fallback": "^1.6.0",
"css-loader": "^3.6.0",
"eslint": "^7.4.0",
"css-loader": "^5.2.4",
"css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^7.24.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-loader": "^3.0.4",
"eslint-plugin-compat": "^3.8.0",
"eslint-config-prettier": "^8.2.0",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jest": "^23.18.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^24.3.5",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.0.8",
"expect-puppeteer": "^4.4.0",
"expect-puppeteer": "^5.0.0",
"express": "^4.17.1",
"favicons-webpack-plugin": "^3.0.1",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.3.0",
"husky": "^4.2.5",
"jest": "^26.1.0",
"jest-circus": "^26.1.0",
"jest-environment-puppeteer": "^4.4.0",
"html-webpack-plugin": "^5.3.1",
"husky": "^7.0.2",
"jest": "^27.0.6",
"jest-circus": "^27.0.6",
"jest-environment-puppeteer": "^5.0.1",
"jest-fetch-mock": "^3.0.3",
"jest-puppeteer": "^4.4.0",
"jest-puppeteer": "^6.2.0",
"json-loader": "^0.5.7",
"lint-staged": "^10.2.11",
"mini-css-extract-plugin": "^0.9.0",
"obsolete-webpack-plugin": "^0.5.6",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"lodash": "^4.17.21",
"lodash.every": "^4.6.0",
"lodash.filter": "^4.6.0",
"lodash.isnumber": "^3.0.3",
"lodash.map": "^4.6.0",
"lodash.zip": "^4.2.0",
"mini-css-extract-plugin": "^1.5.0",
"prettier": "^2.0.5",
"puppeteer": "^3.3.0",
"puppeteer": "^10.4.0",
"rimraf": "^3.0.2",
"serve-favicon": "^2.5.0",
"style-loader": "^1.2.1",
"sw-precache-webpack-plugin": "^1.0.0",
"terser-webpack-plugin": "^3.0.7",
"url-loader": "^4.1.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-dev-middleware": "^3.7.2"
"terser-webpack-plugin": "^5.1.1",
"webpack": "^5.94.0",
"webpack-cli": "^4.6.0",
"webpack-dev-middleware": "^4.1.0",
"webpack-merge": "^5.0.9",
"webpack-obsolete-plugin": "^1.0.5"
},
"jest": {
"testMatch": [
@@ -168,6 +176,18 @@
"loose": true
}
],
[
"@babel/plugin-transform-private-methods",
{
"loose": true
}
],
[
"@babel/plugin-transform-private-property-in-object",
{
"loose": true
}
],
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-transform-react-constant-elements",
"@babel/plugin-transform-runtime",
@@ -176,10 +196,5 @@
]
}
}
},
"husky": {
"hooks": {
"pre-commit": "lint-staged --config \"./configuration/lint-staged/lint-staged.config.js\""
}
}
}
+24 -20
View File
@@ -1,46 +1,50 @@
/* eslint-disable */
// jshint esversion: 6
var path = require("path");
var historyApiFallback = require("connect-history-api-fallback");
var chalk = require("chalk");
var express = require("express");
var favicon = require("serve-favicon");
var webpack = require("webpack");
var config = require("../configuration/webpack/webpack.config.dev");
var utils = require("./utils");
const chalk = require("chalk");
const express = require("express");
const favicon = require("serve-favicon");
const webpack = require("webpack");
const devMiddleware = require("webpack-dev-middleware");
const config = require("../configuration/webpack/webpack.config.dev");
const utils = require("./utils");
process.env.NODE_ENV = "development";
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
var compiler = webpack(config);
const compiler = webpack(config);
compiler.plugin("invalid", () => {
compiler.hooks.invalid.tap("invalid", () => {
utils.clearConsole();
console.log("Compiling...");
});
compiler.plugin("done", (stats) => {
compiler.hooks.done.tap("done", (stats) => {
utils.formatStats(stats, CLIENT_PORT);
});
// Launch server
var app = express();
app.use(historyApiFallback({ verbose: false }));
const app = express();
app.use(
require("webpack-dev-middleware")(compiler, {
logLevel: "warn",
devMiddleware(compiler, {
publicPath: config.output.publicPath,
index: true,
})
);
app.use(favicon("./favicon.png"));
app.get("*", (req, res) => {
res.sendFile(path.resolve("index.html"));
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) => {
+3 -1
View File
@@ -8,7 +8,9 @@ function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
function formatMessage(message) {
function formatMessage(messageObject) {
let { message, details } = messageObject;
if (details) message = message + ": " + details;
return message
.replace("Module build failed: SyntaxError:", friendlySyntaxErrorLabel)
.replace(
+313 -245
View File
@@ -1,287 +1,260 @@
/*
Action creators for user annotation
*/
import _ from "lodash";
import difference from "lodash.difference";
import pako from "pako";
import * as globals from "../globals";
import { MatrixFBS, AnnotationsHelpers } from "../util/stateManager";
const { isUserAnnotation } = AnnotationsHelpers;
export const annotationCreateCategoryAction = (
newCategoryName,
categoryToDuplicate
) => async (dispatch, getState) => {
/*
export const annotationCreateCategoryAction =
(newCategoryName, categoryToDuplicate) => async (dispatch, getState) => {
/*
Add a new user-created category to the obs annotations.
Arguments:
newCategoryName - string name for the category.
categoryToDuplicate - obs category to use for initial values, or null.
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
const { schema } = prevAnnoMatrix;
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
const { schema } = prevAnnoMatrix;
/* name must be a string, non-zero length */
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
throw new Error("user annotations require string name");
/* name must be a string, non-zero length */
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
throw new Error("user annotations require string name");
/* ensure the name isn't already in use! */
if (schema.annotations.obsByName[newCategoryName])
throw new Error("name collision on annotation category create");
/* ensure the name isn't already in use! */
if (schema.annotations.obsByName[newCategoryName])
throw new Error("name collision on annotation category create");
let initialValue;
let newSchema;
let ctor;
if (categoryToDuplicate) {
/* if we are duplicating a category, retrieve it */
const catDupSchema = schema.annotations.obsByName[categoryToDuplicate];
const catDupType = catDupSchema?.type;
if (catDupType !== "string" && catDupType !== "categorical")
throw new Error("categoryToDuplicate does not exist or has invalid type");
let initialValue;
let newSchema;
let ctor;
if (categoryToDuplicate) {
/* if we are duplicating a category, retrieve it */
const catDupSchema = schema.annotations.obsByName[categoryToDuplicate];
const catDupType = catDupSchema?.type;
if (catDupType !== "string" && catDupType !== "categorical")
throw new Error(
"categoryToDuplicate does not exist or has invalid type"
);
const catToDupDf = await prevAnnoMatrix
.base()
.fetch("obs", categoryToDuplicate);
const col = catToDupDf.col(categoryToDuplicate);
initialValue = col.asArray();
const { categories } = col.summarizeCategorical();
// all user-created annotations must have the unassigned category
if (!categories.includes(globals.unassignedCategoryLabel)) {
categories.push(globals.unassignedCategoryLabel);
const catToDupDf = await prevAnnoMatrix
.base()
.fetch("obs", categoryToDuplicate);
const col = catToDupDf.col(categoryToDuplicate);
initialValue = col.asArray();
const { categories } = col.summarizeCategorical();
// all user-created annotations must have the unassigned category
if (!categories.includes(globals.unassignedCategoryLabel)) {
categories.push(globals.unassignedCategoryLabel);
}
ctor = initialValue.constructor;
newSchema = {
...catDupSchema,
name: newCategoryName,
categories,
writable: true,
};
} else {
/* else assign to the standard default value */
initialValue = globals.unassignedCategoryLabel;
ctor = Array;
newSchema = {
name: newCategoryName,
type: "categorical",
categories: [globals.unassignedCategoryLabel],
writable: true,
};
}
ctor = initialValue.constructor;
newSchema = {
...catDupSchema,
name: newCategoryName,
categories,
writable: true,
};
} else {
/* else assign to the standard default value */
initialValue = globals.unassignedCategoryLabel;
ctor = Array;
newSchema = {
name: newCategoryName,
type: "categorical",
categories: [globals.unassignedCategoryLabel],
writable: true,
};
}
const obsCrossfilter = prevObsCrossfilter.addObsColumn(
newSchema,
ctor,
initialValue
);
const obsCrossfilter = prevObsCrossfilter.addObsColumn(
newSchema,
ctor,
initialValue
);
dispatch({
type: "annotation: create category",
data: newCategoryName,
categoryToDuplicate,
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
});
};
dispatch({
type: "annotation: create category",
data: newCategoryName,
categoryToDuplicate,
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
});
};
export const annotationRenameCategoryAction = (
oldCategoryName,
newCategoryName
) => (dispatch, getState) => {
/*
export const annotationRenameCategoryAction =
(oldCategoryName, newCategoryName) => (dispatch, getState) => {
/*
Rename a user-created annotation category
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, oldCategoryName))
throw new Error("not a user annotation");
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, oldCategoryName))
throw new Error("not a user annotation");
/* name must be a string, non-zero length */
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
throw new Error("user annotations require string name");
/* name must be a string, non-zero length */
if (typeof newCategoryName !== "string" || newCategoryName.length === 0)
throw new Error("user annotations require string name");
if (oldCategoryName === newCategoryName) return;
if (oldCategoryName === newCategoryName) return;
const obsCrossfilter = prevObsCrossfilter.renameObsColumn(
oldCategoryName,
newCategoryName
);
const obsCrossfilter = prevObsCrossfilter.renameObsColumn(
oldCategoryName,
newCategoryName
);
dispatch({
type: "annotation: category edited",
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
metadataField: oldCategoryName,
newCategoryText: newCategoryName,
data: newCategoryName,
});
};
dispatch({
type: "annotation: category edited",
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
metadataField: oldCategoryName,
newCategoryText: newCategoryName,
data: newCategoryName,
});
};
export const annotationDeleteCategoryAction = (categoryName) => (
dispatch,
getState
) => {
/*
export const annotationDeleteCategoryAction =
(categoryName) => (dispatch, getState) => {
/*
Delete a user-created category
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
const obsCrossfilter = prevObsCrossfilter.dropObsColumn(categoryName);
dispatch({
type: "annotation: delete category",
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
metadataField: categoryName,
});
};
const obsCrossfilter = prevObsCrossfilter.dropObsColumn(categoryName);
dispatch({
type: "annotation: delete category",
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
metadataField: categoryName,
});
};
export const annotationCreateLabelInCategory = (
categoryName,
labelName,
assignSelected
) => async (dispatch, getState) => {
/*
Add a new label to a user-defined category. If assignSelected is true, assign
export const annotationCreateLabelInCategory =
(categoryName, labelName, assignSelected) => async (dispatch, getState) => {
/*
Add a new label to a user-defined category. If assignSelected is true, assign
the label to all currently selected cells.
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
let obsCrossfilter = prevObsCrossfilter.addObsAnnoCategory(
categoryName,
labelName
);
if (assignSelected) {
obsCrossfilter = await obsCrossfilter.setObsColumnValues(
let obsCrossfilter = prevObsCrossfilter.addObsAnnoCategory(
categoryName,
labelName
);
if (assignSelected) {
obsCrossfilter = await obsCrossfilter.setObsColumnValues(
categoryName,
prevObsCrossfilter.allSelectedLabels(),
labelName
);
}
dispatch({
type: "annotation: add new label to category",
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
metadataField: categoryName,
newLabelText: labelName,
assignSelectedCells: assignSelected,
});
};
export const annotationDeleteLabelFromCategory =
(categoryName, labelName) => async (dispatch, getState) => {
/*
delete a label from a user-defined category
*/
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
const obsCrossfilter = await prevObsCrossfilter.removeObsAnnoCategory(
categoryName,
labelName,
globals.unassignedCategoryLabel
);
dispatch({
type: "annotation: delete label",
metadataField: categoryName,
label: labelName,
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
});
};
export const annotationRenameLabelInCategory =
(categoryName, oldLabelName, newLabelName) => async (dispatch, getState) => {
/*
label name change
*/
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
let obsCrossfilter = await prevObsCrossfilter.resetObsColumnValues(
categoryName,
oldLabelName,
newLabelName
);
obsCrossfilter = await obsCrossfilter.removeObsAnnoCategory(
categoryName,
oldLabelName,
globals.unassignedCategoryLabel
);
dispatch({
type: "annotation: label edited",
editedLabel: newLabelName,
metadataField: categoryName,
label: oldLabelName,
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
});
};
export const annotationLabelCurrentSelection =
(categoryName, labelName) => async (dispatch, getState) => {
/*
set the label on all currently selected
*/
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
const obsCrossfilter = await prevObsCrossfilter.setObsColumnValues(
categoryName,
prevObsCrossfilter.allSelectedLabels(),
labelName
);
}
dispatch({
type: "annotation: add new label to category",
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
metadataField: categoryName,
newLabelText: labelName,
assignSelectedCells: assignSelected,
});
};
export const annotationDeleteLabelFromCategory = (
categoryName,
labelName
) => async (dispatch, getState) => {
/*
delete a label from a user-defined category
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
const obsCrossfilter = await prevObsCrossfilter.removeObsAnnoCategory(
categoryName,
labelName,
globals.unassignedCategoryLabel
);
dispatch({
type: "annotation: delete label",
metadataField: categoryName,
label: labelName,
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
});
};
export const annotationRenameLabelInCategory = (
categoryName,
oldLabelName,
newLabelName
) => async (dispatch, getState) => {
/*
label name change
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
let obsCrossfilter = await prevObsCrossfilter.resetObsColumnValues(
categoryName,
oldLabelName,
newLabelName
);
obsCrossfilter = await obsCrossfilter.removeObsAnnoCategory(
categoryName,
oldLabelName,
globals.unassignedCategoryLabel
);
dispatch({
type: "annotation: label edited",
editedLabel: newLabelName,
metadataField: categoryName,
label: oldLabelName,
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
});
};
export const annotationLabelCurrentSelection = (
categoryName,
labelName
) => async (dispatch, getState) => {
/*
set the label on all currently selected
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
if (!prevAnnoMatrix || !prevObsCrossfilter) return;
if (!isUserAnnotation(prevAnnoMatrix, categoryName))
throw new Error("not a user annotation");
const obsCrossfilter = await prevObsCrossfilter.setObsColumnValues(
categoryName,
prevObsCrossfilter.allSelectedLabels(),
labelName
);
dispatch({
type: "annotation: label current cell selection",
metadataField: categoryName,
label: labelName,
obsCrossfilter,
annoMatrix: obsCrossfilter.annoMatrix,
});
};
dispatch({
type: "annotation: label current cell selection",
metadataField: categoryName,
label: labelName,
obsCrossfilter,
annoMatrix: obsCrossfilter.annoMatrix,
});
};
function writableAnnotations(annoMatrix) {
return annoMatrix.schema.annotations.obs.columns
@@ -307,7 +280,7 @@ export const needToSaveObsAnnotations = (annoMatrix, lastSavedAnnoMatrix) => {
// if the schema has changed, we need to save
const currentWritable = writableAnnotations(annoMatrix);
if (_.difference(currentWritable, writableAnnotations(lastSavedAnnoMatrix))) {
if (difference(currentWritable, writableAnnotations(lastSavedAnnoMatrix))) {
return true;
}
@@ -347,6 +320,7 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
const df = await annoMatrix.fetch("obs", writableAnnotations(annoMatrix));
const matrix = MatrixFBS.encodeMatrixFBS(df);
const compressedMatrix = pako.deflate(matrix);
try {
const queryString =
!dataCollectionNameIsReadOnly && !!dataCollectionName
@@ -358,7 +332,7 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
`${globals.API.prefix}${globals.API.version}annotations/obs${queryString}`,
{
method: "PUT",
body: matrix,
body: compressedMatrix,
headers: new Headers({
"Content-Type": "application/octet-stream",
}),
@@ -385,3 +359,97 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
});
}
};
export const saveGenesetsAction = () => async (dispatch, getState) => {
const state = getState();
// bail if gene sets not available, or in readonly mode.
const { config } = state;
const { lastTid, genesets } = state.genesets;
const genesetsAreAvailable =
config?.parameters?.annotations_genesets ?? false;
const genesetsReadonly =
config?.parameters?.annotations_genesets_readonly ?? true;
if (!genesetsAreAvailable || genesetsReadonly) {
// our non-save was completed!
return dispatch({
type: "autosave: genesets complete",
lastSavedGenesets: genesets,
});
}
dispatch({
type: "autosave: genesets started",
});
/* Create the JSON OTA data structure */
const tid = (lastTid ?? 0) + 1;
const genesetsOTA = [];
for (const [name, gs] of genesets) {
const genes = [];
for (const g of gs.genes.values()) {
genes.push({
gene_symbol: g.geneSymbol,
gene_description: g.geneDescription,
});
}
genesetsOTA.push({
geneset_name: name,
geneset_description: gs.genesetDescription,
genes,
});
}
const ota = {
tid,
genesets: genesetsOTA,
};
/* Save to server */
try {
const { dataCollectionNameIsReadOnly, dataCollectionName } =
state.annotations;
const queryString =
!dataCollectionNameIsReadOnly && !!dataCollectionName
? `?annotation-collection-name=${encodeURIComponent(
dataCollectionName
)}`
: "";
const res = await fetch(
`${globals.API.prefix}${globals.API.version}genesets${queryString}`,
{
method: "PUT",
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
body: JSON.stringify(ota),
credentials: "include",
}
);
if (!res.ok) {
return dispatch({
type: "autosave: genesets error",
message: `HTTP error ${res.status} - ${res.statusText}`,
res,
});
}
return Promise.all([
dispatch({
type: "autosave: genesets complete",
lastSavedGenesets: genesets,
}),
dispatch({
type: "geneset: set tid",
tid,
}),
]);
} catch (error) {
return dispatch({
type: "autosave: genesets error",
message: error.toString(),
error,
});
}
};
+29 -26
View File
@@ -5,40 +5,43 @@ action creators related to embeddings choice
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
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 embedding action creators
*/
const base = prevAnnoMatrix.base();
const embeddingDf = await base.fetch("emb", newEmbeddingName);
const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix).select(
"emb",
newEmbeddingName,
{
mode: "all",
}
);
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(
annoMatrix,
prevCrossfilter.obsCrossfilter
).select("emb", newEmbeddingName, {
mode: "all",
});
return [annoMatrix, obsCrossfilter];
}
export const layoutChoiceAction = (newLayoutChoice) => async (
dispatch,
getState
) => {
/*
export const layoutChoiceAction =
(newLayoutChoice) => async (dispatch, getState) => {
/*
On layout choice, make sure we have selected all on the previous layout, AND the new
layout.
*/
const { annoMatrix: prevAnnoMatrix } = getState();
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
prevAnnoMatrix,
newLayoutChoice
);
dispatch({
type: "set layout choice",
layoutChoice: newLayoutChoice,
obsCrossfilter,
annoMatrix,
});
};
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevCrossfilter } =
getState();
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
prevAnnoMatrix,
prevCrossfilter,
newLayoutChoice
);
dispatch({
type: "set layout choice",
layoutChoice: newLayoutChoice,
obsCrossfilter,
annoMatrix,
});
};
+149
View File
@@ -0,0 +1,149 @@
import { postUserErrorToast } from "../components/framework/toasters";
/*
Action creators for gene sets
Primarily used to keep the crossfilter and underlying data in sync with the UI.
The behavior manifest in these action creators:
Delete a gene set, will
* drop index & clear selection state on the gene set summary
* drop index & clear selection state of each gene in the geneset
Delete a gene from a gene set, will:
* drop index & clear selection state on the gene set summary
* drop index & clear selection state on the gene
Add a gene to a gene set, will:
* drop index & clear selection state on the gene set summary
* will NOT touch the selection state for the gene
Note that crossfilter indices are lazy created, as needed.
*/
export const genesetDelete = (genesetName) => (dispatch, getState) => {
const state = getState();
const { genesets } = state;
const gs = genesets?.genesets?.get(genesetName) ?? {};
const geneSymbols = Array.from(gs.genes.keys());
const obsCrossfilter = dropGeneset(dispatch, state, genesetName, geneSymbols);
if (genesetName === state.colors.colorAccessor) {
dispatch({
type: "reset colorscale",
});
}
dispatch({
type: "geneset: delete",
genesetName,
obsCrossfilter,
annoMatrix: obsCrossfilter.annoMatrix,
});
};
export const genesetAddGenes =
(genesetName, genes) => async (dispatch, getState) => {
const state = getState();
const { obsCrossfilter: prevObsCrossfilter, annoMatrix } = state;
const { schema } = annoMatrix;
const varIndex = schema.annotations.var.index;
const df = await annoMatrix.fetch("var", varIndex);
const geneNames = df.col(varIndex).asArray();
genes = genes.reduce((acc, gene) => {
if (geneNames.indexOf(gene.geneSymbol) === -1) {
postUserErrorToast(
`${gene.geneSymbol} doesn't appear to be a valid gene name.`
);
} else acc.push(gene);
return acc;
}, []);
const obsCrossfilter = dropGenesetSummaryDimension(
prevObsCrossfilter,
state,
genesetName
);
dispatch({
type: "continuous metadata histogram cancel",
continuousNamespace: { isGeneSetSummary: true },
selection: genesetName,
});
return dispatch({
type: "geneset: add genes",
genesetName,
genes,
obsCrossfilter,
annoMatrix: obsCrossfilter.annoMatrix,
});
};
export const genesetDeleteGenes =
(genesetName, geneSymbols) => (dispatch, getState) => {
const state = getState();
const obsCrossfilter = dropGeneset(
dispatch,
state,
genesetName,
geneSymbols
);
return dispatch({
type: "geneset: delete genes",
genesetName,
geneSymbols,
obsCrossfilter,
annoMatrix: obsCrossfilter.annoMatrix,
});
};
/*
Private
*/
function dropGenesetSummaryDimension(obsCrossfilter, state, genesetName) {
const { annoMatrix, genesets } = state;
const varIndex = annoMatrix.schema.annotations?.var?.index;
const gs = genesets?.genesets?.get(genesetName) ?? {};
const genes = Array.from(gs.genes.keys());
const query = {
summarize: {
method: "mean",
field: "var",
column: varIndex,
values: genes,
},
};
return obsCrossfilter.dropDimension("X", query);
}
function dropGeneDimension(obsCrossfilter, state, gene) {
const { annoMatrix } = state;
const varIndex = annoMatrix.schema.annotations?.var?.index;
const query = {
where: {
field: "var",
column: varIndex,
value: gene,
},
};
return obsCrossfilter.dropDimension("X", query);
}
function dropGeneset(dispatch, state, genesetName, geneSymbols) {
const { obsCrossfilter: prevObsCrossfilter } = state;
const obsCrossfilter = geneSymbols.reduce(
(crossfilter, gene) => dropGeneDimension(crossfilter, state, gene),
dropGenesetSummaryDimension(prevObsCrossfilter, state, genesetName)
);
dispatch({
type: "continuous metadata histogram cancel",
continuousNamespace: { isGeneSetSummary: true },
selection: genesetName,
});
geneSymbols.forEach((g) =>
dispatch({
type: "continuous metadata histogram cancel",
continuousNamespace: { isUserDefined: true },
selection: g,
})
);
return obsCrossfilter;
}
+100 -61
View File
@@ -5,14 +5,22 @@ import {
doJsonRequest,
dispatchNetworkErrorMessageToUser,
} from "../util/actionHelpers";
import {
requestReembed /* , reembedResetWorldToUniverse -- disabled temporarily, TODO issue #1606 */,
} from "./reembed";
import { loadUserColorConfig } from "../util/stateManager/colorHelpers";
import * as selnActions from "./selection";
import * as annoActions from "./annotation";
import * as viewActions from "./viewStack";
import * as embActions from "./embedding";
import * as genesetActions from "./geneset";
function setGlobalConfig(config) {
/**
* Set any global run-time config not _exclusively_ managed by the config reducer.
* This should only set fields defined in globals.globalConfig.
*/
globals.globalConfig.maxCategoricalOptionsToDisplay =
config?.parameters?.["max-category-items"] ??
globals.globalConfig.maxCategoricalOptionsToDisplay;
}
/*
return promise fetching user-configured colors
@@ -33,6 +41,9 @@ async function schemaFetch() {
async function configFetch(dispatch) {
return fetchJson("config").then((response) => {
const config = { ...globals.configDefaults, ...response.config };
setGlobalConfig(config);
dispatch({
type: "configuration load complete",
config,
@@ -41,6 +52,27 @@ async function configFetch(dispatch) {
});
}
async function genesetsFetch(dispatch, config) {
/* request genesets ONLY if the backend supports the feature */
const defaultResponse = {
genesets: [],
tid: 0,
};
if (config?.parameters?.annotations_genesets ?? false) {
fetchJson("genesets").then((response) => {
dispatch({
type: "geneset: initial load",
data: response ?? defaultResponse,
});
});
} else {
dispatch({
type: "geneset: initial load",
data: defaultResponse,
});
}
}
function prefetchEmbeddings(annoMatrix) {
/*
prefetch requests for all embeddings
@@ -64,6 +96,8 @@ const doInitialDataLoad = () =>
userColorsFetchAndLoad(dispatch),
]);
genesetsFetch(dispatch, config);
const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema);
const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
@@ -76,7 +110,7 @@ const doInitialDataLoad = () =>
});
dispatch({ type: "initial data load complete" });
const defaultEmbedding = config?.parameters?.["default_embedding"];
const defaultEmbedding = config?.parameters?.default_embedding;
const layoutSchema = schema?.schema?.layout?.obs ?? [];
if (
defaultEmbedding &&
@@ -126,72 +160,74 @@ const dispatchDiffExpErrors = (dispatch, response) => {
}
};
const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
dispatch,
getState
) => {
dispatch({ type: "request differential expression started" });
try {
/*
const requestDifferentialExpression =
(set1, set2, num_genes = 50) =>
async (dispatch, getState) => {
dispatch({ type: "request differential expression started" });
try {
/*
Steps:
1. get the most differentially expressed genes
2. get expression data for each
*/
const { annoMatrix } = getState();
const varIndexName = annoMatrix.schema.annotations.var.index;
const { annoMatrix } = getState();
const varIndexName = annoMatrix.schema.annotations.var.index;
// Legal values are null, Array or TypedArray. Null is initial state.
if (!set1) set1 = [];
if (!set2) set2 = [];
// Legal values are null, Array or TypedArray. Null is initial state.
if (!set1) set1 = [];
if (!set2) set2 = [];
// These lines ensure that we convert any TypedArray to an Array.
// This is necessary because JSON.stringify() does some very strange
// things with TypedArrays (they are marshalled to JSON objects, rather
// than being marshalled as a JSON array).
set1 = Array.isArray(set1) ? set1 : Array.from(set1);
set2 = Array.isArray(set2) ? set2 : Array.from(set2);
// These lines ensure that we convert any TypedArray to an Array.
// This is necessary because JSON.stringify() does some very strange
// things with TypedArrays (they are marshalled to JSON objects, rather
// than being marshalled as a JSON array).
set1 = Array.isArray(set1) ? set1 : Array.from(set1);
set2 = Array.isArray(set2) ? set2 : Array.from(set2);
const res = await fetch(
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
{
method: "POST",
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
body: JSON.stringify({
mode: "topN",
count: num_genes,
set1: { filter: { obs: { index: set1 } } },
set2: { filter: { obs: { index: set2 } } },
}),
credentials: "include",
const res = await fetch(
`${globals.API.prefix}${globals.API.version}diffexp/obs`,
{
method: "POST",
headers: new Headers({
Accept: "application/json",
"Content-Type": "application/json",
}),
body: JSON.stringify({
mode: "topN",
count: num_genes,
set1: { filter: { obs: { index: set1 } } },
set2: { filter: { obs: { index: set2 } } },
}),
credentials: "include",
}
);
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
return dispatchDiffExpErrors(dispatch, res);
}
);
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
return dispatchDiffExpErrors(dispatch, res);
const response = await res.json();
const varIndex = await annoMatrix.fetch("var", varIndexName);
const diffexpLists = { negative: [], positive: [] };
for (const polarity of Object.keys(diffexpLists)) {
diffexpLists[polarity] = response[polarity].map((v) => [
varIndex.at(v[0], varIndexName),
...v.slice(1),
]);
}
/* then send the success case action through */
return dispatch({
type: "request differential expression success",
data: diffexpLists,
});
} catch (error) {
return dispatch({
type: "request differential expression error",
error,
});
}
const response = await res.json();
const varIndex = await annoMatrix.fetch("var", varIndexName);
const data = response.map((v) => [
varIndex.at(v[0], varIndexName),
...v.slice(1),
]);
/* then send the success case action through */
return dispatch({
type: "request differential expression success",
data,
});
} catch (error) {
return dispatch({
type: "request differential expression error",
error,
});
}
};
};
function fetchJson(pathAndQuery) {
return doJsonRequest(
@@ -204,7 +240,6 @@ export default {
requestDifferentialExpression,
requestSingleGeneExpressionCountsForColoringPOST,
requestUserDefinedGene,
requestReembed,
selectContinuousMetadataAction: selnActions.selectContinuousMetadataAction,
selectCategoricalMetadataAction: selnActions.selectCategoricalMetadataAction,
selectCategoricalAllMetadataAction:
@@ -230,7 +265,11 @@ export default {
annotationRenameLabelInCategory: annoActions.annotationRenameLabelInCategory,
annotationLabelCurrentSelection: annoActions.annotationLabelCurrentSelection,
saveObsAnnotationsAction: annoActions.saveObsAnnotationsAction,
saveGenesetsAction: annoActions.saveGenesetsAction,
needToSaveObsAnnotations: annoActions.needToSaveObsAnnotations,
layoutChoiceAction: embActions.layoutChoiceAction,
setCellSetFromSelection: selnActions.setCellSetFromSelection,
genesetDelete: genesetActions.genesetDelete,
genesetAddGenes: genesetActions.genesetAddGenes,
genesetDeleteGenes: genesetActions.genesetDeleteGenes,
};
-108
View File
@@ -1,108 +0,0 @@
import { API } from "../globals";
import {
postNetworkErrorToast,
postAsyncSuccessToast,
postAsyncFailureToast,
} from "../components/framework/toasters";
import { _switchEmbedding } from "./embedding";
function abortableFetch(request, opts, timeout = 0) {
const controller = new AbortController();
const { signal } = controller;
return {
abort: () => controller.abort(),
isAborted: () => signal.aborted,
ready: () => {
if (timeout) {
setTimeout(() => controller.abort(), timeout);
}
return fetch(request, { ...opts, signal });
},
};
}
async function doReembedFetch(dispatch, getState) {
const state = getState();
let cells = state.annoMatrix.rowIndex.labels();
// These lines ensure that we convert any TypedArray to an Array.
// This is necessary because JSON.stringify() does some very strange
// things with TypedArrays (they are marshalled to JSON objects, rather
// than being marshalled as a JSON array).
cells = Array.isArray(cells) ? cells : Array.from(cells);
const af = abortableFetch(
`${API.prefix}${API.version}layout/obs`,
{
method: "PUT",
headers: new Headers({
Accept: "application/octet-stream",
"Content-Type": "application/json",
}),
body: JSON.stringify({
method: "umap",
filter: { obs: { index: cells } },
}),
credentials: "include",
},
60000 // 1 minute timeout
);
dispatch({
type: "reembed: request start",
abortableFetch: af,
});
const res = await af.ready();
if (res.ok && res.headers.get("Content-Type").includes("application/json")) {
return res;
}
// else an error
let msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`;
const body = await res.text();
if (body && body.length > 0) {
msg = `${msg} -- ${body}`;
}
throw new Error(msg);
}
/*
functions below are dispatch-able
*/
export function requestReembed() {
return async (dispatch, getState) => {
try {
const res = await doReembedFetch(dispatch, getState);
const schema = await res.json();
dispatch({
type: "reembed: request completed",
});
const { annoMatrix: prevAnnoMatrix } = getState();
const base = prevAnnoMatrix.base().addEmbedding(schema);
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
base,
schema.name
);
dispatch({
type: "reembed: add reembedding",
schema,
annoMatrix,
obsCrossfilter,
});
postAsyncSuccessToast("Re-embedding has completed.");
} catch (error) {
dispatch({
type: "reembed: request aborted",
});
if (error.name === "AbortError") {
postAsyncFailureToast("Re-embedding calculation was aborted.");
} else {
postNetworkErrorToast(`Re-embedding: ${error.message}`);
}
console.log("Reembed exception:", error, error.name, error.message);
}
};
}
+119 -126
View File
@@ -1,104 +1,101 @@
/*
Action creators for selection
*/
export const selectContinuousMetadataAction = (
type,
query,
range,
oldProps = {}
) => async (dispatch, getState) => {
const { obsCrossfilter: prevObsCrossfilter } = getState();
export const selectContinuousMetadataAction =
(type, query, range, oldProps = {}) =>
async (dispatch, getState) => {
const { obsCrossfilter: prevObsCrossfilter } = getState();
const selection = range
? {
mode: "range",
lo: range[0],
hi: range[1],
inclusive: true, // [lo, hi] incluisve selection
}
: { mode: "all" };
const selection = range
? {
mode: "range",
lo: range[0],
hi: range[1],
inclusive: true, // [lo, hi] incluisve selection
}
: { mode: "all" };
const obsCrossfilter = await prevObsCrossfilter.select(...query, selection);
const obsCrossfilter = await prevObsCrossfilter.select(...query, selection);
dispatch({
type,
obsCrossfilter,
range,
...oldProps,
});
};
export const selectCategoricalMetadataAction = (
type, // action type
metadataField, // annotation category name
labels,
label, // the label being selected/deselected
isSelected, // bool
oldProps = {}
) => async (dispatch, getState) => {
const {
obsCrossfilter: prevObsCrossfilter,
categoricalSelection,
} = getState();
const labelSelectionState = new Map(categoricalSelection[metadataField]);
labels.forEach(
(l) => labelSelectionState.has(l) || labelSelectionState.set(l, true)
);
labelSelectionState.set(label, isSelected);
const values = Array.from(labelSelectionState.keys()).filter((k) =>
labelSelectionState.get(k)
);
const selection = {
mode: "exact",
values,
dispatch({
type,
obsCrossfilter,
range,
...oldProps,
});
};
const obsCrossfilter = await prevObsCrossfilter.select(
"obs",
metadataField,
selection
);
dispatch({
type,
obsCrossfilter,
metadataField,
labelSelectionState,
...oldProps,
});
};
export const selectCategoricalMetadataAction =
(
type, // action type
metadataField, // annotation category name
labels,
label, // the label being selected/deselected
isSelected, // bool
oldProps = {}
) =>
async (dispatch, getState) => {
const { obsCrossfilter: prevObsCrossfilter, categoricalSelection } =
getState();
export const selectCategoricalAllMetadataAction = (
type, // action type
metadataField, // annotation category name
labels,
isSelected, // bool, select all or none
oldProps = {}
) => async (dispatch, getState) => {
const {
obsCrossfilter: prevObsCrossfilter,
categoricalSelection,
} = getState();
const labelSelectionState = new Map(categoricalSelection[metadataField]);
labels.forEach(
(l) => labelSelectionState.has(l) || labelSelectionState.set(l, true)
);
labelSelectionState.set(label, isSelected);
const labelSelectionState = new Map(categoricalSelection[metadataField]);
labels.forEach((label) => labelSelectionState.set(label, isSelected));
const values = Array.from(labelSelectionState.keys()).filter((k) =>
labelSelectionState.get(k)
);
const selection = {
mode: "exact",
values,
};
const obsCrossfilter = await prevObsCrossfilter.select(
"obs",
metadataField,
selection
);
const selection = { mode: isSelected ? "all" : "none" };
const obsCrossfilter = await prevObsCrossfilter.select(
"obs",
metadataField,
selection
);
dispatch({
type,
obsCrossfilter,
metadataField,
labelSelectionState,
...oldProps,
});
};
dispatch({
type,
obsCrossfilter,
metadataField,
labelSelectionState,
...oldProps,
});
};
export const selectCategoricalAllMetadataAction =
(
type, // action type
metadataField, // annotation category name
labels,
isSelected, // bool, select all or none
oldProps = {}
) =>
async (dispatch, getState) => {
const { obsCrossfilter: prevObsCrossfilter, categoricalSelection } =
getState();
const labelSelectionState = new Map(categoricalSelection[metadataField]);
labels.forEach((label) => labelSelectionState.set(label, isSelected));
const selection = { mode: isSelected ? "all" : "none" };
const obsCrossfilter = await prevObsCrossfilter.select(
"obs",
metadataField,
selection
);
dispatch({
type,
obsCrossfilter,
metadataField,
labelSelectionState,
...oldProps,
});
};
/**
** Graph selection-related actions
@@ -108,25 +105,23 @@ export const graphBrushStartAction = () =>
/* no change to crossfilter until a change fires */
({ type: "graph brush start" });
const _graphBrushWithinRectAction = (type, embName, brushCoords) => async (
dispatch,
getState
) => {
const { obsCrossfilter: prevObsCrossfilter } = getState();
const _graphBrushWithinRectAction =
(type, embName, brushCoords) => async (dispatch, getState) => {
const { obsCrossfilter: prevObsCrossfilter } = getState();
const selection = { mode: "within-rect", ...brushCoords };
const obsCrossfilter = await prevObsCrossfilter.select(
"emb",
embName,
selection
);
const selection = { mode: "within-rect", ...brushCoords };
const obsCrossfilter = await prevObsCrossfilter.select(
"emb",
embName,
selection
);
dispatch({
type,
obsCrossfilter,
brushCoords,
});
};
dispatch({
type,
obsCrossfilter,
brushCoords,
});
};
const _graphAllAction = (type, embName) => async (dispatch, getState) => {
const { obsCrossfilter: prevObsCrossfilter } = getState();
@@ -162,28 +157,26 @@ export const graphLassoCancelAction = (embName) =>
export const graphLassoDeselectAction = (embName) =>
_graphAllAction("graph lasso cancel", embName);
export const graphLassoEndAction = (embName, polygon) => async (
dispatch,
getState
) => {
const { obsCrossfilter: prevObsCrossfilter } = getState();
export const graphLassoEndAction =
(embName, polygon) => async (dispatch, getState) => {
const { obsCrossfilter: prevObsCrossfilter } = getState();
const selection = {
mode: "within-polygon",
polygon,
const selection = {
mode: "within-polygon",
polygon,
};
const obsCrossfilter = await prevObsCrossfilter.select(
"emb",
embName,
selection
);
dispatch({
type: "graph lasso end",
obsCrossfilter,
polygon,
});
};
const obsCrossfilter = await prevObsCrossfilter.select(
"emb",
embName,
selection
);
dispatch({
type: "graph lasso end",
obsCrossfilter,
polygon,
});
};
/*
Differential expression set selection
+2 -4
View File
@@ -42,10 +42,8 @@ export const subsetAction = () => (dispatch, getState) => {
By convention, a clip view is ALWAYS the top view, so if present, pop
off and re-apply
*/
const {
annoMatrix: prevAnnoMatrix,
obsCrossfilter: prevObsCrossfilter,
} = getState();
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
getState();
const annoMatrix = _userSubsetAnnoMatrix(
prevAnnoMatrix,
prevObsCrossfilter.allSelectedMask()
+18 -26
View File
@@ -12,6 +12,7 @@ import {
import { indexEntireSchema } from "../util/stateManager/schemaHelpers";
import { _whereCacheGet, _whereCacheMerge } from "./whereCache";
import _shallowClone from "./clone";
import { _queryValidate, _queryCacheKey } from "./query";
const _dataframeCache = dataframeMemo(128);
@@ -181,7 +182,7 @@ export default class AnnoMatrix {
Returns a Promise for the query result, which will resolve to a dataframe.
Field must be one of the matrix fields: 'obs', 'var', 'X', 'emb'. Value
represents the underlying object upon which the query is occuring.
represents the underlying object upon which the query is occurring.
Query is one of:
* a string, representing a single column name from the field, eg,
@@ -197,12 +198,6 @@ export default class AnnoMatrix {
field/column, similar to a join. Currently only supported on the var
dimension, allowing query of X columns by var value (eg, gene name)
The query filter is a single value filter:
{ "field name": [
{name: "column name", values: [ list of values ]}
]}
One and only one value filter is allowed in a value query.
Examples:
1. Fetch the "n_genes" column the "obs":
@@ -220,7 +215,9 @@ export default class AnnoMatrix {
value "TYMP" in the var index.
fetch("X", {
where: {field: "var", column: this.schema.annotations.var.index, value: "TYMP"}
where: {
field: "var", column: this.schema.annotations.var.index, value: "TYMP"
}
})
In AnnData & Pandas DataFrame API, this is equivalent to:
@@ -410,6 +407,14 @@ export default class AnnoMatrix {
_subclassResponsibility();
}
getCacheKeys(field, query) {
/*
Return cache keys for columns associated with this query. May return
[unknown] if no keys are known (ie, nothing is or was cached).
*/
return _whereCacheGet(this._whereCache, this.schema, field, query);
}
/**
** Private interfaces below.
**/
@@ -427,6 +432,7 @@ export default class AnnoMatrix {
async _fetch(field, q) {
if (!AnnoMatrix.fields().includes(field)) return undefined;
const queries = Array.isArray(q) ? q : [q];
queries.forEach(_queryValidate);
/* find cached columns we need, and GC the rest */
const cachedColumns = this._resolveCachedQueries(field, queries);
@@ -507,7 +513,7 @@ export default class AnnoMatrix {
* obs, var and emb do not grow without bounds, and are needed constantly
for rendering.
a) There is no upside to GC'ing these in the base (loader)
b) The undo/redo cache can hold a large number in views, which is worht GC'ing
b) The undo/redo cache can hold a large number in views, which is worth GC'ing
* X is often much larger than memory, and the UI allows add/del from
this. Most of the GC potential is here in both the base and views.
@@ -522,7 +528,7 @@ export default class AnnoMatrix {
as much of the cache is pinned by that data structure.
*/
_gcField(field, isHot, pinnedColumns) {
const maxColumns = isHot ? 256 : 10; // maybe to aggessive?
const maxColumns = isHot ? 256 : 10; // maybe to aggressive?
const cache = this._cache[field];
if (cache.colIndex.size() < maxColumns) return; // trivial rejection
@@ -590,23 +596,18 @@ export default class AnnoMatrix {
called each time a query is performed, allowing the gc to update any bookkeeping
information. Currently, this is just a simple last-fetched timestamp, stored
in a Map.
Map objects preserve order of insertion. This is leveraged as a cheap way to
do LRU, by removing and re-inserting keys. IMPORTANT: the cleanup code assumes
the map insertion order is least-recently-used first.
*/
const cols = dataframe.colIndex.labels();
const { _gcInfo } = this;
const now = Date.now();
cols.forEach((c) => {
// gcInfo.delete(c);
_gcInfo.set(_columnCacheKey(field, c), now);
});
}
/**
Cloning sublcass protocol - we rely in cloning to preserve immutable
symantics while not causing races or other side effects in internal
Cloning subclass protocol - we rely in cloning to preserve immutable
semantics while not causing races or other side effects in internal
cache management.
Subclasses must override _cloneDeeper() if they have state which requires
@@ -639,15 +640,6 @@ export default class AnnoMatrix {
/*
private utility functions below
*/
function _queryCacheKey(field, query) {
if (typeof query === "object") {
const { field: queryField, column: queryColumn, value: queryValue } = query;
return `${field}/${queryField}/${queryColumn}/${queryValue}`;
}
return `${field}/${query}`;
}
function _columnCacheKey(field, column) {
return `${field}/${column}`;
}
+19 -3
View File
@@ -123,6 +123,24 @@ export default class AnnoMatrixObsCrossfilter {
return new AnnoMatrixObsCrossfilter(annoMatrix, this.obsCrossfilter);
}
/**
* Drop the crossfilter dimension. Do not change the annoMatrix. Useful when we
* want to stop trackin the selection state, but aren't sure we want to blow the
* annomatrix cache.
*/
dropDimension(field, query) {
const { annoMatrix } = this;
let { obsCrossfilter } = this;
const keys = annoMatrix
.getCacheKeys(field, query)
.filter((k) => k !== undefined);
const dimName = _dimensionName(field, keys);
if (obsCrossfilter.hasDimension(dimName)) {
obsCrossfilter = obsCrossfilter.delDimension(dimName);
}
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
}
/**
Selection state - API is identical to ImmutableTypedCrossfilter, as these
are just wrappers to lazy create indices.
@@ -164,9 +182,7 @@ export default class AnnoMatrixObsCrossfilter {
*/
const { annoMatrix } = this;
const currentDims = this.obsCrossfilter.dimensionNames();
const obsCrossfilter = currentDims.reduce((xfltr, dim) => {
return xfltr.select(dim, { mode: "all" });
}, this.obsCrossfilter);
const obsCrossfilter = currentDims.reduce((xfltr, dim) => xfltr.select(dim, { mode: "all" }), this.obsCrossfilter);
return new AnnoMatrixObsCrossfilter(annoMatrix, obsCrossfilter);
}
+2 -4
View File
@@ -1,4 +1,4 @@
export { doBinaryRequest } from "../util/actionHelpers";
export { doBinaryRequest, doFetch } from "../util/actionHelpers";
/* double URI encode - needed for query-param filters */
export function _dubEncURIComp(s) {
@@ -19,9 +19,7 @@ export function _fetchResult(promise) {
}
);
res.status = () => {
return _status;
};
res.status = () => _status;
return res;
}
+76 -37
View File
@@ -1,6 +1,6 @@
import { doBinaryRequest, _dubEncURIComp } from "./fetchHelpers";
import { doBinaryRequest, doFetch } from "./fetchHelpers";
import { matrixFBSToDataframe } from "../util/stateManager/matrix";
import { _getColumnSchema, _normalizeCategoricalSchema } from "./schema";
import { _getColumnSchema } from "./schema";
import {
addObsAnnoColumn,
removeObsAnnoColumn,
@@ -12,6 +12,17 @@ import { isArrayOrTypedArray } from "../util/typeHelpers";
import { _whereCacheCreate } from "./whereCache";
import AnnoMatrix from "./annoMatrix";
import PromiseLimit from "../util/promiseLimit";
import {
_expectSimpleQuery,
_expectComplexQuery,
_urlEncodeLabelQuery,
_urlEncodeComplexQuery,
_hashStringValues,
} from "./query";
import {
normalizeResponse,
normalizeWritableCategoricalSchema,
} from "./normalize";
const promiseThrottle = new PromiseLimit(5);
@@ -115,7 +126,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
data = new Ctor(this.nObs).fill(value);
}
newAnnoMatrix._cache.obs = this._cache.obs.withCol(colName, data);
_normalizeCategoricalSchema(
normalizeWritableCategoricalSchema(
colSchema,
newAnnoMatrix._cache.obs.col(colName)
);
@@ -223,27 +234,23 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
/*
_doLoad - evaluates the query against the field. Returns:
* whereCache update: column query map mapping the query to the column labels
* Dataframe containing the new colums (one per dimension)
* Dataframe containing the new columns (one per dimension)
*/
let urlQuery;
let urlBase;
let doRequest;
let priority = 10; // default fetch priority
switch (field) {
case "obs":
case "var": {
urlBase = `${this.baseURL}annotations/${field}`;
urlQuery = _encodeQuery("annotation-name", query);
doRequest = _obsOrVarLoader(this.baseURL, field, query);
break;
}
case "X": {
urlBase = `${this.baseURL}data/var`;
urlQuery = _encodeQuery(undefined, query);
doRequest = _XLoader(this.baseURL, field, query);
break;
}
case "emb": {
urlBase = `${this.baseURL}layout/obs`;
urlQuery = _encodeQuery("layout-name", query);
doRequest = _embLoader(this.baseURL, field, query);
priority = 0; // high prio load for embeddings
break;
}
@@ -251,13 +258,8 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
throw new Error("Unknown field name");
}
const url = `${urlBase}?${urlQuery}`;
const buffer = await promiseThrottle.priorityAdd(
priority,
doBinaryRequest,
url
);
const result = matrixFBSToDataframe(buffer);
const buffer = await promiseThrottle.priorityAdd(priority, doRequest);
let result = matrixFBSToDataframe(buffer);
if (!result || result.isEmpty()) throw Error("Unknown field/col");
const whereCacheUpdate = _whereCacheCreate(
@@ -266,13 +268,7 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
result.colIndex.labels()
);
if (field === "obs") {
/* cough, cough - see comment on method */
_normalizeCategoricalSchema(
this.schema.annotations.obsByName[query],
result.col(query)
);
}
result = normalizeResponse(field, query, this.schema, result);
return [whereCacheUpdate, result];
}
@@ -282,17 +278,6 @@ export default class AnnoMatrixLoader extends AnnoMatrix {
Utility functions below
*/
function _encodeQuery(colKey, q) {
if (typeof q === "object") {
const { field: queryField, column: queryColumn, value: queryValue } = q;
return `${_dubEncURIComp(queryField)}:${_dubEncURIComp(
queryColumn
)}=${_dubEncURIComp(queryValue)}`;
}
if (!colKey) throw new Error("Unsupported query by name");
return `${colKey}=${encodeURIComponent(q)}`;
}
function _writableCheck(colSchema) {
if (!colSchema?.writable) {
throw new Error("Unknown or readonly obs column");
@@ -305,3 +290,57 @@ function _writableCategoryTypeCheck(colSchema) {
throw new Error("column must be categorical");
}
}
function _embLoader(baseURL, _field, query) {
_expectSimpleQuery(query);
const urlBase = `${baseURL}layout/obs`;
const urlQuery = _urlEncodeLabelQuery("layout-name", query);
const url = `${urlBase}?${urlQuery}`;
return () => doBinaryRequest(url);
}
function _obsOrVarLoader(baseURL, field, query) {
_expectSimpleQuery(query);
const urlBase = `${baseURL}annotations/${field}`;
const urlQuery = _urlEncodeLabelQuery("annotation-name", query);
const url = `${urlBase}?${urlQuery}`;
return () => doBinaryRequest(url);
}
function _XLoader(baseURL, field, query) {
_expectComplexQuery(query);
if (query.where) {
const urlBase = `${baseURL}data/var`;
const urlQuery = _urlEncodeComplexQuery(query);
const url = `${urlBase}?${urlQuery}`;
return () => doBinaryRequest(url);
}
if (query.summarize) {
const urlBase = `${baseURL}summarize/var`;
const urlQuery = _urlEncodeComplexQuery(query);
if (urlBase.length + urlQuery.length < 2000) {
const url = `${urlBase}?${urlQuery}`;
return () => doBinaryRequest(url);
}
const url = `${urlBase}?key=${_hashStringValues([urlQuery])}`;
return async () => {
const res = await doFetch(url, {
method: "POST",
body: urlQuery,
headers: new Headers({
Accept: "application/octet-stream",
"Content-Type": "application/x-www-form-urlencoded",
}),
});
return res.arrayBuffer();
};
}
throw new Error("Unknown query structure");
}
+150
View File
@@ -0,0 +1,150 @@
import { _getColumnSchema, _isIndex } from "./schema";
import catLabelSort from "../util/catLabelSort";
import {
unassignedCategoryLabel,
overflowCategoryLabel,
globalConfig,
} from "../globals";
export function normalizeResponse(field, query, schema, response) {
/**
* There are a number of assumptions in the front-end about data typing and data
* characteristics. This routine will normalize a server response dataframe
* to match front-end expectations and UI conventions. This includes cast/transform
* of the data and schema updates.
*
* This consolidates all assumptions into one location, for ease of update.
*
* Currently, this includes normalization for obs/var columns only:
*
* - Dataframe columns in var/obs that are declared type: boolean may be sent by
* the server in a variety of formats (eg uint8, etc). Cast to JS Array[boolean]
*
* - "Categorical" columns may not have all categories represented in the server-provided
* schema (for valid reasons, eg, floating point rounding differences). For all
* types we treat as categorical in the UI (string, boolean, categorical), update
* the schema to contain all categories as a convenience.
*
* - "Categorical" columns (ie, string, boolean, categorical) may contain an excess
* of category values (aka labels). Consolidate any excess into an "all other"
* category.
*/
// currently no data or schema normalization necessary for X or emb
if (field !== "obs" && field !== "var") return response;
const colLabels = response.colIndex.labels();
for (const colLabel of colLabels) {
const colSchema = _getColumnSchema(schema, field, colLabel);
const isIndex = _isIndex(schema, field, colLabel);
const { type, writable } = colSchema;
// Boolean data -- cast entire array to Array[bool]
if (type === "boolean") {
response = castColumnToBoolean(response, colLabel);
}
// Types that are categorical in UI (string, boolean, categorical) OR are writable
// are introspected to ensure the schema `categories` field and data values match,
// and that we do not have an excess of category values (for non-writable columns)
const isEnumType =
type === "boolean" ||
type === "string" ||
type === "categorical" ||
writable;
if (!isIndex && isEnumType) {
response = normalizeCategorical(response, colLabel, colSchema);
}
}
return response;
}
function castColumnToBoolean(df, label) {
const colData = df.col(label).asArray();
const newColData = new Array(colData.length);
for (let i = 0; i < colData.length; i += 1) newColData[i] = !!colData[i];
df = df.replaceColData(label, newColData);
return df;
}
export function normalizeWritableCategoricalSchema(colSchema, col) {
/*
Ensure all enum writable / categorical schema have a categories array, that
the categories array contains all unique values in the data array, AND that
the array is UI sorted.
*/
const categorySet = new Set(
col.summarizeCategorical().categories.concat(colSchema.categories ?? [])
);
if (!categorySet.has(unassignedCategoryLabel)) {
categorySet.add(unassignedCategoryLabel);
}
colSchema.categories = catLabelSort(true, Array.from(categorySet));
return colSchema;
}
export function normalizeCategorical(df, colLabel, colSchema) {
/*
If writable, ensure schema matches data and we have an unassigned label
If not writable, ensure schema matches data and that we consolidate labels in excess
of "top N" into an overflow labels.
*/
const { writable } = colSchema;
const col = df.col(colLabel);
if (writable) {
// writable (aka user) annotations
normalizeWritableCategoricalSchema(colSchema, col);
return df;
}
// else read-only, categorical columns
const TopN = globalConfig.maxCategoricalOptionsToDisplay;
// consolidate all categories from data and schema into a single list
const colDataSummary = col.summarizeCategorical();
const allCategories = new Set(
colDataSummary.categories.concat(colSchema.categories ?? [])
);
// if no overflow, just UI sort schema categories and return
if (allCategories.size <= TopN) {
colSchema.categories = catLabelSort(writable, [...allCategories.keys()]);
return df;
}
// Otherwise, pick top N categories by count and rewrite data
// choose unique overflow category label
let overflowCatName = `${colLabel}${overflowCategoryLabel}`;
while (allCategories.has(overflowCatName)) {
overflowCatName += "_";
}
// pick top N category labels and add overflow label
const topNCategories = new Set(
[...colDataSummary.categoryCounts.keys()].slice(0, TopN)
);
topNCategories.add(overflowCatName);
// rewrite data - consolidate all excess labels into overflow label
const newColData = Array.from(col.asArray());
for (let i = 0; i < newColData.length; i += 1) {
if (!topNCategories.has(newColData[i])) {
newColData[i] = overflowCatName;
}
}
// replace data in dataframe
df = df.replaceColData(colLabel, newColData);
// Update schema with categories, in UI sort order. Ensure overflow label is at end
// of list for display purposes.
const revisedCategories = df.col(colLabel).summarizeCategorical().categories;
revisedCategories.push(
revisedCategories.splice(revisedCategories.indexOf(overflowCatName), 1)[0]
);
colSchema.categories = catLabelSort(writable, revisedCategories);
return df;
}
+126
View File
@@ -0,0 +1,126 @@
import sha1 from "sha1";
import { _dubEncURIComp } from "./fetchHelpers";
/**
* Query utilities, mostly for debugging support and validation.
*/
/**
* Normalize & error check the query.
* @param {object | string} query - the query
* @returns {object | string} - the normalized query
*/
export function _queryValidate(query) {
if (typeof query !== "object") return query;
if (query.where && query.summarize)
throw new Error("query may not specify both where and summarize");
if (query.where) {
const {
field: queryField,
column: queryColumn,
value: queryValue,
} = query.where;
if (!queryField || !queryColumn || !queryValue)
throw new Error("Incomplete where query");
return query;
}
if (query.summarize) {
const {
field: queryField,
column: queryColumn,
values: queryValues,
} = query.summarize;
if (!queryField || !queryColumn || !queryValues)
throw new Error("Incomplete where query");
if (!Array.isArray(queryValues))
throw new Error("Summarize query values must be an array");
return query;
}
throw new Error("query must specify one of where or summarize");
}
export function _expectSimpleQuery(query) {
if (typeof query === "object") throw new Error("expected simple query");
}
export function _expectComplexQuery(query) {
if (typeof query !== "object") throw new Error("expected complex query");
}
/**
* Generate a unique key which can be used to reference this query.
*
* @param {string} field
* @param {string|object} query
* @returns the key
*/
export function _queryCacheKey(field, query) {
if (typeof query === "object") {
// complex query
if (query.where) {
const {
field: queryField,
column: queryColumn,
value: queryValue,
} = query.where;
return `${field}/${queryField}/${queryColumn}/${queryValue}`;
}
if (query.summarize) {
const {
method,
field: queryField,
column: queryColumn,
values: queryValues,
} = query.summarize;
return `${field}/${method}/${queryField}/${queryColumn}/${queryValues.join(
","
)}`;
}
throw new Error("Unrecognized complex query type");
}
// simple query
return `${field}/${query}`;
}
function _urlEncodeWhereQuery(q) {
const { field: queryField, column: queryColumn, value: queryValue } = q;
return `${_dubEncURIComp(queryField)}:${_dubEncURIComp(
queryColumn
)}=${_dubEncURIComp(queryValue)}`;
}
function _urlEncodeSummarizeQuery(q) {
const { method, field, column, values } = q;
const filter = values
.map((value) => _urlEncodeWhereQuery({ field, column, value }))
.join("&");
return `method=${method}&${filter}`;
}
export function _urlEncodeComplexQuery(q) {
if (typeof q === "object") {
if (q.where) {
return _urlEncodeWhereQuery(q.where);
}
if (q.summarize) {
return _urlEncodeSummarizeQuery(q.summarize);
}
}
throw new Error("Unrecognized complex query type");
}
export function _urlEncodeLabelQuery(colKey, q) {
if (!colKey) throw new Error("Unsupported query by name");
if (typeof q !== "string") throw new Error("Query must be a simple label.");
return `${colKey}=${encodeURIComponent(q)}`;
}
/**
* Generate the column key the server will send us for this query.
*/
export function _hashStringValues(arrayOfString) {
const hash = sha1(arrayOfString.join(""));
return hash;
}
+6 -27
View File
@@ -1,9 +1,6 @@
/*
Private helper functions related to schema
*/
import catLabelSort from "../util/catLabelSort";
import { unassignedCategoryLabel } from "../globals";
export function _getColumnSchema(schema, field, col) {
/* look up the column definition */
switch (field) {
@@ -26,10 +23,15 @@ export function _getColumnSchema(schema, field, col) {
}
}
export function _isIndex(schema, field, col) {
const index = schema.annotations?.[field].index;
return index && index === col;
}
export function _getColumnDimensionNames(schema, field, col) {
/*
field/col may be an alias for multiple columns. Currently used to map ND
values to 1D dataframe columns for embeddings/layout. Signfied by the presence
values to 1D dataframe columns for embeddings/layout. Signified by the presence
of the "dims" value in the schema.
*/
const colSchema = _getColumnSchema(schema, field, col);
@@ -63,26 +65,3 @@ export function _isContinuousType(schema) {
const { type } = schema;
return !(type === "string" || type === "boolean" || type === "categorical");
}
export function _normalizeCategoricalSchema(colSchema, col) {
const { type, writable } = colSchema;
if (
type === "string" ||
type === "boolean" ||
type === "categorical" ||
writable
) {
const categorySet = new Set(
col.summarizeCategorical().categories.concat(colSchema.categories ?? [])
);
if (writable && !categorySet.has(unassignedCategoryLabel)) {
categorySet.add(unassignedCategoryLabel);
}
colSchema.categories = Array.from(categorySet);
}
if (colSchema.categories) {
colSchema.categories = catLabelSort(writable, colSchema.categories);
}
return colSchema;
}
+4 -3
View File
@@ -58,10 +58,11 @@ function _maskToList(mask) {
if (!mask) {
return null;
}
const list = new Int32Array(mask.length);
const [...m] = mask;
const list = new Int32Array(m.length);
let elems = 0;
for (let i = 0, l = mask.length; i < l; i += 1) {
if (mask[i]) {
for (let i = 0, l = m.length; i < l; i += 1) {
if (m[i]) {
list[elems] = i;
elems += 1;
}
+134 -52
View File
@@ -1,50 +1,88 @@
/*
Private support functions.
Support for a "where" query, eg,
This implements a query resolver cache, mapping a query onto the column labels
resolved by that query. These labels are then used to manage the acutal data cache,
which stores data by the resolved label.
{ where: { field: "var", column: "gene", value: "FOXP2" }}
There are three query forms:
* primitive (string, number) - which is just reference the column label of same value
* where query (object) - eg, { where: { field: "var", column: "gene", value: "FOXP2" }}
* summary query (object) - eg, { summarize: { method: "mean", field: "var", column: "gene", values: ["FOXP2", "GNE", "F5"]}}
These evaluate to a given column label.
These queries all resolve to one or more column labels on a field. This
cache maintains a record of this, allowing direct access to the data caches
without a server round-trip.
The "where cache" is a map that saves evaluated queries and points
to the column label they resolve to.
The data structure for where queries, the following query against X as an example:
{ where: { field: "var", column: "column_label_in_var", value: "value_in_var_column" } }
results in the following cached entry:
{
where: {
X: {
var: Map(
column_label_in_var => Map(
value_in_var_column => [column_label_in_X, ...]
)
)
}
},
summarize: {},
}
Data structure, using X as the example field being queried, and var as
the index.
{
X: {
var: Map(
column_label_in_var => Map(value_in_var_column => [column_label_in_X, ...])
)
}
And for summarize queries, for the following summary on X:
{ summarize: { method: "mean", field: "var", column: "gene", values: ["G1", "G2"]}}
creates a cache entry of:
{
where: {},
summarize: {
X: {
mean: {
var: Map(
"gene" => Map(
"G1,G2" => [summary_column_label, ...]
)
)
}
},
},
}
*/
import { _getColumnDimensionNames } from "./schema";
import { _hashStringValues } from "./query";
export function _whereCacheGet(whereCache, schema, field, query) {
/*
/*
query will either be an where query (object) or a column name (string).
Return array of column labels or undefined.
*/
if (typeof query === "object") {
const { field: queryField, column: queryColumn, value: queryValue } = query;
const columnMap = whereCache?.[field]?.[queryField];
if (columnMap === undefined) return [undefined];
const valueMap = columnMap.get(queryColumn);
if (valueMap === undefined) return [undefined];
const columnLabels = valueMap.get(queryValue);
return columnLabels === undefined ? [undefined] : columnLabels;
if (query.where) {
const {
field: queryField,
column: queryColumn,
value: queryValue,
} = query.where;
const columnMap = whereCache?.where?.[field]?.[queryField];
return columnMap?.get(queryColumn)?.get(queryValue) ?? [undefined];
}
if (query.summarize) {
const {
method,
field: queryField,
column: queryColumn,
values: queryValues,
} = query.summarize;
const columnMap = whereCache?.summarize?.[field]?.[method]?.[queryField];
const queryValueHash = _hashStringValues(queryValues);
return columnMap?.get(queryColumn)?.get(queryValueHash) ?? [undefined];
}
return [undefined];
}
const colDims = _getColumnDimensionNames(schema, field, query);
return colDims === undefined ? [undefined] : colDims;
return _getColumnDimensionNames(schema, field, query) ?? [undefined];
}
export function _whereCacheCreate(field, query, columnLabels) {
@@ -53,40 +91,84 @@ export function _whereCacheCreate(field, query, columnLabels) {
*/
if (typeof query !== "object") return null;
const { field: queryField, column: queryColumn, value: queryValue } = query;
const whereCache = {
[field]: {
[queryField]: new Map([
[queryColumn, new Map([[queryValue, columnLabels]])],
]),
},
};
return whereCache;
const { where, summarize } = query;
if (where) {
const { field: queryField, column: queryColumn, value: queryValue } = where;
return {
where: {
[field]: {
[queryField]: new Map([
[queryColumn, new Map([[queryValue, columnLabels]])],
]),
},
},
};
}
if (summarize) {
const {
method,
field: queryField,
column: queryColumn,
values: queryValues,
} = summarize;
const queryValueHash = _hashStringValues(queryValues);
return {
summarize: {
[field]: {
[method]: {
[queryField]: new Map([
[queryColumn, new Map([[queryValueHash, columnLabels]])],
]),
},
},
},
};
}
// oops, not sure what that query is!
return {};
}
function __mergeQueries(dst, src) {
for (const [queryField, columnMap] of Object.entries(src)) {
dst[queryField] = dst[queryField] || new Map();
for (const [queryColumn, valueMap] of columnMap) {
if (!dst[queryField].has(queryColumn))
dst[queryField].set(queryColumn, new Map());
for (const [queryValue, columnLabels] of valueMap) {
dst[queryField].get(queryColumn).set(queryValue, columnLabels);
}
}
}
}
function __whereCacheMerge(dst, src) {
/*
merge src into dst (modifies dst)
*/
if (!dst) dst = {};
if (!src || typeof src !== "object") return dst;
Object.entries(src).forEach(([field, query]) => {
if (!Object.prototype.hasOwnProperty.call(dst, field)) dst[field] = {};
Object.entries(query).forEach(([queryField, columnMap]) => {
if (!Object.prototype.hasOwnProperty.call(dst[field], queryField))
dst[field][queryField] = new Map();
columnMap.forEach((valueMap, queryColumn) => {
if (!dst[field][queryField].has(queryColumn))
dst[field][queryField].set(queryColumn, new Map());
valueMap.forEach((columnLabels, queryValue) => {
dst[field][queryField].get(queryColumn).set(queryValue, columnLabels);
});
});
});
});
if (src.where) {
dst.where = dst.where || {};
for (const [field, query] of Object.entries(src.where)) {
dst.where[field] = dst.where[field] || {};
__mergeQueries(dst.where[field], query);
}
}
if (src.summarize) {
dst.summarize = dst.summarize || {};
for (const [field, method] of Object.entries(src.summarize)) {
dst.summarize[field] = dst.summarize[field] || {};
for (const [methodName, query] of Object.entries(method)) {
dst.summarize[field][methodName] =
dst.summarize[field][methodName] || {};
__mergeQueries(dst.summarize[field][methodName], query);
}
}
}
return dst;
}
export function _whereCacheMerge(...caches) {
return caches.reduce((dst, src) => __whereCacheMerge(dst, src), {});
// eslint-disable-next-line compat/compat -- not using web APIs
return caches.reduce(__whereCacheMerge, {});
}
@@ -18,6 +18,8 @@ class AnnoDialog extends React.PureComponent {
validationError,
annoSelect,
annoInput,
secondaryInstructions,
secondaryInput,
handleCancel,
handleSubmit,
primaryButtonText,
@@ -40,12 +42,19 @@ class AnnoDialog extends React.PureComponent {
<p
style={{
marginTop: 7,
visibility: validationError ? "visible" : "hidden",
visibility: errorMessage !== "" ? "visible" : "hidden",
color: Colors.ORANGE3,
}}
>
{errorMessage}
</p>
{/* we might rename, secondary button and secondary input are not related */}
{secondaryInstructions && (
<p style={{ marginTop: secondaryInstructions ? 20 : 0 }}>
{secondaryInstructions}
</p>
)}
{secondaryInput || null}
</div>
{annoSelect || null}
</div>
@@ -54,6 +63,7 @@ class AnnoDialog extends React.PureComponent {
<Tooltip content={cancelTooltipContent}>
<Button onClick={handleCancel}>Cancel</Button>
</Tooltip>
{/* we might rename, secondary button and secondary input are not related */}
{handleSecondaryButtonSubmit && secondaryButtonText ? (
<Button
onClick={handleSecondaryButtonSubmit}
+1 -4
View File
@@ -1,4 +1,3 @@
// jshint esversion: 6
import React from "react";
import Helmet from "react-helmet";
import { connect } from "react-redux";
@@ -12,7 +11,6 @@ import Graph from "./graph/graph";
import MenuBar from "./menubar";
import Autosave from "./autosave";
import Embedding from "./embedding";
import TermsOfServicePrompt from "./termsPrompt";
import actions from "../actions";
@@ -43,7 +41,7 @@ class App extends React.Component {
const { loading, error, graphRenderCounter } = this.props;
return (
<Container>
<Helmet title="cellxgene" />
<Helmet title="CELL&times;GENE | Annotate" />
{loading ? (
<div
style={{
@@ -76,7 +74,6 @@ class App extends React.Component {
<MenuBar />
<Embedding />
<Autosave />
<TermsOfServicePrompt />
<Legend viewportRef={viewportRef} />
<Graph key={graphRenderCounter} viewportRef={viewportRef} />
</>
@@ -3,18 +3,21 @@ import { connect } from "react-redux";
import {
Button,
Tooltip,
InputGroup,
Dialog,
Classes,
Code,
Colors,
Dialog,
InputGroup,
Tooltip,
} from "@blueprintjs/core";
@connect((state) => ({
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
annotations: state.annotations,
auth: state.config?.authentication,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
writableGenesetsEnabled: !(
state.config?.parameters?.annotations_genesets_readonly ?? true
),
}))
class FilenameDialog extends React.Component {
constructor(props) {
@@ -91,16 +94,21 @@ class FilenameDialog extends React.Component {
};
render() {
const { writableCategoriesEnabled, annotations, idhash, auth } = this.props;
const {
writableCategoriesEnabled,
writableGenesetsEnabled,
annotations,
idhash,
} = this.props;
const { filenameText } = this.state;
return writableCategoriesEnabled &&
return (writableCategoriesEnabled || writableGenesetsEnabled) &&
annotations.promptForFilename &&
!annotations.dataCollectionNameIsReadOnly &&
!annotations.dataCollectionName &&
auth.is_authenticated ? (
!annotations.dataCollectionName ? (
<Dialog
icon="tag"
title="Annotations Collection"
title="User Generated Data Directory"
isOpen={!annotations.dataCollectionName}
onClose={this.dismissFilenameDialog}
>
@@ -112,7 +120,7 @@ class FilenameDialog extends React.Component {
>
<div className={Classes.DIALOG_BODY} data-testid="annotation-dialog">
<div style={{ marginBottom: 20 }}>
<p>Name your annotations collection:</p>
<p>Name your user generated data directory:</p>
<InputGroup
autoFocus
value={filenameText}
@@ -137,10 +145,16 @@ class FilenameDialog extends React.Component {
</div>
<div>
<p>
Your annotations are stored in this file:
<code className="bp3-code">
{filenameText}-{idhash}.csv
</code>
{"Your annotations are stored in this file: "}
<Code>
{filenameText}-cell-labels-{idhash}.csv
</Code>
</p>
<p>
{"Your gene sets are stored in this file: "}
<Code>
{filenameText}-gene-sets-{idhash}.csv
</Code>
</p>
<p style={{ fontStyle: "italic" }}>
(We added a unique ID to your filename)
@@ -159,7 +173,7 @@ class FilenameDialog extends React.Component {
type="submit"
data-testid="submit-annotation"
>
Create annotations collection
Create user generated data directory
</Button>
</div>
</div>
+36 -10
View File
@@ -5,11 +5,18 @@ import FilenameDialog from "./filenameDialog";
@connect((state) => ({
annotations: state.annotations,
saveInProgress: state.autosave?.saveInProgress ?? false,
obsAnnotationSaveInProgress:
state.autosave?.obsAnnotationSaveInProgress ?? false,
genesetSaveInProgress: state.autosave?.genesetSaveInProgress ?? false,
error: state.autosave?.error,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
writableGenesetsEnabled: !(
state.config?.parameters?.annotations_genesets_readonly ?? true
),
annoMatrix: state.annoMatrix,
genesets: state.genesets,
lastSavedAnnoMatrix: state.autosave?.lastSavedAnnoMatrix,
lastSavedGenesets: state.autosave?.lastSavedGenesets,
}))
class Autosave extends React.Component {
constructor(props) {
@@ -20,11 +27,11 @@ class Autosave extends React.Component {
}
componentDidMount() {
const { writableCategoriesEnabled } = this.props;
const { writableCategoriesEnabled, writableGenesetsEnabled } = this.props;
let { timer } = this.state;
if (timer) clearInterval(timer);
if (writableCategoriesEnabled) {
if (writableCategoriesEnabled || writableGenesetsEnabled) {
timer = setInterval(this.tick, 2500);
} else {
timer = null;
@@ -38,18 +45,37 @@ class Autosave extends React.Component {
}
tick = () => {
const { dispatch, saveInProgress } = this.props;
if (this.needToSave() && !saveInProgress) {
const { dispatch, obsAnnotationSaveInProgress, genesetSaveInProgress } =
this.props;
if (!obsAnnotationSaveInProgress && this.needToSaveObsAnnotations()) {
dispatch(actions.saveObsAnnotationsAction());
}
if (!genesetSaveInProgress && this.needToSaveGenesets()) {
dispatch(actions.saveGenesetsAction());
}
};
needToSave = () => {
/* return true if we need to save, false if we don't */
needToSaveObsAnnotations = () => {
/* return true if we need to save obs cell labels, false if we don't */
const { annoMatrix, lastSavedAnnoMatrix } = this.props;
return actions.needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix);
};
needToSaveGenesets = () => {
/* return true if we need to save gene ses, false if we do not */
const { genesets, lastSavedGenesets } = this.props;
return genesets.initialized && genesets.genesets !== lastSavedGenesets;
};
needToSave() {
return this.needToSaveGenesets() || this.needToSaveObsAnnotations();
}
saveInProgress() {
const { obsAnnotationSaveInProgress, genesetSaveInProgress } = this.props;
return obsAnnotationSaveInProgress || genesetSaveInProgress;
}
statusMessage() {
const { error } = this.props;
if (error) {
@@ -61,12 +87,12 @@ class Autosave extends React.Component {
render() {
const {
writableCategoriesEnabled,
saveInProgress,
writableGenesetsEnabled,
lastSavedAnnoMatrix,
} = this.props;
const initialDataLoadComplete = lastSavedAnnoMatrix;
if (!writableCategoriesEnabled) return null;
if (!writableCategoriesEnabled && !writableGenesetsEnabled) return null;
return (
<div
@@ -74,7 +100,7 @@ class Autosave extends React.Component {
data-testclass={
!initialDataLoadComplete
? "autosave-init"
: this.needToSave() || saveInProgress
: this.saveInProgress() || this.needToSave()
? "autosave-incomplete"
: "autosave-complete"
}
@@ -0,0 +1,15 @@
import React from "react";
import * as globals from "../../globals";
const ErrorLoading = ({ displayName, zebra }) => (
<div
style={{
backgroundColor: zebra ? globals.lightestGrey : "white",
fontStyle: "italic",
}}
>
<span>{`Failure loading ${displayName}`}</span>
</div>
);
export default ErrorLoading;
@@ -0,0 +1,63 @@
import React from "react";
const HistogramFooter = React.memo(
({
displayName,
hideRanges,
rangeMin,
rangeMax,
rangeColorMin,
rangeColorMax,
isObs,
isGeneSetSummary,
}) =>
/*
Footer of each histogram. Will render range and title.
Required props:
* displayName - the displayName, aka "n_genes", "FOXP2", etc.
* hideRanges - true/false, enables/disable rendering of ranges
* range - length two array, [min, max], containing the range values to display
* rangeColor - length two array, [mincolor, maxcolor], each a CSS color
*/
(
<div>
<div
style={{
display: "flex",
justifyContent: hideRanges ? "center" : "space-between",
}}
>
<span
style={{
color: rangeColorMin,
display: hideRanges ? "none" : "block",
}}
>
min {rangeMin.toPrecision(4)}
</span>
<span
data-testclass="brushable-histogram-field-name"
style={{ fontStyle: "italic" }}
>
{isObs && displayName}
{isGeneSetSummary && "gene set mean expression"}
</span>
<div style={{ display: hideRanges ? "block" : "none" }}>
: {rangeMin}
</div>
<span
style={{
color: rangeColorMax,
display: hideRanges ? "none" : "block",
}}
>
max {rangeMax.toPrecision(4)}
</span>
</div>
</div>
)
);
export default HistogramFooter;
@@ -0,0 +1,100 @@
import React, { useCallback } from "react";
import { Button, ButtonGroup, Tooltip, Icon } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../globals";
const HistogramHeader = React.memo(
({
fieldId,
isColorBy,
onColorByClick,
onRemoveClick,
isScatterPlotX,
isScatterPlotY,
onScatterPlotXClick,
onScatterPlotYClick,
isObs,
}) => {
/*
Render the toolbar for the histogram. Props:
* fieldId - field identifier, used for various IDs
* isColorBy - true/false, is this the current color-by
* onColorByClick - color-by click handler
* onRemoveClick - optional handler for remove. Button will not render if not defined.
* isScatterPlotX - optional, true/false if currently the X scatterplot field
* isScatterPlotY - optional, true/false if currently the Y scatterplot field
* onScatterPlotXClick - optional, handler for scatterPlot X button.
* onScatterPlotYClick - optional, handler for scatterPlot X button.
Scatterplot controls will not render if either handler unspecified.
*/
const memoizedColorByCallback = useCallback(
() => onColorByClick(fieldId, isObs),
[fieldId, isObs]
);
return (
<div
style={{
display: "flex",
justifyContent: "flex-end",
paddingBottom: "8px",
}}
>
{onScatterPlotXClick && onScatterPlotYClick ? (
<span>
<Icon icon={IconNames.SCATTER_PLOT} style={{ marginRight: 7 }} />
<ButtonGroup style={{ marginRight: 7 }}>
<Button
data-testid={`plot-x-${fieldId}`}
onClick={onScatterPlotXClick}
active={isScatterPlotX}
intent={isScatterPlotX ? "primary" : "none"}
>
plot x
</Button>
<Button
data-testid={`plot-y-${fieldId}`}
onClick={onScatterPlotYClick}
active={isScatterPlotY}
intent={isScatterPlotY ? "primary" : "none"}
>
plot y
</Button>
</ButtonGroup>
</span>
) : null}
{onRemoveClick ? (
<Button
minimal
onClick={onRemoveClick}
style={{
color: globals.blue,
cursor: "pointer",
marginLeft: 7,
}}
>
remove
</Button>
) : null}
<Tooltip
content="Use as color scale"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<Button
onClick={memoizedColorByCallback}
active={isColorBy}
intent={isColorBy ? "primary" : "none"}
data-testclass="colorby"
data-testid={`colorby-${fieldId}`}
icon="tint"
/>
</Tooltip>
</div>
);
}
);
export default HistogramHeader;
@@ -0,0 +1,191 @@
import React, { useEffect, useRef, useState } from "react";
import { interpolateCool } from "d3-scale-chromatic";
import * as d3 from "d3";
import maybeScientific from "../../util/maybeScientific";
import clamp from "../../util/clamp";
const Histogram = ({
field,
fieldForId,
display,
histogram,
width,
height,
onBrush,
onBrushEnd,
margin,
isColorBy,
selectionRange,
mini,
}) => {
const svgRef = useRef(null);
const [brush, setBrush] = useState(null);
useEffect(() => {
/*
Create the d3 histogram
*/
// This is just a constant that's flipped by parent's `mini` boolean
const {
LEFT: marginLeft,
RIGHT: marginRight,
BOTTOM: marginBottom,
TOP: marginTop,
} = margin;
const { x, y, bins, binStart, binEnd, binWidth } = histogram;
const svg = d3.select(svgRef.current);
const binPadding = mini ? 0 : -1;
const defaultBarColor = mini ? "black" : "#bbb";
/* Remove everything */
svg.selectAll("*").remove();
/* Set margins within the SVG */
const container = svg
.attr("width", width + marginLeft + marginRight)
.attr("height", height + marginTop + marginBottom)
.append("g")
.attr("class", "histogram-container")
.attr("transform", `translate(${marginLeft},${marginTop})`);
const colorScale = d3
.scaleSequential(interpolateCool)
.domain([0, bins.length]);
const histogramScale = d3
.scaleLinear()
.domain(x.domain())
.range([
colorScale.domain()[1],
colorScale.domain()[0],
]); /* we flip this to make colors dark if high in the color scale */
if (binWidth > 0) {
/* BINS */
container
.insert("g", "*")
.selectAll("rect")
.data(bins)
.enter()
.append("rect")
.attr("x", (d, i) => x(binStart(i)) + 1)
.attr("y", (d) => y(d))
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - binPadding)
.attr("height", (d) => y(0) - y(d))
.style(
"fill",
isColorBy
? (d, i) => colorScale(histogramScale(binStart(i)))
: defaultBarColor
);
}
if (!mini) {
// BRUSH
// Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
const brushX = d3
.brushX()
.extent([
[x.range()[0], y.range()[1]],
[x.range()[1], marginTop + height + marginBottom],
])
/*
emit start so that the Undoable history can save an undo point
upon drag start, and ignore the subsequent intermediate drag events.
*/
.on("start", onBrush(field, x.invert, "start"))
.on("brush", onBrush(field, x.invert, "brush"))
.on("end", onBrushEnd(field, x.invert));
const brushXselection = container
.insert("g")
.attr("class", "brush")
.attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
.call(brushX);
/* X AXIS */
container
.insert("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${marginTop + height})`)
.call(
d3
.axisBottom(x)
.ticks(4)
.tickFormat(d3.format(maybeScientific(x)))
);
/* Y AXIS */
container
.insert("g")
.attr("class", "axis axis--y")
.attr("transform", `translate(${marginLeft + width},0)`)
.call(
d3
.axisRight(y)
.ticks(3)
.tickFormat(
d3.format(
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
)
)
);
/* axis style */
svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
setBrush({ brushX, brushXselection });
}
}, [histogram, isColorBy]);
useEffect(() => {
/*
paint/update selection brush
*/
if (!brush) return;
const { brushX, brushXselection } = brush;
const selection = d3.brushSelection(brushXselection.node());
if (!selectionRange && selection) {
/* no active selection - clear brush */
brushXselection.call(brushX.move, null);
} else if (selectionRange) {
const { x, domain } = histogram;
const [min, max] = domain;
const x0 = x(clamp(selectionRange[0], [min, max]));
const x1 = x(clamp(selectionRange[1], [min, max]));
if (!selection) {
/* there is an active selection, but no brush - set the brush */
brushXselection.call(brushX.move, [x0, x1]);
} else {
/* there is an active selection and a brush - make sure they match */
const moveDeltaThreshold = 1;
const dX0 = Math.abs(x0 - selection[0]);
const dX1 = Math.abs(x1 - selection[1]);
/*
only update the brush if it is grossly incorrect,
as defined by the moveDeltaThreshold
*/
if (dX0 > moveDeltaThreshold || dX1 > moveDeltaThreshold) {
brushXselection.call(brushX.move, [x0, x1]);
}
}
}
}, [brush, selectionRange]);
return (
<svg
style={{ display }}
width={width}
height={height}
id={`histogram_${fieldForId}_svg`}
data-testclass="histogram-plot"
data-testid={`histogram-${field}-plot`}
ref={svgRef}
/>
);
};
export default Histogram;
+140 -517
View File
@@ -1,449 +1,38 @@
/*
https://bl.ocks.org/mbostock/4341954
https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
https://bl.ocks.org/mbostock/3019563
*/
import React, { useEffect, useRef, useState, useCallback } from "react";
import { Button, ButtonGroup, Tooltip } from "@blueprintjs/core";
import { connect } from "react-redux";
import React from "react";
import { connect, shallowEqual } from "react-redux";
import * as d3 from "d3";
import { interpolateCool } from "d3-scale-chromatic";
import Async from "react-async";
import memoize from "memoize-one";
import * as globals from "../../globals";
import actions from "../../actions";
import { histogramContinuous } from "../../util/dataframe/histogram";
import { makeContinuousDimensionName } from "../../util/nameCreators";
import significantDigits from "../../util/significantDigits";
import HistogramHeader from "./header";
import Histogram from "./histogram";
import HistogramFooter from "./footer";
import StillLoading from "./loading";
import ErrorLoading from "./error";
function clamp(val, rng) {
return Math.max(Math.min(val, rng[1]), rng[0]);
}
function maybeScientific(x) {
let format = ",";
const _ticks = x.ticks(4);
if (x.domain().some((n) => Math.abs(n) >= 10000)) {
/*
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
4.20e+n
*/
format = significantDigits(_ticks[_ticks.length - 1]) === 1 ? ".0e" : ".2e";
}
return format;
}
const StillLoading = ({ zebra, displayName }) => {
/*
Render a loading indicator for the field.
*/
return (
<div
style={{
padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
justifyItems: "center",
alignItems: "center",
}}
>
<div style={{ minWidth: 30 }} />
<div style={{ display: "flex", alignSelf: "center" }}>
<span style={{ fontStyle: "italic" }}>{displayName}</span>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
}}
>
<Button minimal loading intent="primary" />
</div>
</div>
</div>
);
const MARGIN = {
LEFT: 10, // Space for 0 tick label on X axis
RIGHT: 54, // space for Y axis & labels
BOTTOM: 25, // space for X axis & labels
TOP: 3,
};
const ErrorLoading = ({ displayName, error, zebra }) => {
console.log(error); // log to console as this is unexpected
return (
<div
style={{
padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
<span>{`Failure loading ${displayName}`}</span>
</div>
);
};
const HistogramFooter = React.memo(
({
displayName,
hideRanges,
rangeMin,
rangeMax,
rangeColorMin,
rangeColorMax,
logFoldChange,
pvalAdj,
}) => {
/*
Footer of each histogram. Will render range, title, and optionally
differential expression info.
Required props:
* displayName - the displayName, aka "n_genes", "FOXP2", etc.
* hideRanges - true/false, enables/disable rendering of ranges
* range - length two array, [min, max], containing the range values to display
* rangeColor - length two array, [mincolor, maxcolor], each a CSS color
* logFoldChange - lfc to display, optional.
* pValue - pValue to display, optional.
*/
return (
<div>
<div
style={{
display: "flex",
justifyContent: hideRanges ? "center" : "space-between",
}}
>
<span
style={{
color: rangeColorMin,
display: hideRanges ? "none" : "block",
}}
>
min {rangeMin.toPrecision(4)}
</span>
<span
data-testclass="brushable-histogram-field-name"
style={{ fontStyle: "italic" }}
>
{displayName}
</span>
<div style={{ display: hideRanges ? "block" : "none" }}>
: {rangeMin}
</div>
<span
style={{
color: rangeColorMax,
display: hideRanges ? "none" : "block",
}}
>
max {rangeMax.toPrecision(4)}
</span>
</div>
{logFoldChange && pvalAdj ? (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "baseline",
}}
>
<span>
<strong>log fold change:</strong>
{` ${logFoldChange.toPrecision(4)}`}
</span>
<span
style={{
marginLeft: 7,
padding: 2,
}}
>
<strong>p-value (adj):</strong>
{pvalAdj < 0.0001 ? " < 0.0001" : ` ${pvalAdj.toFixed(4)}`}
</span>
</div>
) : null}
</div>
);
}
);
const HistogramHeader = React.memo(
({
fieldId,
isColorBy,
onColorByClick,
onRemoveClick,
isScatterPlotX,
isScatterPlotY,
onScatterPlotXClick,
onScatterPlotYClick,
isObs,
}) => {
/*
Render the toolbar for the histogram. Props:
* fieldId - field identifier, used for various IDs
* isColorBy - true/false, is this the current color-by
* onColorByClick - color-by click handler
* onRemoveClick - optional handler for remove. Button will not render if not defined.
* isScatterPlotX - optional, true/false if currently the X scatterplot field
* isScatterPlotY - optional, true/false if currently the Y scatterplot field
* onScatterPlotXClick - optional, handler for scatterPlot X button.
* onScatterPlotYClick - optional, handler for scatterPlot X button.
Scatterplot controls will not render if either handler unspecified.
*/
const memoizedColorByCallback = useCallback(
() => onColorByClick(fieldId, isObs),
[fieldId, isObs]
);
return (
<div
style={{
display: "flex",
justifyContent: "flex-end",
paddingBottom: "8px",
}}
>
{onScatterPlotXClick && onScatterPlotYClick ? (
<span>
<span
style={{ marginRight: 7 }}
className="bp3-icon-standard bp3-icon-scatter-plot"
/>
<ButtonGroup style={{ marginRight: 7 }}>
<Button
data-testid={`plot-x-${fieldId}`}
onClick={onScatterPlotXClick}
active={isScatterPlotX}
intent={isScatterPlotX ? "primary" : "none"}
>
plot x
</Button>
<Button
data-testid={`plot-y-${fieldId}`}
onClick={onScatterPlotYClick}
active={isScatterPlotY}
intent={isScatterPlotY ? "primary" : "none"}
>
plot y
</Button>
</ButtonGroup>
</span>
) : null}
{onRemoveClick ? (
<Button
minimal
onClick={onRemoveClick}
style={{
color: globals.blue,
cursor: "pointer",
marginLeft: 7,
}}
>
remove
</Button>
) : null}
<Tooltip
content="Use as color scale"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<Button
onClick={memoizedColorByCallback}
active={isColorBy}
intent={isColorBy ? "primary" : "none"}
data-testclass="colorby"
data-testid={`colorby-${fieldId}`}
icon="tint"
/>
</Tooltip>
</div>
);
}
);
const Histogram = ({
field,
fieldForId,
display,
histogram,
width,
height,
onBrush,
onBrushEnd,
margin,
isColorBy,
selectionRange,
}) => {
const svgRef = useRef(null);
const [brush, setBrush] = useState(null);
useEffect(() => {
/*
Create the d3 histogram
*/
const { marginLeft, marginRight, marginBottom, marginTop } = margin;
const { x, y, bins, binStart, binEnd, binWidth } = histogram;
const svg = d3.select(svgRef.current);
/* Remove everything */
svg.selectAll("*").remove();
/* Set margins within the SVG */
const container = svg
.attr("width", width + marginLeft + marginRight)
.attr("height", height + marginTop + marginBottom)
.append("g")
.attr("class", "histogram-container")
.attr("transform", `translate(${marginLeft},${marginTop})`);
const colorScale = d3
.scaleSequential(interpolateCool)
.domain([0, bins.length]);
const histogramScale = d3
.scaleLinear()
.domain(x.domain())
.range([
colorScale.domain()[1],
colorScale.domain()[0],
]); /* we flip this to make colors dark if high in the color scale */
if (binWidth > 0) {
/* BINS */
container
.insert("g", "*")
.selectAll("rect")
.data(bins)
.enter()
.append("rect")
.attr("x", (d, i) => x(binStart(i)) + 1)
.attr("y", (d) => y(d))
.attr("width", (d, i) => x(binEnd(i)) - x(binStart(i)) - 1)
.attr("height", (d) => y(0) - y(d))
.style(
"fill",
isColorBy ? (d, i) => colorScale(histogramScale(binStart(i))) : "#bbb"
);
}
// BRUSH
// Note the brushable area is bounded by the data on three sides, but goes down to cover the x-axis
const brushX = d3
.brushX()
.extent([
[x.range()[0], y.range()[1]],
[x.range()[1], marginTop + height + marginBottom],
])
/*
emit start so that the Undoable history can save an undo point
upon drag start, and ignore the subsequent intermediate drag events.
*/
.on("start", onBrush(field, x.invert, "start"))
.on("brush", onBrush(field, x.invert, "brush"))
.on("end", onBrushEnd(field, x.invert));
const brushXselection = container
.insert("g")
.attr("class", "brush")
.attr("data-testid", `${svgRef.current.dataset.testid}-brushable-area`)
.call(brushX);
/* X AXIS */
container
.insert("g")
.attr("class", "axis axis--x")
.attr("transform", `translate(0,${marginTop + height})`)
.call(
d3
.axisBottom(x)
.ticks(4)
.tickFormat(d3.format(maybeScientific(x)))
);
/* Y AXIS */
container
.insert("g")
.attr("class", "axis axis--y")
.attr("transform", `translate(${marginLeft + width},0)`)
.call(
d3
.axisRight(y)
.ticks(3)
.tickFormat(
d3.format(
y.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
)
)
);
/* axis style */
svg.selectAll(".axis text").style("fill", "rgb(80,80,80)");
svg.selectAll(".axis path").style("stroke", "rgb(230,230,230)");
svg.selectAll(".axis line").style("stroke", "rgb(230,230,230)");
setBrush({ brushX, brushXselection });
}, [histogram, isColorBy]);
useEffect(() => {
/*
paint/update selection brush
*/
if (!brush) return;
const { brushX, brushXselection } = brush;
const selection = d3.brushSelection(brushXselection.node());
if (!selectionRange && selection) {
/* no active selection - clear brush */
brushXselection.call(brushX.move, null);
} else if (selectionRange) {
const { x, domain } = histogram;
const [min, max] = domain;
const x0 = x(clamp(selectionRange[0], [min, max]));
const x1 = x(clamp(selectionRange[1], [min, max]));
if (!selection) {
/* there is an active selection, but no brush - set the brush */
brushXselection.call(brushX.move, [x0, x1]);
} else {
/* there is an active selection and a brush - make sure they match */
const moveDeltaThreshold = 1;
const dX0 = Math.abs(x0 - selection[0]);
const dX1 = Math.abs(x1 - selection[1]);
/*
only update the brush if it is grossly incorrect,
as defined by the moveDeltaThreshold
*/
if (dX0 > moveDeltaThreshold || dX1 > moveDeltaThreshold) {
brushXselection.call(brushX.move, [x0, x1]);
}
}
}
}, [brush, selectionRange]);
return (
<svg
style={{ display }}
width={width}
height={height}
id={`histogram_${fieldForId}_svg`}
data-testclass="histogram-plot"
data-testid={`histogram-${field}-plot`}
ref={svgRef}
/>
);
const WIDTH = 340 - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = 135 - MARGIN.TOP - MARGIN.BOTTOM;
const MARGIN_MINI = {
LEFT: 0, // Space for 0 tick label on X axis
RIGHT: 0, // space for Y axis & labels
BOTTOM: 0, // space for X axis & labels
TOP: 0,
};
const WIDTH_MINI = 120 - MARGIN_MINI.LEFT - MARGIN_MINI.RIGHT;
const HEIGHT_MINI = 15 - MARGIN_MINI.TOP - MARGIN_MINI.BOTTOM;
@connect((state, ownProps) => {
const { isObs, isUserDefined, isDiffExp, field } = ownProps;
const { isObs, isUserDefined, isGeneSetSummary, field } = ownProps;
const myName = makeContinuousDimensionName(
{ isObs, isUserDefined, isDiffExp },
{ isObs, isUserDefined, isGeneSetSummary },
field
);
return {
@@ -455,6 +44,10 @@ const Histogram = ({
};
})
class HistogramBrush extends React.PureComponent {
static watchAsync(props, prevProps) {
return !shallowEqual(props.watchProps, prevProps.watchProps);
}
/* memoized closure to prevent HistogramHeader unecessary repaint */
handleColorAction = memoize((dispatch) => (field, isObs) => {
if (isObs) {
@@ -467,27 +60,11 @@ class HistogramBrush extends React.PureComponent {
}
});
constructor(props) {
super(props);
const marginLeft = 10; // Space for 0 tick label on X axis
const marginRight = 54; // space for Y axis & labels
const marginBottom = 25; // space for X axis & labels
const marginTop = 3;
this.margin = {
marginLeft,
marginRight,
marginBottom,
marginTop,
};
this.width = 340 - marginLeft - marginRight;
this.height = 135 - marginTop - marginBottom;
}
onBrush = (selection, x, eventType) => {
const type = `continuous metadata histogram ${eventType}`;
return () => {
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
const { dispatch, field, isObs, isUserDefined, isGeneSetSummary } =
this.props;
// ignore programmatically generated events
if (!d3.event.sourceEvent) return;
@@ -503,7 +80,7 @@ class HistogramBrush extends React.PureComponent {
continuousNamespace: {
isObs,
isUserDefined,
isDiffExp,
isGeneSetSummary,
},
};
dispatch(
@@ -512,9 +89,9 @@ class HistogramBrush extends React.PureComponent {
};
};
onBrushEnd = (selection, x) => {
return () => {
const { dispatch, field, isObs, isUserDefined, isDiffExp } = this.props;
onBrushEnd = (selection, x) => () => {
const { dispatch, field, isObs, isUserDefined, isGeneSetSummary } =
this.props;
const minAllowedBrushSize = 10;
const smallAmountToAvoidInfiniteLoop = 0.1;
@@ -553,14 +130,13 @@ class HistogramBrush extends React.PureComponent {
continuousNamespace: {
isObs,
isUserDefined,
isDiffExp,
isGeneSetSummary,
},
};
dispatch(
actions.selectContinuousMetadataAction(type, query, range, otherProps)
);
};
};
handleSetGeneAsScatterplotX = () => {
const { dispatch, field } = this.props;
@@ -610,7 +186,8 @@ class HistogramBrush extends React.PureComponent {
};
fetchAsyncProps = async () => {
const { annoMatrix } = this.props;
const { annoMatrix, width } = this.props;
const { isClipped } = annoMatrix;
const query = this.createQuery();
@@ -640,9 +217,15 @@ class HistogramBrush extends React.PureComponent {
const histogram = this.calcHistogramCache(
column,
this.margin,
this.width,
this.height
MARGIN,
width || WIDTH,
HEIGHT
);
const miniHistogram = this.calcHistogramCache(
column,
MARGIN_MINI,
width || WIDTH_MINI,
HEIGHT_MINI
);
const isSingleValue = summary.min === summary.max;
@@ -656,6 +239,7 @@ class HistogramBrush extends React.PureComponent {
return {
histogram,
miniHistogram,
range,
unclippedRange,
unclippedRangeColor,
@@ -665,27 +249,33 @@ class HistogramBrush extends React.PureComponent {
};
// eslint-disable-next-line class-methods-use-this -- instance method allows for memoization per annotation
calcHistogramCache(col, margin, width, height) {
calcHistogramCache(col, newMargin, newWidth, newHeight) {
/*
recalculate expensive stuff, notably bins, summaries, etc.
*/
const histogramCache = {};
const summary = col.summarize();
const histogramCache = {}; /* maybe change this so that it computes ... */
const summary =
col.summarize(); /* this is memoized, so it's free the second time you call it */
const { min: domainMin, max: domainMax } = summary;
const numBins = 40;
const { marginTop, marginLeft } = margin;
const { TOP: topMargin, LEFT: leftMargin } =
newMargin; /* changes with mini */
histogramCache.domain = [domainMin, domainMax];
histogramCache.domain = [
domainMin,
domainMax,
]; /* doesn't change with mini */
histogramCache.x = d3
.scaleLinear()
.domain([domainMin, domainMax])
.range([marginLeft, marginLeft + width]);
.range([leftMargin, leftMargin + newWidth]);
histogramCache.bins = histogramContinuous(col, numBins, [
histogramCache.bins = col.histogram(numBins, [
domainMin,
domainMax,
]);
]); /* memoized */
histogramCache.binWidth = (domainMax - domainMin) / numBins;
histogramCache.binStart = (i) => domainMin + i * histogramCache.binWidth;
@@ -697,25 +287,43 @@ class HistogramBrush extends React.PureComponent {
histogramCache.y = d3
.scaleLinear()
.domain([0, yMax])
.range([marginTop + height, marginTop]);
.range([topMargin + newHeight, topMargin]);
return histogramCache;
}
createQuery() {
const { isObs, field, annoMatrix } = this.props;
const { isObs, isGeneSetSummary, field, setGenes, annoMatrix } = this.props;
const { schema } = annoMatrix;
if (isObs) {
return ["obs", field];
}
const varIndex = schema?.annotations?.var?.index;
if (!varIndex) return null;
if (isGeneSetSummary) {
return [
"X",
{
summarize: {
method: "mean",
field: "var",
column: varIndex,
values: [...setGenes.keys()],
},
},
];
}
// else, we assume it is a gene expression
return [
"X",
{
field: "var",
column: varIndex,
value: field,
where: {
field: "var",
column: varIndex,
value: field,
},
},
];
}
@@ -727,20 +335,34 @@ class HistogramBrush extends React.PureComponent {
field,
isColorAccessor,
isUserDefined,
isDiffExp,
logFoldChange,
pvalAdj,
isGeneSetSummary,
isScatterplotXXaccessor,
isScatterplotYYaccessor,
zebra,
continuousSelectionRange,
isObs,
mini,
setGenes,
} = this.props;
let { width } = this.props;
if (!width) {
width = mini ? WIDTH_MINI : WIDTH;
}
const fieldForId = field.replace(/\s/g, "_");
const showScatterPlot = isDiffExp || isUserDefined;
const showScatterPlot = isUserDefined;
let testClass = "histogram-continuous-metadata";
if (isUserDefined) testClass = "histogram-user-gene";
else if (isGeneSetSummary) testClass = "histogram-gene-set-summary";
return (
<Async watch={annoMatrix} promiseFn={this.fetchAsyncProps}>
<Async
watchFn={HistogramBrush.watchAsync}
promiseFn={this.fetchAsyncProps}
watchProps={{ annoMatrix, setGenes }}
>
<Async.Pending initial>
<StillLoading displayName={field} zebra={zebra} />
</Async.Pending>
@@ -755,56 +377,57 @@ class HistogramBrush extends React.PureComponent {
<div
id={`histogram_${fieldForId}`}
data-testid={`histogram-${field}`}
data-testclass={
isDiffExp
? "histogram-diffexp"
: isUserDefined
? "histogram-user-gene"
: "histogram-continuous-metadata"
}
data-testclass={testClass}
style={{
padding: globals.leftSidebarSectionPadding,
padding: mini ? 0 : globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
<HistogramHeader
fieldId={field}
isColorBy={isColorAccessor}
isObs={isObs}
onColorByClick={this.handleColorAction(dispatch)}
onRemoveClick={isUserDefined ? this.removeHistogram : null}
isScatterPlotX={isScatterplotXXaccessor}
isScatterPlotY={isScatterplotYYaccessor}
onScatterPlotXClick={
showScatterPlot ? this.handleSetGeneAsScatterplotX : null
}
onScatterPlotYClick={
showScatterPlot ? this.handleSetGeneAsScatterplotY : null
}
/>
{!mini && isObs ? (
<HistogramHeader
fieldId={field}
isColorBy={isColorAccessor}
isObs={isObs}
onColorByClick={this.handleColorAction(dispatch)}
onRemoveClick={isUserDefined ? this.removeHistogram : null}
isScatterPlotX={isScatterplotXXaccessor}
isScatterPlotY={isScatterplotYYaccessor}
onScatterPlotXClick={
showScatterPlot ? this.handleSetGeneAsScatterplotX : null
}
onScatterPlotYClick={
showScatterPlot ? this.handleSetGeneAsScatterplotY : null
}
/>
) : null}
<Histogram
field={field}
fieldForId={fieldForId}
display={asyncProps.isSingleValue ? "none" : "block"}
histogram={asyncProps.histogram}
width={this.width}
height={this.height}
histogram={
mini ? asyncProps.miniHistogram : asyncProps.histogram
}
width={width}
height={mini ? HEIGHT_MINI : HEIGHT}
onBrush={this.onBrush}
onBrushEnd={this.onBrushEnd}
margin={this.margin}
margin={mini ? MARGIN_MINI : MARGIN}
isColorBy={isColorAccessor}
selectionRange={continuousSelectionRange}
mini={mini}
/>
<HistogramFooter
displayName={field}
hideRanges={asyncProps.isSingleValue}
rangeMin={asyncProps.unclippedRange[0]}
rangeMax={asyncProps.unclippedRange[1]}
rangeColorMin={asyncProps.unclippedRangeColor[0]}
rangeColorMax={asyncProps.unclippedRangeColor[1]}
logFoldChange={logFoldChange}
pvalAdj={pvalAdj}
/>
{!mini && (
<HistogramFooter
isGeneSetSummary={isGeneSetSummary}
isObs={isObs}
displayName={field}
hideRanges={asyncProps.isSingleValue}
rangeMin={asyncProps.unclippedRange[0]}
rangeMax={asyncProps.unclippedRange[1]}
rangeColorMin={asyncProps.unclippedRangeColor[0]}
rangeColorMax={asyncProps.unclippedRangeColor[1]}
/>
)}
</div>
) : null
}
@@ -0,0 +1,43 @@
import React from "react";
import { Button } from "@blueprintjs/core";
import * as globals from "../../globals";
const StillLoading = ({ zebra, displayName }) =>
/*
Render a loading indicator for the field.
*/
(
<div
data-testclass="gene-loading-spinner"
style={{
padding: globals.leftSidebarSectionPadding,
backgroundColor: zebra ? globals.lightestGrey : "white",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
justifyItems: "center",
alignItems: "center",
}}
>
<div style={{ minWidth: 30 }} />
<div style={{ display: "flex", alignSelf: "center" }}>
<span style={{ fontStyle: "italic" }}>{displayName}</span>
</div>
<div
style={{
display: "flex",
justifyContent: "flex-end",
}}
>
<Button minimal loading intent="primary" />
</div>
</div>
</div>
)
;
export default StillLoading;
@@ -26,16 +26,14 @@ class DuplicateCategorySelect extends React.PureComponent {
[] /* this is a placeholder, could be a subcomponent to avoid this */
}
filterable={false}
itemRenderer={(d, { handleClick }) => {
return (
itemRenderer={(d, { handleClick }) => (
<MenuItem
data-testclass="duplicate-category-dropdown-option"
onClick={handleClick}
key={d}
text={d}
/>
);
}}
)}
noResults={<MenuItem disabled text="No results." />}
onItemSelect={(d) => {
handleModalDuplicateCategorySelection(d);
@@ -1,14 +1,13 @@
import React from "react";
import { connect } from "react-redux";
import AnnoDialog from "../annoDialog";
import LabelInput from "../labelInput";
import AnnoDialog from "../../annoDialog";
import LabelInput from "../../labelInput";
import { labelPrompt, isLabelErroneous } from "../labelUtil";
import actions from "../../../actions";
@connect((state) => ({
annotations: state.annotations,
schema: state.annoMatrix?.schema,
ontology: state.ontology,
obsCrossfilter: state.obsCrossfilter,
}))
class Category extends React.PureComponent {
@@ -57,13 +56,11 @@ class Category extends React.PureComponent {
};
labelNameError = (name) => {
const { metadataField, ontology, schema } = this.props;
return isLabelErroneous(name, metadataField, ontology, schema);
const { metadataField, schema } = this.props;
return isLabelErroneous(name, metadataField, schema);
};
instruction = (label) => {
return labelPrompt(this.labelNameError(label), "New, unique label", ":");
};
instruction = (label) => labelPrompt(this.labelNameError(label), "New, unique label", ":");
handleChangeOrSelect = (label) => {
this.setState({ newLabelText: label });
@@ -71,8 +68,7 @@ class Category extends React.PureComponent {
render() {
const { newLabelText } = this.state;
const { metadataField, annotations, ontology, obsCrossfilter } = this.props;
const ontologyEnabled = ontology?.enabled ?? false;
const { metadataField, annotations, obsCrossfilter } = this.props;
return (
<>
@@ -97,7 +93,7 @@ class Category extends React.PureComponent {
handleCancel={this.disableAddNewLabelMode}
annoInput={
<LabelInput
labelSuggestions={ontologyEnabled ? ontology.terms : null}
labelSuggestions={null}
onChange={this.handleChangeOrSelect}
onSelect={this.handleChangeOrSelect}
inputProps={{
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import AnnoDialog from "../annoDialog";
import LabelInput from "../labelInput";
import AnnoDialog from "../../annoDialog";
import LabelInput from "../../labelInput";
import { labelPrompt } from "../labelUtil";
import { AnnotationsHelpers } from "../../../util/stateManager";
@@ -10,7 +10,6 @@ import actions from "../../../actions";
@connect((state) => ({
annotations: state.annotations,
schema: state.annoMatrix?.schema,
ontology: state.ontology,
}))
class AnnoDialogEditCategoryName extends React.PureComponent {
constructor(props) {
@@ -90,13 +89,11 @@ class AnnoDialogEditCategoryName extends React.PureComponent {
return false;
};
instruction = (name) => {
return labelPrompt(
instruction = (name) => labelPrompt(
this.editedCategoryNameError(name),
"New, unique category name",
":"
);
};
allCategoryNames() {
const { schema } = this.props;
@@ -105,8 +102,7 @@ class AnnoDialogEditCategoryName extends React.PureComponent {
render() {
const { newCategoryText } = this.state;
const { metadataField, annotations, ontology } = this.props;
const ontologyEnabled = ontology?.enabled ?? false;
const { metadataField, annotations } = this.props;
return (
<>
@@ -132,7 +128,7 @@ class AnnoDialogEditCategoryName extends React.PureComponent {
annoInput={
<LabelInput
label={newCategoryText}
labelSuggestions={ontologyEnabled ? ontology.terms : null}
labelSuggestions={null}
onChange={this.handleChangeOrSelect}
onSelect={this.handleChangeOrSelect}
inputProps={{
@@ -9,7 +9,9 @@ import {
Tooltip,
Icon,
PopoverInteractionKind,
Intent,
} from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../../globals";
import actions from "../../../actions";
@@ -89,8 +91,8 @@ class AnnoMenuCategory extends React.PureComponent {
text={editText}
/>
<MenuItem
icon="delete"
intent="danger"
icon={IconNames.TRASH}
intent={Intent.DANGER}
data-testclass="handleDeleteCategory"
data-testid={`${metadataField}:delete-category`}
onClick={this.handleDeleteCategory}

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