Compare commits

...
916 Commits
Author SHA1 Message Date
LiudengZhang 76a39fcf92 fix: correct opacity prop typo and style object in centroid labels (#2767) 2026-03-09 13:39:56 -07:00
rwbaber 5eb3021752 fix: Handle non-categorical columns with colors + update deprecated AnnData API (#2758)
Yeah that makes sense! Mergingg
2026-02-24 13:37:57 -08:00
Timmy HuangandClaude Opus 4.5 7c7203bdfe chore: replace deprecated codecov with GitHub Action (#2747)
Migrate from the deprecated codecov npm package and bash uploader to the
official codecov/codecov-action@v5. This fixes gcov errors on macOS runners
and provides better reliability for coverage uploads.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 09:28:12 -08:00
rwbaber 5ac256fc8b fix: remove incompatible float16 fixture from general adaptor tests (#2746)
* test: remove incompatible float16 fixture from generic adaptor tests

* test: remove obsolete error test file for float16
2026-01-15 15:18:45 -08:00
2b86f8e600 fix: improve anndata error msg (#2743)
* feat: improve error message for anndata version mismatches

* fix: Remove mentioning of python update and only mention upgrading anndata

* Update server/data_anndata/anndata_adaptor.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 09:22:22 -08:00
Timmy Huang 0fddcd042a docs: Add open source required docs (#2745) 2026-01-14 09:13:54 -08: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
Severiano Badajoz 53a268fb71 Surround script hash in single quotes (#1735)
* add disclaimer about changing the script

* add hash for obsolete browser script

* add explicit domain img-src

* add single quotes

* add quotes to `data:`

* change order and remove single quotes

* lint, remove extra slash, and make hash an array

* add links to S3

* surround script has in single quotes
2020-08-12 11:31:12 -07:00
Severiano Badajoz 518768e8f8 change asset src's to S3 links (#1734)
* add disclaimer about changing the script

* add hash for obsolete browser script

* add explicit domain img-src

* add single quotes

* add quotes to `data:`

* change order and remove single quotes

* lint, remove extra slash, and make hash an array

* add links to S3
2020-08-12 10:38:49 -07:00
maniarathi 44c738e2f4 Bumping version of cellxgene for desktop release (#1733) 2020-08-12 09:58:52 -07:00
Severiano Badajoz f221856ae1 add CSP sources for obsolete browser prompt (#1731)
Adds script hash and explicit domain to `img-src` directive
2020-08-11 16:50:46 -07:00
Madison Dunitz 80f6137528 retrieve latest annotation from db (#1723)
* add function to retrieve latest annotation from db, db updates

* dont create directory in s3
2020-08-11 15:48:12 -05:00
maniarathi 49b89ba7af Swap using href to using origin+pathname to avoid errors with including any query parameters from being passed in. (#1722) 2020-08-07 10:02:22 -07:00
Severiano Badajoz 018daacdbb create truncation edge case snapshot tests (#1695) 2020-08-07 09:54:01 -07:00
bmccandless 4291f6005d fix logout for auth0 (#1719)
* fix logout for auth0

the redirect from logout needs to be registered, and therefore cannot
be an arbitrary path in the server.  When the user logs out, they are
redirected to the index page

* no need to provide dataset to logout url
2020-08-06 11:52:19 -07:00
Colin Megill c913935d90 Login / Logout button (#1718)
* first pass auth button

* only pop anno dialogue if authenticated

* add config to ignore

* remove config
2020-08-06 14:39:58 -04:00
Severiano Badajoz 8d96477fae Remove hash source from CSP style-src directive (#1717)
* remove style csp hash generation + lint

* remove references to style_hashes
2020-08-05 16:43:35 -07:00
Severiano Badajoz b18f96da77 check for bins change for canvas draw (#1693)
* check for bins change for canvas draw

* PR feedback
2020-08-05 15:20:03 -07:00
bmccandless b5e5ee0168 Update hosted app to get the oauth client secret from the secret manager (#1713)
* Update the hosted app to get the oauth client secret from the secret manager

* fix to eb app, and set no cache on oauth endpoints
2020-08-05 12:00:35 -07:00
maniarathi 0d94c9e092 DRY-ing flatbuffer code (#1716) 2020-08-05 11:56:34 -07:00
maniarathi cdae4f9f10 Reorganize the server testing directory (#1705) 2020-08-05 08:31:02 -07:00
Marcus Kinsella 550847f763 Add dataset 29 (#1712) 2020-08-04 16:03:44 -07:00
Marcus Kinsella 6bda27f554 Add datasets 45-47 (#1706) 2020-08-04 11:07:08 -07:00
Madison Dunitz f632a8db91 Dunitz/db setup (#1619)
* initial database setup
2020-08-03 17:54:06 -05:00
8bbc183647 Explicit Browser Support (#1682)
* add FastestSmallestTextEncoderDecoder polyfill

* remove nomodule from script import

* add browserslist

* add obsolete-webpack-plugin

* switch out modern-browser for preset-env

* add prompt on non target browser

* propagate prod changes to dev

* add core-js-3 and TextEncoder TextDecoder (#1671)

* add script to remove react, style html

* propagate changes to prod

* add script-ext-html-webpack-plugin for async

* more styling

* add eslint-plugin-compat

* extend compat plugin

* add existing polyfills

* add github fetch polyfill

* add AbortController polyfill

* change promptOnNonTargetBrowser to false

* propagate

* add browser links

* prettier

* add browser support to readme

* move polyfills to webpack

* remove CDN encoder polyfill

* Add no Explorer support

* fix incorrect package name

* propagate changes

* Update README.md

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

* add new deps

* create shared config

* swap out html-loader for filestream

* sanitize template

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
Co-authored-by: Ambrose J Carr <ambrosejcarr@users.noreply.github.com>
2020-08-03 12:50:09 -07:00
bmccandless ce13a9c7ca oauth support, add the token in a configuration specified cookie (#1702)
* oauth support, add the token in a configuration specified cookie

Previously, the id token was stored in the session token.
Now, it can be placed in a different cookie with different properties.
2020-08-03 10:45:21 -07:00
bmccandless 2afa48cf11 add oauth authentication (#1681)
* add oauth authentication

Add support for OAuth2.

Change the interface to AuthTypeBase
  - better handling of config parameters
  - add a complete_setup function for additional setup steps

Added a function wrapper to enforce authentication for the
routes that require authenticaiton.

* change fsspec requirement

fsspec 0.8.0 breaks our tests
it imports a module that is does not require.
2020-07-31 18:16:57 -07:00
dependabot[bot] bb2326525e Bump elliptic from 6.5.2 to 6.5.3 in /client (#1697)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-31 13:54:56 -04:00
Bruce Martin f2fbeff511 add support for corpora default_embedding field (#1696)
* fix mispelling

* re-implement re-embedding

* always load base embedding to fetch counts

* format

* lint

* fix tests

* lint

* fix accept handling

* test log

* more debug

* more

* more

* more

* more

* remove logging

* logging

* jsonify

* remove debugging logs

* lint

* clean up errors a bit

* fix issue found in PR review

* add support for corpora default_embedding

* fix botched merge

* PR review

* PR review
2020-07-31 07:36:48 -07:00
Severiano Badajoz 055511fe60 fix colorby popup settings (#1694) 2020-07-30 15:33:48 -07:00
Madison Dunitz d748b9f691 use czi-sci-single-cell-eng github user/access token (#1698) 2020-07-30 15:54:57 -05:00
Bruce Martin 75cb513dd9 re-implement re-embeddings (#1679)
* fix mispelling

* re-implement re-embedding

* always load base embedding to fetch counts

* format

* lint

* fix tests

* lint

* fix accept handling

* test log

* more debug

* more

* more

* more

* more

* remove logging

* logging

* jsonify

* remove debugging logs

* lint

* clean up errors a bit

* fix issue found in PR review

* PR review changes
2020-07-30 12:31:36 -07:00
bmccandless bd147abb3f Fix eb logging. (#1692)
It now logs the requests to the file

Fixes #1611
2020-07-29 16:03:47 -07:00
bmccandless 5633d7c761 Fix server exception classes (#1683)
str(e) and e.message will both show the error message.
refactored the error.py file to simplify our exception class definitions
2020-07-29 13:05:59 -07:00
maniarathi af3a76c354 Adding relative links support to jekyll (#1680) 2020-07-29 12:38:46 -07:00
Bruce Martin 59f989d26f initial support for corpora schema conventions (#1676)
* initial support for corpora schema conventions

* remove debugging print

* add corpora util module

* tests

* lint

* PR review edits

* PR changes

* more PR changes

* more PR chnages

* PR fixes

* formatting

* PR updates

* lint

* PR review
2020-07-28 17:32:27 -07:00
bmccandless 5285556415 Add basic authentication in the server (#1670)
* Add basic authentication in the server

A pattern for creating authentication methods is introduced, with three
authentication types defined:
  none - no authentication
  session - like the current session based auth used for user annotations
  test - used to test the login/logout process end to end

The config endpoint now returns informations about the authentication, like if
the user is authenticated and their username.  The redirect uri's for login and
logout are also returned if the authentication type requires login

This is the first a several PRs for authentication.

*. Update server tests to avoid hardcoded ports

test_api and test_nan_rest now use a common function for starting a test server,
than will initially choose a random port.
2020-07-28 13:28:30 -07:00
Bruce Martin bbef27b8c9 minor prose change on embedding chooser (#1678) 2020-07-27 19:34:04 -07:00
maniarathi 38ce1f90fb Update issue templates 2020-07-27 12:57:59 -07:00
maniarathi 98c5cae9f4 Update issue templates 2020-07-27 12:55:44 -07:00
Bruce Martin 14cd1b9f0b work around blueprint restriction (#1677) 2020-07-27 12:52:22 -07:00
Bruce Martin 0a626537d5 subset embedding UI improvement (#1667)
* add user flag feature to annomatrix

* add implicit subsetting for partial embeddings

* lint

* add embedding cell counts to embedding choice menu

* layout
2020-07-27 09:58:14 -07:00
maniarathi c25dd33d28 Removing duplicate index.html and regenerating files. (#1672) 2020-07-27 09:57:48 -07:00
dependabot[bot] ad1bda07e9 Bump codecov from 3.7.0 to 3.7.1 in /client (#1654)
Bumps [codecov](https://github.com/codecov/codecov-node) from 3.7.0 to 3.7.1.
- [Release notes](https://github.com/codecov/codecov-node/releases)
- [Commits](https://github.com/codecov/codecov-node/compare/v3.7.0...v3.7.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-27 11:53:57 -04:00
Ambrose J Carr 27f75641cc update index links to include 'post' directory (#1666)
* update index links to include 'post' directory

* rebuild github pages site

* document how to develop documentation

* build, not serve
2020-07-23 21:49:57 -07:00
bmccandless 0344cfacce Fix the app config for reembedding and add a test (#1664)
* Fix the app config for reembedding and add a test

* Add scanpy to requirements-dev
2020-07-23 12:27:53 -07:00
Bruce Martin 83d572cde2 force categorical treatment of user annotations (#1663) 2020-07-22 17:02:19 -07:00
Colin Megillandbkmartinjr 03bad04436 Embedding button to lower left, cell selection (#1658)
* embedding

* menu bottom left

* button

* change gutters to support lower toolbar

* fix scatterplot layout

* fix tests to match new layout

* fix smoke tests to match new layout

* better sentence, dataset.nObs to top

* scatterplot position

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
2020-07-22 18:48:21 -04:00
Bruce Martin a44da11f3f correctly handle non-string categoricals (#1660) 2020-07-22 12:14:31 -07:00
Bruce Martin 009fa2ff02 clear selection state upon subset (#1655) 2020-07-21 12:51:39 -07:00
Bruce Martin 2715157793 categorical UI improvements (#1649)
* return undefined for unknown fields

* memoize fetch response

* handle rename of user annotation

* performance and cleanup

* fix snapshots

* fix comment

* lint
2020-07-20 08:52:41 -07:00
Bruce Martin 410566f5e1 fix label selection on trunctated category lists (#1646) 2020-07-17 17:16:15 -07:00
Bruce Martin b6412b5b8a ensure there is always an unassigned label in user created category (#1644) 2020-07-17 13:22:26 -07:00
Bruce Martin 7b99d1a1b8 update JS deps (#1642) 2020-07-17 12:38:54 -07:00
bmccandless 2cb3ae7046 Fix error handling in plugin module (#1645) 2020-07-17 08:14:10 -07:00
Bruce Martin 18d0fd78cf fix view stack bug (#1640) 2020-07-16 16:14:52 -07:00
bmccandless f223d7504e Small fix for handling display versions (#1625)
* Small fix for handling display versions

Making a distinction between __version__ and the version we display in the info panel (displayr_version).
The hosted cellxgene can overwrite the display_version using a plugin.

Improve version handling in the customized assets
2020-07-16 15:11:37 -07:00
Severiano Badajoz ed23d5dd9d fix truncation edge cases (#1637) 2020-07-16 14:00:57 -07:00
Bruce Martin 2f700b377f permit NaN in embedding coordinates (#1631)
* permit NaN in embedding coordinates

* lint
2020-07-16 11:43:56 -07:00
bmccandless 3c6d90a4db Better error checking on bad params (#1626) 2020-07-16 11:08:27 -07:00
Bruce Martin c592ff35f6 improve graph loading UI (#1636) 2020-07-16 11:07:06 -07:00
Bruce Martin 2265bfc921 render perf refinements (#1632)
* render performance improvements

* improve render perf

* remove logging

* lint

* improve memoziation
2020-07-16 09:53:46 -07:00
Bruce Martin 17c1145ee4 protect against reference detach (#1634) 2020-07-16 09:53:15 -07:00
Bruce Martin 68cc0ae16c simply state management (#1628)
* simply viewport size state management

* lint
2020-07-15 10:31:42 -07:00
Bruce Martin 1269e188be Redux refactor (#1571)
* refactor categorical controls state

* lint

* fix race condition in tests

* fix typo

* add missing update on subset

* remove obsolete code

* update jest and puppeteer major version; update all minors

* update when label changes

* remove lint from tests; increase timeouts in e2e tests

* initial refactoring to new async annomatrix

* refine error handling

* fix bad merge

* add continuous legend

* lint

* fix memoization in color table creators

* partial implementation of user defined annotations

* add new annotations action creator file

* first pass at user annotations

* additional user annotation bug fixes

* user annotation auto-save

* unit test cleanup

* lint

* refactor into multiple files

* cleanup

* add column GC

* fix several bugs in user annotations

* remove debug code

* no anonymous functions

* undo redo cleanup

* file cleanup

* scatterplot

* performance

* cleanup

* remove old code

* render in parallel with load

* fix race condition

* simply graph rendering

* render throttle DRY

* fix category label order

* fix typo in e2e test setup

* re-fix the e2e test setup

* be more tolerant of races

* anno matrix unit tests

* temp disable reembedding

* pilot port continuous histo to react-async

* name change

* lint

* fix repaint bug

* typo fix

* update snap to match new ids

* world/universe name cleanup

* move annoMatrix to src dir

* use private underscore naming convention

* fix corner case in all selected

* name cleanup

* add layout control

* init edge case

* lint

* port scatterplot

* fix label indexing bug and improve tests

* port category to react-async

* fix user annotation labelling while subset

* select all of prev layout on layout switch

* fix race with crossfilter update

* prettier lint

* fix misleading comment

* fix url composition in loader

* first pass at crossfilter tests

* lint

* lint

* fix typo

* improved error handling for network errors

* fix memoization bug

* add memo

* refactor for performnce

* add missing single-value handling in select exact parser

* small bugs discovered by tests

* lint

* additional crossfilter unit tests

* remove extraneous comment

* add support for automatic category determination

* lint

* fix render bug in category

* take advantage of schema categories guarantee

* lint

* do not clear history when resetting

* enhanced annomatrix gc

* lint

* finish renaming to follow conventions; fix clone race bug

* lint

* add priority based loading to improve initial data load UX

* crossfilter cache perf

* perf tuning

* remove timers

* documentation

* PR review changes

* PR review changes

* more PR review edits

* improve clarity of comment

* more PR review fixes

* port centroidLabels to use react-async

* remove dead code

* pr review updates

* oops, remove logging
2020-07-14 13:53:33 -07:00
bmccandless f69d141336 refactor config to support different config options for datasets in different dataroots. (#1596)
This will give us the ability to specify different config options for
different dataroots.

the key of the dataroot dictionary is no longer the same as the dataroot_url.
Previously key==dataroot_url, and now those are separated.

Added an "is_multi_dataset" function to simplify logic where it branched on single vs multi.

Simplified the rest.py interface by no longer passing in the user annotations object, since
that can be retrieved from the dataset.
2020-07-10 16:21:40 -07:00
Bruce Martin 13246cb6d1 add more datasets (#1618)
* add more datasets

* add more about links
2020-07-10 14:55:55 -07:00
Bruce Martin dcbee43b88 add kampmann datasets (#1617)
* add kampmann datasets

* build for production
2020-07-10 05:12:21 -07:00
Severiano Badajoz 45cac75665 Explicitly set Heroku button branch as main (#1609)
Heroku was implicitly pulling the app.json from our deprecated `master` branch.
2020-07-07 16:49:55 -07:00
Severiano Badajoz 75be3a83f9 fix buildpack syntax (#1608)
There was a syntax error in our app.json that was leading to the inability to use the "deploy-to-heroku" button.
2020-07-07 16:27:08 -07:00
bmccandless f3d5081d38 exclude a tiledb version that has a known bug (#1604)
exclude a tiledb version that has a known bug
2020-07-07 08:39:50 -07:00
Matt Weiden e5670d1732 Do not cache index.html (#1603)
* Fix typo

* Run black for code formatting

* Do not cache index.html
2020-07-01 14:35:44 -07:00
Timmy Huang 2b0c4692ba e2e-add-more-logs (#1598) 2020-06-30 21:27:38 -07:00
bmccandless 5015c5ca3f Return float32 for embedding data type (#1597) 2020-06-30 14:16:55 -07:00
Bruce Martin 8898a9817c protect against non-integer array indexing (#1595)
* do not assume non-iteger array bracket access will return undefined

* revert to spec
2020-06-29 09:37:59 -07:00
Timmy Huang b79f09c550 FIX-test-target-closed: remove jestPuppeteer.resetBrowser() (#1592) 2020-06-26 10:02:25 -07:00
Timmy Huang 52a3f8b85a disable-puppeteer-dumpio (#1591) 2020-06-25 19:52:59 -07:00
Matt Weiden 152c07a28e Change default branch from 'master' to 'main' (#1589)
The default branch has been changed. This PR cleans up references to
`master` and fixes links that would otherwise be broken.

For more background see the following references:
* https://www.independent.co.uk/life-style/gadgets-and-tech/news/github-master-slave-slavery-whitelist-language-inclusive-a9568576.html
* https://tools.ietf.org/id/draft-knodel-terminology-00.html
2020-06-25 14:04:30 -07:00
Matt Weiden 0fd3d4b8f4 Add default environment override back to common.mk (#1586)
* As part of https://github.com/chanzuckerberg/cellxgene/pull/1548 we
  accidentally removed the part of the "get_or_else_dev_env_default"
  function that allowed users to override the environment variables.
  This commit adds that back.
* When environment.default was changed from a shell script file to json,
  the shell commands used to parameterize DATASET were not evaluated.
  This commit fixes this issue as well.
2020-06-25 08:25:29 -07:00
83376627e8 1510-smoke-test (#1548)
* 1510-smoke-test

* config default

* update tests

* update test config

* fix linter errors

* more comments

* address comments

* use npm install in push_tests.yml

* use environment.default.json

* adding docs

* Take care of @mweiden's nits

* Save screenshots in the __tests__/screenshots/ directory

* typo

* docs

* Add chart tests (#1580)

* merge tests

* check if bin creation returned null before rendering charts (#1576)

* check if bin creation returned null before rendering charts

* refactor chart rendering into functions (#1577)

* little fixes from PR

* reintroduce fix to check for null values

* change getAllByClass to return element

* slice instead

* new stackedbar test

* feedback-1573-test (#1579)

* feedback-1573-test

* enable whole test set

* revert tests

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

* tweak test to actually render chart

* include snapshot

* remove async

* fix getAllHistograms

* properly grab id

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

Co-authored-by: Matt Weiden <538456+mweiden@users.noreply.github.com>
Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2020-06-24 11:45:39 -07:00
bmccandless e22e671f10 Return 404 when the dataset does not exist (#1581)
Also in this PR, restructure the exception handling.

fixes #1566
2020-06-23 17:14:42 -07:00
Severiano BadajozandTimmy Huang 9e7ecfa279 check if bin creation returned null before rendering charts (#1576)
* check if bin creation returned null before rendering charts

* refactor chart rendering into functions (#1577)

* little fixes from PR

* reintroduce fix to check for null values

* change getAllByClass to return element

* slice instead

* new stackedbar test

* feedback-1573-test (#1579)

* feedback-1573-test

* enable whole test set

* revert tests

Co-authored-by: Timmy Huang <tihuan@users.noreply.github.com>
2020-06-23 11:59:39 -07:00
Severiano Badajoz 0e28df0bd6 Fix Truncation styling issues (#1558)
* fix color inheritance

* tweak width constants

* update snapshot

* Trigger workflow
2020-06-16 10:48:28 -07:00
Donald Paul Herman d6a96e9c29 Improve Lasso 'will close' visual feedback. (#1550)
* Decreased the scope of polygonToPath and distance variables.  Added a
new variable for the lasso path color.  Removed the color setting from
handleDragStart, since this is already set in handleDrag.  Added a
closePathColor "#bb2f00" which is the complementary of lasso path color.
When a lasso can close, the color will change from blue to red.

* Fix some linter issues.

* Fixed some linter issues.

* Fixing some linter issues.

* Changed lasso open and closed to BLUE5 and GREEN5 respectively.
2020-06-16 11:48:29 -04:00
Severiano Badajoz 23619010c3 Componentize bar charts (#1557)
* create miniHistogram Component

* use MiniHistogram

* create MiniStackedBar Component

* Use MiniStackedBar

* update graphs on colorAccessor change

* Move bin creation out of miniHistogram

* rename expressionLabel

* breakout stackedbar bin creation

* Trigger workflow

* move components to individual folders

* rename constant
2020-06-15 11:32:55 -07:00
Colin MegillandSeveriano Badajoz 77ffa0712e If min / max are the same, hide histo + legend, show value (#1554)
* hide legend, histo single value

* display block

* extra check for domain

* Update client/src/components/continuousLegend/index.js

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

* readability on variables, use clipped ranges

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2020-06-11 16:27:02 -04:00
mattcai d6c606b72e minor typo fix (#1553) 2020-06-10 12:57:27 -07:00
Bruce Martin bc7a712048 refactor categorical controls state (#1549)
* refactor categorical controls state

* lint

* fix race condition in tests

* fix typo

* add missing update on subset

* remove obsolete code

* update jest and puppeteer major version; update all minors

* update when label changes

* remove lint from tests; increase timeouts in e2e tests

* changes in response to PR review

* lint

* more PR comment changes

* more PR comment fixes

* lint

* more PR comment resolutions
2020-06-10 12:56:38 -07:00
Severiano Badajoz f063708b33 Add linting rules around ESLint directive comments (#1551)
* add eslint-plugin-eslint-comments and require descriptive configuration

https://eslint.org/docs/user-guide/configuring#using-configuration-comments

* update ESLint

* audit ESLint disables

* update ESLint and related dependencies
2020-06-08 17:31:25 -07:00
Severiano Badajoz 4e96847032 Create Truncation Component (#1500)
* remove ESLint rules

operator-linebreak is no different then default
quotes is overwriting prettier config

* add string-pixel-width dep

* don't lint-staged src, only staged

* add widthMap

* create Truncate component

* refactor in truncate component

* add font load checking

* remove font-family styling

* render Truncate's child instead of creating own component to render

* refactor to use Truncate component

* add span back

* support children

* remove maybeTruncateString

* sub in Truncate component

* add bold prop

* accurately compute largest possible string

* remove logs

* tweak truncation method

* memoize comp function

* explain disable

* fix bugs w/ abs/floor

* tweak widths

* tweak widths

* fix font size

* remove border

* move test-id

* attempt css solution

* Revert "attempt css solution"

This reverts commit aac4d8a6f6.

* CSS solution v2

* remove string-pixel-width

* remove widthsMap

* remove dead code

* remove "data-truncated" as it is always true

* tweak label width

* fix e2e tests

* remove testing string

* e2e annotations tweaks

* correct snapshot

* remove resolves

* check for labels

* add test-id

* format fix

* update snapshot

* fix color

* pull constants out where available
2020-06-05 10:14:42 -07:00
bmccandless 99d004d1f0 add config option to handle multiple dataroots (#1531)
#1513
2020-06-04 19:29:37 -07:00
Matt Weiden df6b42f5d6 Update client development documentation (#1504)
Update documentation
* on installing the server
* on how to get nodejs hot-reloading to work
* on running standard and annotations smoke tests with hot-reloading
2020-06-04 12:34:46 -07:00
Matt Weiden 101f446f8d Add .gitignore file added by elastic beanstalk (#1506) 2020-06-03 09:14:24 -07:00
Bruce Martin 2ba4944f5c initial bug fixes and test improvements for the matrix refactor (#1503)
* initial bug fixes and test improvements for the matrix refactor

* lint
2020-06-02 09:47:40 -07:00
bmccandless 76523d4f32 sparse column shift encoding. (#1502)
Many of our matrices are log normalized, which tends to eliminate
the number of non zero values (if there were any).  This prevents
the matrix from being stored as a sparse matrix.  The solution here
is to use a simple transformation to make it sparse again.  The most
common value from each column is subtracted from that column.  These
values that were subtracted are saved in an array called X_col_shift.

The cellxgene code needs to understand how to undo the transformation when
operating over the X matrix.

- added script to create a synthetic dataset for testing
- added a script to convert an existing CXG dataset to a sparse CXG dataset
2020-06-02 08:23:52 -07:00
bmccandless d0577b94af Return an empty matrix if no rows or columns are selected (#1501)
Return an empty matrix if no rows or columns are selected

Fixes #1499
2020-05-29 14:47:46 -07:00
bmccandless f7585eef1e Support for sparse tiledb arrays for the X matrix (#1496)
Support for sparse tiledb arrays for the X matrix

1. cxgtool can now output sparse matrices
2. cxg_adaptor and diffexp_cxg updated to handle sparse matrices
3. added a test in test_diffexp to test sparse diffexp and get_X_array
2020-05-28 18:36:02 -07:00
bmccandless 030eea1898 Update run_diffexp.py (#1494)
You can now select labels within groups instead of random rows.
2020-05-26 16:06:33 -07:00
Matt Weiden 51758a0be9 Remove the trusted types requirement (#1495)
There seem to be breaking changes in Chrome that are causing this to
fail. We've weighed the risk of disabling the feature with the issues
we've encountered using it and decided to disable it for now.
2020-05-26 12:31:56 -07:00
Severiano Badajoz 271e7a1e76 Fix prettier related ESLint issues (#1493)
* add quotes

* add prettier dev-dep

* run prettier on src

* add eslint-prettier-plugin to install
2020-05-26 09:33:59 -07:00
bmccandless 4d100d4507 Upgrade to tiledb 2.0 (#1485)
* Upgrade to tiledb 2.0
2020-05-21 14:55:11 -07:00
Severiano Badajoz 889b582538 ESLint tooling and enforcement(#1492)
* add eslint-prettier

* add lint-staged

* ignore .eslintcache

* remove unused import

* attempt test change

* Revert "attempt test change"

This reverts commit 78d0e31d41.

* attempt to add error

* Revert "attempt to add error"

This reverts commit a5885c0942.

* add error

* create config

* add comment

* tweak glob

* Revert "add error"

This reverts commit 3187522396.

* add python error

* Revert "add python error"

This reverts commit d0b116ed15.

* lint on push + PR

* Revert "add comment"

This reverts commit efa4eb6676.

* add prettier rules to ESLint

* remove lint-diff targets
2020-05-21 14:27:49 -07:00
Severiano Badajoz 18da8911a0 Change appearance of centroid labels of unselected values (#1491)
* add opacity change on selection state

* change overlay z-index to capture pointer

* https://www.youtube.com/watch?v=xrg-RgF5F8o

* render null if no coloring by continuous

* run lint on push + pr

* revert commit

wrong branch
2020-05-21 09:22:10 -07:00
Matt Weiden 8d762e59ca Deduplicate merged gene list in subsetAndResetGeneLists (#1490)
Fixes https://github.com/chanzuckerberg/cellxgene/issues/1486
2020-05-20 15:35:27 -07:00
Matt Weiden efdae22fbe Lengthen e2e test timeout to account for page load (#1488) 2020-05-20 14:55:43 -07:00
Severiano Badajoz b06b08ce33 Fix centroid label + continuous color-by interactions (#1484)
* add case to removing labels

* add disabled state to centroid button

* fix centroid test button clicking order

* don't accidentally un-toggle first color
2020-05-20 14:02:10 -07:00
Colin Megill bc5b01da0f Break out anno plus button (#1473)
* break out plus button

* remove menu item

* padding, remove icon

* icon padding
2020-05-20 16:22:29 -04:00
Severiano Badajoz ce819383af re-enable front-end lint test (#1483)
* change lint to only src

* reenable lint all
2020-05-19 15:46:04 -07:00
Bruce Martin 2992d17c1d add more detasets to hosted site (#1481)
* add more detasets to hosted site

* fix build
2020-05-19 14:24:27 -07:00
Severiano Badajoz c34a68304e remove all linting errors on client/src (#1463)
* run eslint --fix

* camelcase

* camelCase config part 1

* part 2

* part 3 - removing subscripts

* fix "class-methods-use-this"

* fix "class-methods-use-this"

* fix eslint ignores

* add eslint ignore for set state in update

* reformat comments to appease eslint

* add a11y features

* sort-comp fix

* a11y fix

* add ignore for set state in update

* add a11y htmlFor

* remove unused toast

* remove unnecessary bind

* add ignore for set state in update

* add rel="noopener noreferrer"

Using target="_blank" without rel="noopener noreferrer" is a security risk: see https://mathiasbynens.github.io/rel-noopener

* use arrow function to bind

* remove unused definitions/declarations

* prettier

* remove unused state

* add comments to empty catch blocks remove curly brackets

* escape '

* use eqeqeq

* switch from default export

* remove ignore log

* remove static

* fix import

* revert subscripting config

* clean-up

* remove unnecessary subscript

* fix new errors from master

* change category click handler to a class property

* fix camelcase changes that slipped by

* unused import

* Fix newly introduced ESLint errors from addGenes
2020-05-19 12:38:29 -07:00
Colin Megill 5fc76edf2d Factor adding genes into own component (#1480)
* factor out add genes to own component

* correct import
2020-05-19 11:41:49 -04:00
Matt Weiden e21997799c Upgrade python requirements to click>=7.1.2 (#1472)
6.7 does not have the `hidden` flag used in the code. Users building the
app with an older version of click within the current range specified by
requirements.txt may fail.
2020-05-14 10:04:28 -07:00
Matt Weiden 730410c5e1 Autoformat python to fix lint errors (#1470)
* Autoformat python to fix lint errors

* Fix lint errors not caught by black
2020-05-12 13:19:38 -07:00
Matt Weiden e55595cc55 Fix smoke tests to work with remote deployments of cellxgene (#1469)
* Add test for terms of service

* Add workaround for chromium CSP require-trusted-types-for error
2020-05-12 13:01:57 -07:00
Bruce MartinandMatt Weiden c1bf491a96 Hooks for sentry integration (#1460)
* add sentry webpack plugin

* allow override of webpack config

* work around cheerio inability to parse jinga templates

* webpack can not minify jinja templates

* allow script injection to specify other attributes

* allow script injection to specify other attributes

* Adjustments to make plugin systems work

* Add sourcemaps for javascript in prod webpack

* Update .gitignore

* Fix spelling errors

Co-authored-by: Matt Weiden <538456+mweiden@users.noreply.github.com>
2020-05-08 15:59:56 -07:00
Colin Megill 9026e0ce41 Sci notation on continuous legend (#1450)
* sci notation on continuous legend

* significant digits
2020-05-07 12:59:10 -04:00
Colin Megill 7ec9bb92f7 Create new label button overflow (#1459)
* Tighten up copy

* amp;
2020-05-07 12:58:21 -04:00
Matt Weiden 888f730526 Refactor CSS and React logic for layout (#1424)
* Refactor CSS layout and react logic for layout

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1022

* Menubar should wrap inside middle pane instead of overlapping left
  sidebar when window is scrunched
* cellxgene should have a minimum width of 1240px

1. Replace absolute positioning and dimension calculation with css grid
2. Use flexbox for wrapping menubar buttons

* Middle pane (graph) can calculate its own size
* Removing components calculating their size/position relative to
  eachother increases modularity, decreases use of global variables
* Improved some scrollbar behavior
* Removed responsive reducer, propagating window size to components
  triggers unnecessary events and encourages breaking modularity; doing
  this made some components state agnostic

Reference: https://css-tricks.com/snippets/css/complete-guide-grid/

* Reposition the continuous legend

* Small fixes

* Respond to feedback from @colinmegill

* Respond to feedback from @colinmegill

Add more documentation on the renderGraph method.
2020-05-06 13:46:13 -07:00
Matt Weiden a7d641ea6b Provide ToS button identifiers for smoke test to use (#1462) 2020-05-06 13:45:09 -07:00
bmccandless f49507f18a app_config, fix bug with list/tuple command line arguments. (#1464)
* app_config, fix bug with list/tuple command line arguments.

There was a error caused by pyyaml using lists, and click using tuples.
Now tuples are automatically converted to lists when the config is
updated.
2020-05-06 12:00:02 -07:00
Matt Weiden 5947306ca0 Add server plugin system (#1447)
* Add server plugin system

Plugins are optional modules loaded at runtime. Specification:
* Plugins are loaded from the server.plugins module (directory
  server/plugins)
* The import_plugins method is run as part of the initialization of the
  server module in __init__.py

* Add plugins to the EB build process

* Remove bit of dead code

* Respond to feedback from @bmccandless
2020-05-05 17:05:42 -07:00
Colin Megill 67d7b8160f was 27, 41 was too long, 33 seems ok (#1458) 2020-05-05 15:47:18 -04:00
Colin Megill 630b59d4a7 remove popover (#1456) 2020-05-05 14:34:11 -04:00
bmccandless 55a7967001 Add a process for specifying exact requirements for an EB deployment (#1451)
Add a process for keeping specifying exact requirements for an EB deployment
2020-05-05 10:51:02 -07:00
bmccandlessandSeveriano Badajoz 61ca75e846 hosted, update order to look for config file. (#1452)
* hosted, update order to look for config file.

The app now uses a local config.yaml file bundled with the artifact
(if present), if it exists, then looks in the CXG_CONFIG_FILE
environment variable.  This is the reverse of previous behavior.
The purpose of this change is to move away from using the
config file on s3, since that could lead to problem where an older
version of the app uses a newer version of the config.

Also in this PR:
1. Changed documentation around dataroot, to describe the posibility of using lustre.
2. Added a few improvements around the secret manager region name.  If we use lustre for dataroot and a local config file, then we will no longer be able to
auto determine the region for the secret manager.  I plan to start using the
environment variable option for hosted cellxgene.

* small edit to README

Co-authored-by: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2020-05-05 10:50:28 -07:00
Matt Weiden 1ee76826c7 Pin tiledb version 0.5.9 (#1453)
There is a regression in 0.6.0
2020-05-05 10:03:57 -07:00
Bruce Martin f42f5151a6 Refactor build and CSP headers for Safari compat (#1442)
* add unsafe-inline directive to style-src

* debugging - turn on csp reporting

* revert reporting only csp

* do not inline JS and CSS in build

* enable HTTPs only when in production mode

* remove debug printf

* fix clean target

* revert force_https removal
2020-05-04 12:47:35 -07:00
Severiano Badajoz 6cccc41c0f run prettier(2.0.5) (#1438) 2020-05-04 10:26:42 -07:00
bmccandless b255e32548 Add a more consistent way to customize the eb deployment (#1446)
* Add a more consistent way to customize the eb deployment

Plus a few minor bugfixes
2020-05-04 09:52:03 -07:00
Matt Weiden a17fff83cf Return empty colors for .cxg v0.0 files (#1441)
* Return empty colors for .cxg v0.0 files

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1440

The CxgAdaptor.get_colors method currently assumes that the .cxg file has
cxg_group_metadata. As a result, the /api/v0.2/colors endpoint always fails for
.cxg v0.0 files.

* Add test fixture
2020-04-30 17:21:20 -07:00
Matt Weiden 40fbc42b13 Cache crossfilter when embedding type is changed (#1439)
Fixes https://github.com/chanzuckerberg/cellxgene/issues/1433

When selection is deselected the world is reset to the universe the
crossfilter is reset to that in the resetCache, including the embeddings
in the layout_XY dim.  However, the embedding selection stays the same.

If the embedding selected is not the default, the embedding shown to the
user will be different than the embedding layout_XY in the crossfilter,
causing lasso selections to be made against the wrong embedding
coordinates
2020-04-30 13:48:14 -07:00
bmccandless cd821a0344 Update the EB build process to package in scripts (#1434)
This is implemented specifically to handle the google analytics
script for hosted cellxgene
2020-04-28 14:38:35 -07:00
bmccandless e495c571c3 Change the EB log level from DEBUG to INFO (#1435)
The log level was mistakenly set to DEBUG, which sent out tons
of less than useful messages.  INFO works much more useful,
and much less verbose.
2020-04-28 14:37:41 -07:00
Bruce MartinandColin Megill 05fcdaf93c Revised terms and privacy consent dialog, analytics hooks (#1426)
* revised terms and privacy consent

* reorg code

* fix conditional

* Overlay reflects un-dissmissable state

* add inline scripts, and consent callback

* add csp_directive config hook

* revert config.yaml

* fix logic error

Co-authored-by: Colin Megill <colinmegill@gmail.com>
2020-04-28 14:21:53 -07:00
Madison Dunitz 666e6d9849 Add Deployment hook (#1430)
* test deployment hook

* remove test branch

* update heroku review deployment
2020-04-27 13:01:31 -07:00
Matt Weiden 546e272a60 Add user-defined category-label colors (#1402)
* Add user-defined category-label colors

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1152

As described in https://github.com/chanzuckerberg/cellxgene/issues/1307

* Respond to feedback from @bkmartinjr in nodejs

* Respond to feedback from @bkmartinjr in python

* Add tests to the server module

* Autoformat python, run linter

* Make colors_get error handling specific

* Respond to feedback from @bkmartinjr

* Respond to feedback from @bkmartinjr

* Fix whitespace

* Fix python lint errrors

* Update documentation

* Add --disable-user-colors option to launch and cxgtool.py

* Fix python formatting

* Rename '--disable-user-colors' to '--disable-custom-colors'
2020-04-26 22:52:57 -07:00
Bruce Martin 3c191c3f93 add dsuid 25 (#1429) 2020-04-26 07:53:49 -07:00
Eduardo LopezandMadison Dunitz 565a3f148d [feature] Prototype a Heroku deployment of hosted cellxgene (#1427)
* hello world

* stuff

* successful build

* updates"

* maybe a basic example

* simplify

* reamde into dockerfile

* some more stuff

* Release procfile

* package.json at top levle

* don't release in procfile for now

* more package.json stuff

* copy assets

* merge master

* not in the relase phase

* revert not necessary

* pin gunicorn version

* reset common.mk

* Update package.json

Co-authored-by: Madison Dunitz <dunitzm@gmail.com>
2020-04-24 12:44:38 -07:00
Bruce Martin d31b7dc797 clamp brush selection range to data range (#1423) 2020-04-24 09:24:01 -07:00
Bruce Martin 735eb11eb7 refinments to csp (#1422) 2020-04-23 09:54:09 -07:00
Matt Weiden a2047b90ae Make the URL hit by jest smoke tests configurable (#1420)
* Make the URL hit by jest smoke tests configurable

* Fix typo

* Make names consistent
2020-04-22 15:23:11 -07:00
Bruce Martin 9b12b729c9 more data (#1419) 2020-04-21 18:22:52 -07:00
Bruce Martin 8742f9094e cleanup (#1411) 2020-04-20 09:56:26 -07:00
Severiano Badajoz 8b59755157 increase category label max length (#1394) 2020-04-20 09:48:47 -07:00
Bruce Martin cbcd3f4f8f ensure crossfilter index dimensions match data (#1409) 2020-04-19 08:44:46 -07:00
Bruce Martin 5dc40158c4 disable eslint in github workflow (#1412) 2020-04-18 09:06:05 -07:00
Bruce Martin 55ef1448e8 CSP content hashes (#1406)
* remove duplicate content-type header

* plumbing to compute CSP content hashes

* add logging of missing CSP hashes

* convert sub-class init protocol to static

* factor function

* lint
2020-04-17 11:42:09 -07:00
Bruce Martin d22300e3db fix typo in cxgtool bool handler (#1404) 2020-04-17 11:11:14 -07:00
bmccandless 03a75ec566 s3 listings were cached, preventing new datasets from being accessible (#1408)
Fixes #1407
2020-04-17 10:14:24 -07:00
Matt Weiden 58e4c2489b Auto format python, make sure it passes lint checks (#1401) 2020-04-16 08:07:10 -07:00
bmccandless 7e7ed74b92 Provide a hook into the AWS Secret Manager for the flask secret key (#1398)
Also, the secret manager required a region name, so there was some
refactoring around how regions are handled.

Fixes #1239
2020-04-15 14:33:40 -07:00
Matt Weiden 95ade476e8 Add end of line marker to lint diff regexp (#1400)
This prevents json files from being linted
2020-04-15 13:46:36 -07:00
Colin Megillandbkmartinjr ea434fc46f Handle loading state when rendering categories with one label (#1393)
* refactor & loading state

* truncate

* fix minor PR review issues

* lint

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
2020-04-15 14:38:44 -04:00
Matt Weiden 7d4d360e52 Refactor developer convenience scripts (#1377)
* Cleanup the backend-dev convenience method

* Add the frontend_dev convenience method

frontend_dev is a soup-to-nuts convenience method for setting up the FE
development environment with node running a the client code on port 3000
with the a separate cellxgene package serving the API over port 5005 in
the background.

The script can be run from Finder.

* Update the developer scripts documentation

* Remove the 'test' make target in the client Makefile

Rationale:
* Given how long the smoke tests take to run, it is unlikely that
  developers will want to run all tests together.
* It is unlikely that developers will have set up the backend server
  properly for the tests to pass.
* Available commands should be safe-ish and not lend themselves to
  confusing errors.
* You can still group tests by concatenating them in a make command, as
  in `make unit-test smoke-test`.
* This target isn't used in any of our CI pipelines -- KISS.

* Some version of python3...

* Minor typos in docs

* Respond to feedback from @bkmartinjr

* Make adjustments so that DATASET path is predictable

* Simplify environment defaults a bit
2020-04-15 10:32:19 -07:00
Severiano Badajoz e5dfd6e8fa Add eslint steps to lint job (#1335)
* add lint-client and lint-diff-client targets

* add lint-diff and lint targets

* prettier

* add lint-diff call to lint task

* temp

* tweak lint-diff

* add lint for PRs and lint for master

* remove temp

* remove incorrect branches syntax, use github_ref

* pull all branches

* format

* proper target and comment

* create separate steps with conditionals

* fix indentation

* refactor lint->lint-server, introduce lint to lint all

* trade diff-index for diff, do check against base instead of master

* remove fetching all branches

* Revert "remove fetching all branches"

This reverts commit 26ce7a0f05.

* tweak comparison

* use local eslint

* add eslint  dep install

* temp

* grab only base

* simplify fetch

* add pull_request type trigger

* specify pushes only to master

* change conditionals to be based on event name

* Revert "temp"

This reverts commit 3d59134cc0.

* "branch" => "branches"

* create separate installation step

* change command based on os
2020-04-15 09:41:45 -07:00
Colin Megill 64bd908c25 Render categories with one label without checkbox (#1375)
* categories with 1 item rendered without checkbox

* break out into helper functions

* prettier
2020-04-13 23:04:23 -04:00
Bruce Martin a3df1a9f4a performance and correctness fixes to brushable histogram (#1390) 2020-04-13 20:03:46 -07:00
bmccandless 5c0b8c6296 Improve diffexp for tiledb (#1388)
* Improve diffexp for tiledb

- The rows from the A and B sets are gathered and processed at the same time.  In this
  way the matrix is only accessed once instead of twice for each tile.
- There is now a single thread queue that gets shared between all callers of the diffexp.
  This will slow down work if diffexp gets too busy.
- There is a target_workunit amount of work given to each thread.  Previously the
  workunit was (rows selected * width of tile), which could be small.  Now multiple
  column tiles can be combined into one workunit.  If the target is too small then
  thread and other overheads may reduce performance.  If target_workunit is too large
  then the size of the gathered sub matrix may take up too much memory.
- add configuration parameters (max_workers, cpu_multiplier, and  target_workunit)
2020-04-13 18:53:13 -07:00
Bruce Martin 136093d583 add GET routes for expression data (#1387)
* add GET routes for expression data

* fix comment typo
2020-04-13 11:28:35 -07:00
bmccandless 98c2a1359b Specialize diffexp for tiledb (#1384)
* Specialize diffexp for tiledb

This patch adds a new diffexp algorithm which is tuned for tiledb.
This algorithm was written by Bruce and is adapted here to plug into the
current framework.  The anndata_adaptor still calls the original
algotithm (which was move from diffexp.py to diffexp_generic.py).
The cxg_adaptor now calls the new diffexp_tiledb version.  Some
code is shared between the two.

This is part 1 of the diffexp for tiledb.  Further tuning and
global throttles are still needed.

A script to run and time diffexp with various options is also
added: test/run_diffexp.py.
2020-04-12 09:56:55 -07:00
bmccandless 5dcfcb5467 s3 region should have a single config param (#1386)
* s3 region should have a single config param

The s3 region can also now be automatically determined to further
reduce errors.

This patch also fixes a bug with order of handling the config params.
The tiledb config needs to be fixed before attempting to load
(need to handle_adaptor before handle_single_dataset)
2020-04-11 15:20:01 -07:00
bmccandless b3c26e7bc7 Allow diffexp for hosted cellxgene (#1385) 2020-04-11 13:15:26 -07:00
Bruce Martin bff5745139 disable SSL cert verification (#1374) 2020-04-10 12:58:51 -07:00
Bruce Martin cfacdb15a8 fix typo (#1379) 2020-04-10 12:58:30 -07:00
Bruce Martin 2d2a7c2108 bump cache control max_age to one week (#1380) 2020-04-10 12:58:09 -07:00
Bruce Martin 61b330b9b0 fix non-finite condition in mean/var calculation (#1381) 2020-04-10 12:52:02 -07:00
Matt Weiden 14021db239 Clean up main Makefile (#1376)
* Remove unnecessary make target alias

* Remove installing from build directory

* Update documentation
2020-04-09 17:35:49 -07:00
Bruce Martin 0398249a20 CXG performance improvements (#1371)
* initial kv cache

* add per-key locks

* comments

* memoize schema

* add missing initialization

* fix sever timing

* fetch only what is requested

* fix tests to not require strict ordering of columns

* clean up annotation request

* remove debugging print
2020-04-09 10:19:26 -07:00
Bruce MartinandColin Megill e2a12ba9bb diffexp limit UI and configuration (#1336)
* warning on maxCount for diffexp

* cleanup logging

* clarification

* make the limits configurable

* make diff exp limit work

* danger!

* remove debugging code

* fix merge with master

* fix unit tests

Co-authored-by: Colin Megill <colinmegill@gmail.com>
2020-04-09 07:13:41 -07:00
bmccandless d457988810 Update for app config (#1369)
fixes an issue with "cellxgene launch" which had a bad interaction between
command line parameters and config file parameters.
Now, the config files are applied first, followed by the parameters that
were provided in the command line.

There is also now a check that each of the config attributes is type checked.
2020-04-08 15:44:56 -07:00
Matt Weiden 1f94c71c22 Use sci notation for axis ticks abs(tick) >= 10000 (#1363)
Fixes https://github.com/chanzuckerberg/cellxgene/issues/1349

For more information see https://github.com/d3/d3-format

Note that does not _fully_ fix the issue described in #1349, but rather
makes the formatting issue far less likely. It is _still_ possible for
this to occur if the difference between two ticks in axes happes in the
a significant digit cropped by the scientific notation format
2020-04-08 09:45:32 -07:00
Bruce Martin b466d952f9 add config and init for server timings (#1368)
* add config and init for server timings

* add missing config error check
2020-04-07 15:08:57 -07:00
Bruce Martin bcacb75296 load embeddings in parallel (#1352)
* load embeddings in parallel

* correctly capture unclipped

* test

* another test

* add convenient copy assets target

* cleanup
2020-04-07 09:48:07 -07:00
Bruce Martin 3b341a7191 add hcl dataset (#1366) 2020-04-07 07:51:32 -07:00
Bruce Martin 6193ae4997 Cleanup front-end build and python module contents (#1350)
* cleanup build and module contents

* lint

* update chalk

* more cleanup

* fix unit test
2020-04-06 19:04:06 -07:00
Severiano Badajoz 4efabf5231 refactor to remove non-standard terms in centroid files (#1330)
* refactor to remove non-standard terms

* switch key/value -> label/coords

* fix missing parenth
2020-04-06 10:20:11 -07:00
Bruce Martin ffa95b5a55 update s3fs requirements (#1348) 2020-04-05 09:22:32 -07:00
Bruce Martin 0d9f24115c cziscience landing page (#1347)
* fix dead link in launch page

* first cut at landing page

* wording changes

* add additional refinements

* fixes from PR review
2020-04-03 16:30:02 -07:00
Bruce Martin de8033e551 correctly handle categoricals that are not strings (#1346) 2020-04-03 15:50:02 -07:00
Matt Weiden 4117050081 make install should fail if there's no build dir (#1344)
Currently, if there is no build directory, the `make install` target
will attempt to cd into the build directory, fail, and run pip install
-e . in the root directory anyway. This causes cellxgene to be installed
from the source tree instead of what the user would expect.

This commit changes the behavior such that the `make install` will fail
if there is no build directory.
2020-04-03 15:09:54 -07:00
Bruce Martin 7c56041b11 categorical type handling fix (#1342)
* fix numeric category conversion bug

* correctly compute categorical summaries

* lint

* remove debugging print
2020-04-02 14:44:07 -07:00
bmccandless 308ee64f30 Improvements to the matrix cache (#1340)
* Improvements to the matrix cache

- Add a timelimit for the matrix in the cache.
Once the timelimit is reached, the matrix can be removed.

- If a DatasetAccessError occurs, then remove the dataset
from the matrix cache.

Fixes #1322
2020-04-02 13:44:11 -07:00
Bruce Martin fa0164fdf6 use no-store cache control (#1337) 2020-04-02 09:20:35 -07:00
Bruce Martin c2a24ae0c6 add cache control headers (#1333)
* add cache control headers

* add config param check
2020-04-01 15:43:43 -07:00
Bruce Martin 33ce95ba09 work-around for column name encoding bug (#1324)
* add attribute cleanup

* fix typo

* handle mappings

* logging

* fix regex

* remove debugging printfs

* update masked characters

* fix typo

* add missing incr
2020-04-01 15:06:21 -07:00
Bruce Martin 708a5af039 Add fetch concurrency limit on obs annotation loading (#1318)
* add fetch concurrency limit

* add tests for PromiseLimit
2020-03-30 16:23:09 -07:00
Severiano Badajoz 033727632c add truncation to loading (#1320) 2020-03-30 15:48:52 -07:00
Bruce Martin d4ec976aaa update locust routes to match new server URL structure (#1321) 2020-03-30 15:09:45 -07:00
bmccandless 03d7a107bf Fix for s3 region name in data locator (#1316)
There is a small chicken and egg problem.
The config file could be in s3, therefore when using the DataLocator to
download the config file, we don't yet have an app_config object.
Adding a check to handle this case.
2020-03-29 09:20:34 -07:00
Bruce MartinandColin Megill c8f98917c5 tos prompt (#1313)
* tos toast

* finish ToS prompt

Co-authored-by: Colin Megill <colinmegill@gmail.com>
2020-03-28 22:31:19 -07:00
Bruce Martin 8fac40b6ae various fixes for s3fs use (#1312)
* various fixes for s3fs use

* lint
2020-03-28 22:26:03 -07:00
bmccandless 54a75ffd7e Add support to include static files into the eb deployment (#1305)
Mostly this is just instructions for how to do this,
with a small addition to the makefile.
This enables support for serving the about_legal_tos and about_legal_privacy
from the cellxgene server.
2020-03-26 15:03:37 -07:00
bmccandless 8c0480b0bc Added a config hook for secret key into the app. (#1304)
* Added a config hook for secret key into the app.

the server first looks in an environment variable,
then looks in a config file.

For the cellxgene launch app, a default key is used if none is provided.
For the eb app, a secret key must be provided.
2020-03-26 15:03:08 -07:00
Bruce Martin a398f85ccc turn off auto-reload in debug mode (#1301) 2020-03-26 13:34:56 -07:00
Bruce Martin 57938b487b remove static route redirects / bump static to top level (#1299)
* add another favicon rule

* catch only top-level dataset names

* add config flag to disable redirects

* remove static redirect, point all links to top-level /static

* remove extraneous argument

* remove httpd redirect
2020-03-26 10:45:10 -07:00
bmccandless 40c2e4c164 Change route to the datasets (#1302)
Previously is was /<dataset>/...
Now there is an extra levle:  /d/<dataset>/...
2020-03-26 10:35:17 -07:00
Colin Megill adf7010f50 Add TOS and Privacy url via config (#1300)
* add tos and privacy url via config

* typo

* readd condition

* param
2020-03-26 11:33:41 -04:00
bmccandless 91e17e64a5 Fixes for static_rewrite.config (#1298)
replace tabs with spaces (yaml parsing complained).

change to favicon.png.
2020-03-25 17:44:15 -07:00
Bruce Martin 26605049a4 Various hardening to REST routes (#1293)
* URL reweriting for static

* request size limits

* improve quotas, make tests work

* remove debugging code

* pass limits to front-end

* fix renaming boggle
2020-03-25 16:14:52 -07:00
bmccandless 7b53209ae3 Fix a deadlock when an exception is thrown during matrix validate/open (#1296) 2020-03-25 15:49:49 -07:00
Bruce Martin 674f14f9bd harden dataset name handling (#1297) 2020-03-25 15:37:06 -07:00
Matt Weiden e07ebf85e8 Small documentation fix in prepare (#1292)
See sample of current behavior below:
```
venv❯ cellxgene prepare example-dataset/pbmc3k.h5ad
[cellxgene] Starting CLI...
Error: [cellxgene] cellxgene prepare has not been installed. Please run
`pip install cellxgene[prepare]` to install the necessary requirements.

~/workspace/cellxgene mweiden/446-custom-color-palette*
1 venv❯ pip install cellxgene[prepare]
zsh: no matches found: cellxgene[prepare]
```

Fix:
Wrap cellxgene[prepare] in single quotes.
2020-03-25 14:38:07 -07:00
bmccandless 5ec66c5b0e Avoid race condition in the matrix cache handling. (#1280)
* Improved fix for matrix cache handling.

During the MatrixDataCacheItem acquire function there was a
time when the write lock was released and the read lock was taken.
During that time, the dataset could have been deleted, later
result in the MatrixDataCacheManageri data adaptor returning None.

The solution is to demote the writer lock to a reader lock instead
of unlocking and relocking.

Also, when a the cache needs to delete an entry, the delete
is done outside the MatrixDataCacheManager lock.   This operation
only requires the write lock for the MatrixDataCacheItem.

Fixes #1255
2020-03-24 12:23:47 -07:00
bmccandless bea1836386 hosted cellxgene, add an environment variable to specify the config file (#1288)
Fixes #1272
2020-03-24 11:43:35 -07:00
Bruce Martin 752b9e4ab3 CORS and CSP headers (#1286)
* do in-app compression only for CLI

* CORS and CSP headers

* lint

* add --debug to targets

* lint

* fix botched merge with master
2020-03-24 08:05:37 -07:00
Bruce Martin d196cbc8f6 do in-app compression only for CLI (#1285) 2020-03-24 06:36:40 -07:00
Bruce Martin 96b092f0eb fix health root exception (#1283)
* fix health root exception

* remove extraneous lambda
2020-03-23 15:13:32 -07:00
bmccandless 785bf5b5bc update for the legal configuration options (#1281)
* Configuration for legal information
2020-03-23 13:25:17 -07:00
Severiano Badajoz b249df9036 add/fix label tooltips (#1278)
* Move tooltip to right

* Change function to only return truncated string

* add tooltip to category label

* use position instead of PopoutPosition
2020-03-23 10:28:54 -07:00
Bruce MartinandMatt Weiden d99b84ba09 prepare - work around anndata bug (#1260)
* work around anndata bug 344

* fix accidental cut and paste error

* Use modified make_index_unique function

Temporarily copy code from https://github.com/theislab/anndata/pull/345
until the issue is resolved and released.

* Add notes and test for make_index_unique

* Lint fix

* Format python

Co-authored-by: Matt Weiden <538456+mweiden@users.noreply.github.com>
2020-03-22 12:27:59 -07:00
Bruce Martin db7a485796 tighten up error reporting (#1269)
* black reformat

* tighten up error reporting

* lint

* fine tuning

* additional improvements in exception handling

* lint

* include exception and traceback in log

* fix typo
2020-03-22 09:55:47 -07:00
bmccandless 8180be83b8 Introduce a config file to cellxgene (#1264)
* Introduce a config file to cellxgene

The config file format is in yaml.  The default config is located
in server/common/default_config.py.  A user may create a yaml file
that contains a subset of these fields.  It can be used during cellxgene
launch, or for hosted cellxgene.

The code has also been refactored.  Much of the logic to check arguments
has moved from launch to app config.

It is now possible to set the tiledb context parameters using the config
file.  Other feature will soon be handled in a similar way.
2020-03-22 09:34:11 -07:00
Bruce Martin 1351c8f724 restrict methods on all endpoints (#1271)
* restrict methods on all endpoints

* lint
2020-03-22 08:23:28 -07:00
Colin Megill c4731d52e2 target blank (#1254) 2020-03-20 20:07:51 -04:00
Bruce Martin de006f8f91 initial implementation of a health check route (#1262)
* initial implementation of a health check route

* remove debugging code

* lint
2020-03-20 14:21:32 -07:00
Bruce Martin 040dbd3a95 refine load test (#1257) 2020-03-20 14:09:12 -07:00
Bruce Martin 8f09a4b1bc JS package fixes (#1248)
* Remove accidental package install from build

* remove accidental package install
2020-03-18 19:00:28 -07:00
bmccandless e4bf65c54a Improve hosted cellxgene (#1234)
* Improve hosted cellxgene

 - option to turn off the test index page, or supply a page for redirect.
   For EB, The default is to return 404.  For cli launch, the default is the test page.

 - option to select which matrix types are allowed for multi dataset servers.
   For EB, The default is CXG only.  For cli launch, the default is any matrix type.

 - Return early with an error response if diffexp is requested when not configured

 - Verified that reembedings and user annotations also return with an error response
   if used when not enabled.

TODO:  The new options cannot currently be set by the user.
I plan to add a configuration file where these and all other settings can be set.

 Fixes #1210 
 Fixes #1228  
 Fixes #1229
2020-03-18 16:21:03 -07:00
Bruce Martin 38c4ecac8c locust load test script (#1244)
* initial version of a locustfile driver

* README
2020-03-18 11:27:07 -07:00
bmccandless 8a733270d7 fixes for hosted cellxgene (#1227)
* Fix for favicon with --dataroot

* fix static assets in hosted cxg 

The web proxy at aws eb was not finding the static assets.

The solution here is very simple: just copy the directory
containing the static assets to the top level of the artifact.zip.

This is not really the ideal solution.  According to the AWS
docs you can make a mapping to the correct location in an
an ebextentions config file.  I tried this and many combinations but
was not able to get this to work following that pattern.

Since we control the construction of the zip file, the solution
here isn't bad, but it could probably be made better.
2020-03-16 09:47:01 -07:00
Matt WeidenandDonald Paul Herman 86cbe64ae8 Add color mapping to the bar chart (#1218)
* Add color mapping to the bar chart.

* Change histogram color generation from Viridis to InterpolateCool.

* Coloring of Histrogram based up the x axis instead of y axis.

Coloring of Histrogram based up the x axis instead of y axis.

* Respond to feedback from @colinmegill

* Only color histograms that are selected for colorby
* Add some small refactors to BrushableHistogram's componentDidUpdate

* Fix histogram coloring and binning

* Reuse binning functions from util/dataframe/histogram.js; this fixes
an issue with there being near-zero width bins
* Fix color mapping so that it matches the scale in the legend
* Do not attempt to plot bins if the calculated binWidth is zero; this
can happen if all values are the same
* Refactor the function that draws the histogram a bit

* Respond to feedback from @bkmartinjr

Co-authored-by: Donald Paul Herman <Donaldpherman@hotmail.com>
2020-03-14 15:24:52 -07:00
Bruce Martin 1e8d89f8f2 refinements for 0.15 (#1226) 2020-03-13 16:30:47 -07:00
Severiano Badajoz 9c2f57df39 bump version to 0.15.0 (#1222) 2020-03-13 13:11:12 -07:00
Bruce Martin 9bc83fdd02 update contributor process recommendations (#1213)
* update contrib process recommendations

* fix typos

* clarification

* fix build instructions

* updates suggested by Matt

* fix dead link
2020-03-13 11:41:30 -07:00
bmccandless bbfd2e5ac5 Add a simple annotations cache to the server (#1220)
Reading the user annotations had a performance problem.
The same file was read over and over.  Now, the last set of user
annotations are cached.
2020-03-12 20:06:44 -07:00
Bruce Martin f28bd52d79 fix performance regression caused by excessive rendering (#1219) 2020-03-12 15:05:08 -07:00
Bruce Martin 63f1f38f7c use AnchorButton so the tooltip dismisses (#1217) 2020-03-12 08:36:36 -07:00
Severiano Badajoz 3ba4cf4483 fix bug created from master rebase (#1216) 2020-03-11 15:20:20 -07:00
Matt Weiden a862f2b2a4 Further refine compatibility tests (#1215)
* Make sure apt is up to date before pulling hdf5

* Only install py dev reqs in cxg release vs anndata master test

* Don't need bu flag when using sed on ubuntu

* Don't re-install package reqs in python x anndata ver tests

* Minor documentation fix
2020-03-11 14:16:05 -07:00
Severiano Badajoz 3bf72829c2 pass empty coordinates when a continuous annotation is colored (#1212) 2020-03-11 13:01:14 -07:00
Severiano Badajoz 7cd9a0032a optimize centroid util (#1147)
* move unvarying evaluations outside of loop

* refactoring

* remove perf checks

* commenting

* minor fix + renaming

* small fix

* benchmarking

* merge master

* Revert pref checks

* renaming and comment

* remove redundant sets in Map
2020-03-11 12:35:42 -07:00
Sidney BellandMatt Weiden 9089fc98f2 annotations cli updates (#1190)
* Switch logic from `--annotations` to `--disable-annotations

* Rename `--annotations-file` --> `--annotations-input-file` and remove `experimental`

* update docs

* update makefile

* update tests

* Disable annotations on standard client smoke test

* Update docs/posts/annotations.md

Co-Authored-By: Matt Weiden <538456+mweiden@users.noreply.github.com>

* Update docs/posts/annotations.md

* Renaming

* Docs

* Update tests

* Pesky typo -_-

Co-authored-by: Matt Weiden <538456+mweiden@users.noreply.github.com>
2020-03-11 11:57:52 -07:00
Bruce Martin 8beeb57c96 Use DataLocator consistently on backend (#1208)
* allow DataLocator to accept another locator as init param

* migrate to DataLocator

* migrate to DataLocator

* lint

* migrate to DataLocator

* add check for erroroneous use of remote path and annotations

* lint

* revert default data location - now back go CWD

* remove unused import
2020-03-10 19:12:49 -07:00
Colin MegillandMatt Weiden 234f25b782 Conditionally truncate category string (#1206)
* maybe truncate string

* add string formatting to test

* correct import

* destructuring

* add maxlength

* test

* Respond to feedback from @bkmartinjr

Co-authored-by: Matt Weiden <538456+mweiden@users.noreply.github.com>
2020-03-10 18:14:26 -07:00
Bruce Martin 28ddb60b2f update JS deps (#1203) 2020-03-10 10:59:12 -07:00
Colin Megill 1a93068810 Prevent category collapsing on annotation edit, # of cells on add label button (#1204)
* collapse functionality

* expand collapse
2020-03-10 13:21:06 -04:00
Matt Weiden d8231a9a3b Fixes to compatibility tests (#1205)
* Unpin anndata ver in tests against anndata master

* Add branch config to work on push

* Add links to tests in GitHub Actions status badges

* Remove temporary branch name
2020-03-10 08:19:36 -07:00
Bruce Martin ffdcc24a40 remove pinned tables dependency (#1202) 2020-03-10 07:47:54 -07:00
Severiano Badajoz 023a73cab8 smoke-test centroids + graph overlay layer (#1189)
* add test labels

* prettier + add clcik return to clickOn()

* prettier + begin test

* finish label counting test

* add util to get coordinates of element

* add test id to labels

* add test to check overlay transform

* remove logs

* rename to match master
2020-03-09 17:01:22 -07:00
Bruce Martin 144b19c449 experimental re-embedding (#1186)
* first cut at re-embedding route and back-end support

* update and expand config route tests

* add scanpy_umap

* add reembedding to config route parameters

* front-end support for reembedding fetch and UI

* remove unused imports

* add loading state

* save reembedding in reducer state

* improve withColsFrom

* transmit reembed schema to client; pick unique embedding names

* display embeddings

* format

* lint

* spaces, tab size 2

* lint

* test hack for smoke-test race

* back out hack sleep

* add check for backed mode

* add unit test for reembedding

* lint

* hide re-embedding CLI param from help
2020-03-09 16:53:30 -07:00
bmccandlessandbkmartinjr b3e9719602 hosted cellxgene (#38) (#1200)
* early, non-working eb config

* hosted cellxgene

In this PR, contains scripts and instructions for deploying cellxgene
for AWS elastic beanstalk.  It supports the multi-dataset option.

The Makefile in the server/eb directory creates an artifact.zip
file, which can be deploy at AWS EB.

The server/eb directory contains:
  app.py - flask app to run the server
  Makefile - which creates an artifact.zip file which can be deployed.
  README.md - instructions for setting up and deploying the eb app.

* hosted cellxgene (#38)

In this PR, contains scripts and instructions for deploying cellxgene
for AWS elastic beanstalk.  It supports the multi-dataset option.

The Makefile in the server/eb directory creates an artifact.zip
file, which can be deploy at AWS EB.

The server/eb directory contains:
  app.py - flask app to run the server
  Makefile - which creates an artifact.zip file which can be deployed.
  README.md - instructions for setting up and deploying the eb app.

* Update how artifact.zip is created

prune the server/test and server/eb directories

* Remove debugging print statements

* fixes from review comments

* fix lint

Co-authored-by: bkmartinjr <bruce@chanzuckerberg.com>
2020-03-09 12:08:07 -07:00
Matt Weiden bdd69160e2 Do not over-prune var data cache (#1198)
* Remove unused variables and imports

* Simplify conditional

* Fix typo

* Do not overprune var data cache

There is a bug in how the universe and world gene sets are constructed
and passed to `ControlsHelpers.pruneVarDataCache` that causes the var
data cache to be over-pruned. This commit fixes the issue.

Consider the following example from the node console:

```
❯ node
Welcome to Node.js v13.5.0.
Type ".help" for more information.
> new Set([1], [2], [3])
Set(1) { 1 }
```

What we really want is the set `Set(3) { 1, 2, 3 }`, which can be
constructed as:

```
> new Set([].concat([1], [2], [3]))
Set(3) { 1, 2, 3 }
```
2020-03-05 18:04:14 -08:00
Matt Weiden bddf66d62a Use blueprint "disable" property to indicate disabled buttons (#1197)
* Use blueprint "disable" property to indicate that colorby disabled

* Disable subset and reset buttons when action is not possible
2020-03-05 09:46:41 -08:00
Matt Weiden 7b77bf4bdd Make smoke tests faster, more stable (#1195)
* Refactor smoke tests & utils for conciseness/style

* Modularize test utilities
2020-03-04 16:11:32 -08:00
Severiano Badajoz cc890fe391 fix url launching on Heroku deployments (#1196)
* fix improper branch link

* set app to use dataset link

* temp repo change

* revert to master

* add options var

* tweak option order

* remove options config arg
2020-03-04 13:05:11 -08:00
Severiano Badajoz 62e944796b enable camera interaction on centroids (#1160)
* pass handleCanvasEvent to graphOverlayLayer

* dont pass handlecanvasevent if not interactive
2020-03-04 11:31:30 -08:00
Matt Weiden 1547a864c2 Disable ColorBy button for truncated categories (#1191)
* Disable ColorBy button for truncated categories

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1156

For categories that have more than 100 labels we truncate the labels in
the UI, but still allowed users to ColorBy these categories. Coloring by
these categories can cause browsers to get bogged down.

This commit disables ColorBy for truncated categories.

* Minor documentation spelling and typo fixes

* Respond to feedback from @liaprins-czi

* Respond to feedback from @colinmegill and @bkmartinjr
2020-03-04 09:19:12 -08:00
Matt Weiden 05323ae643 Undo selection appends genes from differential expression to user gene list (#1183)
* Undo selection appends diffExp genes to user gene list

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1171

Need: When a user performs a differential expression from within
a sub-selection (world) of the data and then resets the selection to all
cells (universe), the differential expression results are no longer
valid.

Approach:
* When the selection is reset, move the top (maxUserDefinedGenes
- len(userDefinedGenes) from the differential expression results to the
list of user defined genes
* Raise maxUserDefinedGenes to 25 to give users more room and
accommodate the extra genes transferred in from differential expression

Other commits:
* Choose different button icons
* Add diff exp genes to user defined genes on subset too
* Respond to feedback from @liaprins-czi and @bkmartinjr
2020-03-02 12:07:12 -08:00
bmccandless 87efcc0e99 Fix a few bugs from server refactor (#1188)
- Fixes #1185 port command line argument was not handle correctly
 - Fixes #1184 was not catching the "DatasetAccessError" exception.
2020-03-02 11:55:57 -08:00
Bruce Martin bf7d7342d5 Improve label picking (#1179)
* add simple error message helper

* port all label name pickers to use the new LabelInput component

* use pure components where possible

* cleanup

* more cleanup

* lint

* change new label prompt
2020-02-28 15:46:40 -07:00
Matt Weiden 1e4381ab7f Make the Clip button primary (blue) (#1181)
Fixes https://github.com/chanzuckerberg/cellxgene/issues/1101

I can't reproduce the active/inactive issue described in #1101

The only change remaining in #1101 is to make the clip button blue.
This commit makes that change.
2020-02-26 16:54:30 -08:00
Matt Weiden 8b0adcdfcc Add codecov configuration (#1180)
* Add codecov configuration

* Modify workflow
2020-02-26 14:57:46 -08:00
Severiano Badajoz 0043308b52 add centroid util unit tests (#1172)
* scaffold the test

* create tests
2020-02-26 12:16:13 -08:00
Matt Weiden 8a62c9f027 Add undo/redo smoke tests for annotations (#1175)
* Add undo/redo tests for annotations

Fixes https://github.com/chanzuckerberg/cellxgene/issues/969

... also refactor the tests for DRY.

* Add done()

* Make e2e annotations tests safer to concurrency

* Add data-testclass for save state.

* Simplify tests and make them dependent on save state
2020-02-26 12:10:03 -08:00
Matt Weiden ded35c7252 Spelling fix in Compatibility Test workflow (#1178) 2020-02-26 11:12:16 -08:00
bmccandless 46876e2fb1 Fix various bugs with CXG format at annotations (#1173)
- Enable testing for this path

 - Fixes #1166
 - Fixes #1167
 - Fixes #1168
2020-02-24 18:19:39 -08:00
Bruce Martin 7bc58bba2b undo/redo cleanup (#1165)
* fix refactoring error which disabled annotation file clearing

* fix undo behavior on add category label

* fix various undo/redo bugs

* remove logging

* further refinement of annotation undo/redo and actions

* address PR comment
2020-02-24 18:51:26 -07:00
Matt Weiden 5c70cc5bcd Add codecov for code coverage of python and javascript (#1170)
* Add codecov to Push Test workflow

* Empty commit

* Clear reports and tag each with flags

* Tag code reports by test

* Fix codecov tags

* One more fix
2020-02-24 17:13:16 -08:00
Matt Weiden 281154f591 Add exclusions to flake8 config (#1169)
This excludes directories which should not be linted. This
1. makes the lint test more representative of what is run in CI
2. is faster
2020-02-24 08:33:25 -08:00
Matt Weiden c7f2032dd7 Add user-generated annotations tests to the server (#1164)
* Add user-generated annotations tests to the server

Partially completes https://github.com/chanzuckerberg/cellxgene/issues/969

* Auto-format python code

* @skip_if: passing lambdas > than property strings

* Respond to feedback from @bkmartinjr
2020-02-23 15:32:13 -08:00
Bruce Martin fb1f0c6469 fix latent crossfilter bug (#1162) 2020-02-21 14:14:41 -07:00
Bruce Martin d2bae0c4f6 CXG creator and dataset parameters (#1163)
* remove redundant call to make_response (lint)

* initial commit of H5AD to CXG converter

* lint

* PR review suggested comments

* cxg adaptor now understands group metadata

* lint
2020-02-21 12:49:45 -07:00
bmccandless 907cc634f5 server refactor (#1140)
This PR contains a refactoring to make adding new features easier.

The new features include supporting the tiledb format, and the multi dataset application.

The refactoring includes

Simplifying the directory structure and files.
a class structure to handle annotations (currently one type: AnnotationsLocalFile).
a class to handle application configuration
a class structure to handle matrix data (currently AnndataAdaptor and CxgAdaptor). CxgAdaptor uses tiledb.
Algorithms that were previously dependent on the scanpy anndata object are now generalized to work with an abstract interface.
The multi dataset option is not fully supported yet, and so the option to use it is hidden.
Use "cli launch --dataroot ..."
To access this feature.

All combinations of app single dataset/ app multi dataset and AnndataAdaptor/CxgAdaptor work with all the features, such as annotations, ontologies, diffexp.
2020-02-19 10:22:35 -08:00
Bruce Martin 349c413d8b anndata X indexing & version compatibility improvements (#1157)
* revert MatrixProxy; replace with correct use of adata slicing

* work around 0.6 adata slicing bug

* fix incorrect var slice

* simplify slicing of X

* add warning about performance impact of anndata<=0.7

* lint and remove unused code

* improve comment

* lint

* correctly parse versions

* temp files should preserve file suffix if possible - anndata 0.7 compat

* update anndata dependency to 0.6.20

* resolve PR review comments
2020-02-19 09:57:51 -07:00
Matt Weiden c630be33df Simplify CI checks on git push, add nightly checks on cron (#1158)
* Switch push tests to Github Actions

* Add nightly tests

* Add anndata check

* Add test status badges

* Add anndata 0.6.22.post1 workaround

* Rename Nightly tests to Compatibility Tests

* Fix to anndata fix

* Turn on cron schedule

* Fix badge mardown in README.md

* Fix cron schedule
2020-02-18 11:57:19 -08:00
Matt Weiden d30087ca63 Add frontend smoke tests for annotations (#1154)
* Add smoke test for annotations features

* Do not save during annotations tests

* Fix botched rebase in dev guidelines

* Revert "Do not save during annotations tests"

This reverts commit f0bd970bb2.

* Respond to feedback from @bkmartinjr
2020-02-14 17:23:45 -08:00
Matt Weiden ac13b31e13 Collect all env vars in one, easy-to-find place (#1149)
* Collect all env vars in one, easy-to-find place

Past state:
* Default environement variables were stored in both client/package.json
and client/__tests__/e2e/config.js
* Constants that should have been linked--like the cellxgene server port
during testing--were repeated.

With this commit:
* All environment variables are parameterized
* All environment variables are packaged in default env files

* Move npm scripts to client Makefile

* Respond to feedback from @seve and @bkmartinjr
2020-02-12 12:50:48 -08:00
Severiano Badajoz 5340a5f951 graph overlay improvements (#1141)
* check to see if display state has changed

* add display state

* create onDisplayChange

* check to see if displaying anything and add opacity drop

* pass callback down to children

* add middle truncation to labels

* remove unused import

* add a bit of documentation

* make prop addition more clear

* rename onDisplayChange -> overlayToggled for readability
2020-02-12 10:56:41 -08:00
Bruce Martin e770db1e2c load annotations incrementally (#1107)
* load annotations individually

* fix type check to be more general

* update node CI version from 10 to 12

* node 11

* debug print node version

* travis node version to latest

* try nvm

* remove extraneous node_js statement

* remove node version debugging printf

* incrementally load all annotations and layout

* process annotations and layout as they are loaded

* fix tests

* sort categories incrementally

* incrementally build category view summary; add category loading spinner

* add spinner to continuous metadata

* configure undoable reducer

* incremental crossfilter creation

* improve busy layout

* more layout cleanup

* correctly reconcile categories in schema

* refine layout of lsb spinners

* more spinner layout work

* more spinner layout

* always load layout before obs annotations
2020-02-10 11:22:27 -08:00
Bruce Martin 1c9b9f6a08 js package dep update (#1145)
* js package dep update

* roll back start-server-and-test
2020-02-05 13:11:05 -08:00
Bruce Martin 3afc03b95b fix state prop bug with camera transform (#1138) 2020-01-31 12:13:35 -08:00
Sidney Bell a927d733c2 [WIP] Patch for anndata 0.7 (#1132)
* Fix JSON encoding bug introduced by anndata 0.7

* Add anndata csr and csc classes to allowed matrix proxy inputs
2020-01-28 16:12:43 -08:00
Matt Weiden 3dd50e5676 Version bump to 0.14.0 2020-01-27 16:18:19 -08:00
Matt Weiden 588ddc103a Remove extra version replacement (#1133) 2020-01-27 16:07:55 -08:00
Colin Megill 9bdcf44564 Separate ontologies dialogue (#1126)
* componetization

* Ontologies as own menu option

* remove log

* remove dead suggest module
2020-01-27 15:10:57 -05:00
Matt Weiden 8b8a800453 Undo feature: fix case where previous state has no state filter (#1124)
* Undo feature: fix case where previous state has no state filter

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1099

When the previous state that the undo feature is trying to roll back to
has no filter state, merging javascript dictionaries result in keeping
the current state filter, preventing the actionFilter from saving the
new state.

* Fix whitespace
2020-01-27 08:29:41 -08:00
d48647a655 Ontologies (#1110)
* add sample ontologies file

* add ontologies reducer

* Move select category to own component

* Dialog and Input factored out

* refactoring categorical, partway

* validationn

* anno

* suggest  populates input

* frontend for ontology working

* initial implementation of back-end support for ontologies

* edit is now dialog again

* autosuggest working on edit

* part way through create arbitrary label

* handle choice in function

* pass duplicate cat  prop

* editing works

* update test to match new CLI params

* fix occupancy alignment

* edit category as dialogue

* secondary button

* remove stubbed out ontologies

* add label setting upon new label creation

* Update legal characters for labels (#1119)

* Allow any term in the ontology (bypass legal name check)

* Add hyphens and parens to legal characters in names

* improve performance for large ontologies

* correctly handle case where ontologies are disabled

* fix logic error in CLI

Co-authored-by: Bruce Martin <bruce@chanzuckerberg.com>

* PR cleanup 1

* lint

* validate user generated labels

* finish hooking up connected suggest component

* protect against undefined callbacks

* Fix illegal characters error message

* break out npm run commands

* fix error detection on label edit

Co-authored-by: Bruce Martin <bruce@chanzuckerberg.com>
Co-authored-by: Sidney Bell <sidneymbell@users.noreply.github.com>
2020-01-23 17:04:17 -05:00
Severiano Badajoz 8d725b1ad9 centroid labels (#872)
* refactor reducer to no longer support hover state and hold many labels

* refactor to generate centroidCoordinates for all values of a category

* create hash for function and memoize export

* create button to display all labels for a category

* clear state

* create label for each thing

* calculate on each value

* change to in place modification of map

* switch to for loop with iterator instead of forEach

* use map from centroidLabel instead of creating copy

* adapt for map

* utilize tarrays

* begin documentation

* disable centroids if in zoom mode

* clean up

* persist uncalc coordinates

* document

* clean up and document

* cleanup and document

* fix

* fix undefined labels and document changes

* fix first element skip

* fix conditional recalc

* rename centroidLabel -> centroidLabels

* break out dilation on hover to new reducer

* numerous styling changes for readability

* change centroid icon

* remove colorAccessor from parameters

* recalc centroids on world change

* make label toggle undoable

* remove unused import

* highlight labels on hover

* remove special characters from svg id

* lighten backdrop

* only generate new centroids if they pre-exist

* fix issue with spaces in catagorical value name

* add label buttons to menubar

* change reducer to use colorAccessor and have single toggle

* fix check to see if svg should be rendered

* move svg overlays onto a single svg layer

* dilate on label hover

* remove logs

* allow centroid to update along side regl renders

* allow actions to pass through svg if in zoom mode

* remove artifact from circle

* remove comment

* remove disabling of centroid button

* fix conditional map to screen

* make styling label conditions stricter

* prettier

* refactor onto master

* refactor computePointFlags() to use pointDilation store

* notify when viewport changes

* move svg attributes out of lasso setup and prevent rerenders/writes

* begin playing with transform matrix

* first solution for camera interaction

* create transform using nested groups

* semi-working method using nested groups with transforms

* inversely scale text

* properly do final transform

* cleanup dead / test code

* reinstate original functionality

* breakout centroid labels labels into separate component

* default toggle on for testing

* separate lasso and centroid layers

* remove unnecessary attributes, working hover

* dilation on label hover

* fix dilation on scatterplot

* add dilation on label hover

* break overlay into separate component

* make overlay agnostic to children

* move label mouse actions to centroidlabels component, add overlay state

* remove lasso on switch to camera

* disallow user selection

* fix reducer

* fix subset with continuous color error

* reset labels on color by continuous

* revert centroids on by default

* refactor for nested restructuring

* remove update checking

* remove unused method

* readd deleted hover delay

* remove old centroid setup

* remove centroid from undoable

* cleanup dead code

* remove dead code

* rollback unnecessary changes

* begin adding annotation functionality

* add annotation functionality

* add reset and undo functionality

* change centroids on layout change

* don't create label for unassigned

* add comment pointing out POI for performance

* touch up matrix transform comment

* add comment explaining coordinate space and children's assumed space

* remove dead code

* switch to pure component

* connect centroidLabels to redux

* clean up camera check and null result

* tool tip change

* rename centroid toggle and the like

* fix the misalignment of buttons, also make blueprint use consistent

* fix comment spelling mistakes

* introduce variable for cleaner logic expressions and state assignment

* alter tooltip text to back color by interaction

* remove manual iterator manipulation for forEach()

* remove debounce

* nit fix

* tooltip wording fix

* lint
2020-01-22 13:40:22 -08:00
Bruce Martin 69c639b14d pin anndata to 0.6.22post1 (#1122) 2020-01-22 13:23:05 -08:00
Bruce Martin 431c8bdabc disable magic nan parsing in read_csv (#1115) 2020-01-17 09:26:15 -08:00
Sidney Bell 2832393f40 Make diffexp docs more specific (#1083)
* Update diffexp docs w/ more explicit assumptions, etc.

* Minor edits

* Formatting and cross-checking with `diffexp.py`
2020-01-16 15:44:10 -08:00
Bruce Martin 8b329630f8 fix for issue 1112 - all user annotation category names must be unique across all obs annotatoins (#1114) 2020-01-16 09:28:29 -08:00
Bruce Martin 30f3ce49e4 update JS dependencies (#1106) 2020-01-15 17:11:51 -08:00
Matt Weiden ddbfc458d7 Remove d3 dependency in crossfilter (#1108)
* Remove d3 dependency in crossfilter

Fixes https://github.com/chanzuckerberg/cellxgene/issues/648

* Revert "Remove d3 dependency in crossfilter"

This reverts commit 03b478f7dc.

* Remove d3 dependency in crossfilter

Fixes https://github.com/chanzuckerberg/cellxgene/issues/648

* Fix typo in docstring
2020-01-14 13:46:53 -08:00
Sidney Bell 124254d5ed typo 2020-01-13 10:55:53 -08:00
Bruce Martin 677433bbf5 Fix label sorting bugs (#1102)
* move category label sort to utils

* refactor cat label sort

* fix category label sort and color assignment

* convert whitespace from tabs to spaces
2020-01-09 13:34:25 -08:00
Matt Weiden e1ff800980 Rename Makefile target build-server to build-cli (#1103)
The `build-server` makefile target does not actually build the server
module, rather it builds the CLI. Rename this target to make it less
confusing.
2020-01-09 07:52:20 -08:00
Bruce Martin 2d39cb3b30 sort the embedding names for the UI (#1098) 2020-01-09 06:33:14 -08:00
Bruce Martin 18177b108f fix 1090 - add unassigned label to the new category, not the source category (#1094) 2020-01-09 06:32:38 -08:00
Matt Weiden be54d583f1 Only copy versioned files to the package (#1096)
Fixes https://github.com/chanzuckerberg/cellxgene/issues/582
2020-01-08 08:12:51 -08:00
Matt Weiden 4c1ecf9e52 Specify histogram thresholds in fixed-length array (#1086)
* Specify histogram thresholds in fixed-length array

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1082

For background, see the following:
* https://github.com/d3/d3-array/issues/46
* https://stackoverflow.com/questions/15880058/d3-js-ticks-function-giving-more-elements-than-needed

* Oops, off by one!
2020-01-06 11:13:02 -08:00
Matt Weiden b172ff7f15 Add margins to histogram SVGs (#1085)
* Add a left margin to histogram SVGs

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1042

1. Added "margins" to the SVG as in https://bl.ocks.org/mbostock/3019563
2. Tailored the brushable area to be just where the data is and locked
   puppeteerUtils to use that in e2e testing.

* Dial in margins to avoid y-axis clipping
2020-01-06 10:46:06 -08:00
Matt Weiden 327d005a1f Add requests 2.22.0 as a server dependency (#1089) 2020-01-03 10:59:46 -08:00
Matt Weiden 0d2cb14796 Fix smoke test instructions (#1087) 2020-01-02 16:32:00 -08:00
Sidney Bell 97738e5c2d Remove FAQ link from info menu (#1084) 2020-01-02 16:04:38 -08:00
Bruce Martin dfaf0d0603 allow hyphen in annotation file name (#1074)
* remove old detritus

* allow hyphen in label file name
2020-01-02 15:44:40 -08:00
Matt Weiden 0b1bdca022 Use npm ci instead of npm install during build (#1080)
Currently the client build is not reproducible since, each time you run
`make build-client` the package lockfile is updated. This should be
handled separately by `make gen-package-lock` when developers actually
want to update the dependencies.

`npm ci` installs dependencies directly from the lockfile without
updating them, making builds reproducible.
2020-01-02 10:21:14 -08:00
Bruce Martin 6370025caf update all JS package deps (#1076)
* update dependencies - minor revisions

* update min node_js version to 10

* major npm package dep updates

* a few more updates
2020-01-02 09:14:09 -08:00
Matt Weiden 53a6d01fa3 Notify users of new versions of cellxgene (#1078)
* Notify users of new versions of cellxgene

Fixes https://github.com/chanzuckerberg/cellxgene/issues/683

* Do not use PyGithub client

* Protect against AttributeError

* Document that all version tags must follow SemVer

* Release tags `should -> MUST` follow semantic versioning
2019-12-31 17:43:11 -08:00
Matt Weiden 29947e25c3 Serve Roboto Condensed font from the app directly (#1077)
* Serve Roboto Condensed font from the app directly

Fixes https://github.com/chanzuckerberg/cellxgene/issues/344

* Remove second google fonts reference
2019-12-31 17:07:16 -08:00
Matt Weiden f3015cb9df Makefile modularity, test targets, and auto-formatting (#1070)
* Fix Makefile whitespace and .PHONY use

* Fix Makefile filename

* Modularize Makefile into client and server Makefiles

Part of the reason that the Makefile in the root directory is a bit
complicated is that it tries to handle tasks that can be handled
separately in the client and server modules.

This commit pushes some of the make logic specific to each module into
their own makefiles and calls out to those makefiles from that in the
project root.

* Add auto-formatting to client and server modules

One thing that can make linting faster is auto-formatting. This commit
adds the yapf auto-formatting tool to the server module and uses
eslint's "fix" functionality to speed up the linting/formatting process.

* Add yapf for automatic code formatting

* Add a root test target that calls sub-tests

* Apply yapf to python files

* Do not duplicate npm commands, simply pass through

* Update documentation

* Do not shadow reserved word len

* Add general test target

* Fix make call in dev-env

* Use black instead of yapf

* Run flake8 from the root directory

* Revert "Apply yapf to python files"

This reverts commit cdca128a01.

* Apply black to python code

* Resolve lint errors resulting from black format

* Add explanation of server unit tests in dev guidelines
2019-12-27 14:43:37 -08:00
Sidney Bell ec79995be8 Reorganize annotations docs to emphasize hosted vs single-user modes (#1067) 2019-12-03 17:02:35 -08:00
Sidney Bell 517e0f3826 [docs] Fix annotations command 2019-12-02 09:26:07 -08:00
Sidney Bell 5812e00e01 Merge remote-tracking branch 'origin/master' 2019-11-27 08:49:56 -08:00
Sidney Bell 1ed68261d4 Add sceasy package to docs 2019-11-27 08:49:21 -08:00
Sidney Bell 9c09ee6329 Update readme and other repo MD files (#1063)
* Update readme

* Move roadmap to main docs site

* Avoid duplication by just putting a pointer to the actual docs
2019-11-26 16:09:59 -08:00
Bruce Martin c9b6044c4f release 0.13.0 (#1064) 2019-11-26 15:45:13 -08:00
Bruce Martin fa89a5a375 create annotations output dir if it does not exist (#1062)
* create annotations output dir if it doees not exist

* lint
2019-11-26 13:35:20 -08:00
Bruce Martin f8cef71eed help text changes (#1061)
* requested CLI help changes for launch sub-command

* update file name prose

* more wording changes

* more PR requests
2019-11-26 13:35:01 -08:00
Sidney Bell bb3032ea2f Update getting started docs w/ direct url launch 2019-11-26 12:57:50 -08:00
Sidney Bell 5e471349e7 Cleanup docs files 2019-11-26 08:44:23 -08:00
Sidney Bell 0aee4b89e0 Fix docs font 2019-11-26 08:43:57 -08:00
Bruce Martin d2171b9a2f fix perf regression introduced by user anno changes (#1060) 2019-11-25 18:16:49 -08:00
Bruce Martin a593e95ab3 annotations CLI and file UX rework (#1049)
* rename config param label-file

* annotations rework - CLI params, file naming and backups

* lint

* improve cli option error checks

* enable session cookies

* enable session cookies

* add session id

* name annotations file in multi-dataset and multi-user safe manner

* pass data user hash to front-end

* add annotation collection name support to front-end

* add constant for annotation data collection name

* parameterize annotation collection name; make it sticky in the session

* clarify comments

* hard wire a temporary data collection name for testing

* prettier

* test comment

* package command

* set annotations  filename dialog

* name  and hash are visible

* wire up data collection capture
2019-11-25 15:28:28 -08:00
Sidney Bell 575f71aaee Fix base url 2019-11-22 18:17:43 -08:00
Sidney Bell 04aa6f8fe9 Merge branch 'sidneymbell/docs-overhaul' 2019-11-22 15:00:24 -08:00
Sidney Bell 298ded6e2d Add site files 2019-11-22 14:59:40 -08:00
Sidney Bell 9bcafeb7c1 [WIP] Overhaul the documentation site (#1030)
Overhaul the whole documentation site
2019-11-22 14:54:20 -08:00
Sidney Bell 85c1b6957e Final annotations notes 2019-11-22 14:53:35 -08:00
Sidney Bell 2f99b317a2 Update annotations docs 2019-11-22 13:57:50 -08:00
Sidney Bell 8535664e9c Typo 2019-11-22 11:26:55 -08:00
Sidney Bell 5ed9f5126b Switch demo datasets to launch directly from URL 2019-11-22 11:26:51 -08:00
Bruce Martin 5713aa8792 category and label name validation updates (#1056)
* update label validation with new schema

* add new error messages for new label errors; general cleanup
2019-11-22 10:32:00 -08:00
Severiano Badajoz 9aaaad6709 tweak allotted width for value name + occupancy and tweak truncating (#1051) 2019-11-21 20:14:11 -08:00
Sidney Bell 621b88bff4 Simplify demo data download 2019-11-21 18:51:30 -08:00
Sidney Bell ecedf666c7 Bold current page in nav 2019-11-21 18:51:13 -08:00
Sidney Bell afff2ceec7 Update logo 2019-11-21 18:50:55 -08:00
Sidney Bell be9a415a4a Typo 2019-11-21 17:02:59 -08:00
Sidney Bell 0295041972 Fix typos, etc in prepare 2019-11-21 17:02:50 -08:00
Sidney Bell 8fe7ee75e8 Minor text edits to launch 2019-11-21 17:02:30 -08:00
Sidney Bell a3d96aae0a Minor text edits to hosted (i.e., typos) 2019-11-21 17:02:01 -08:00
Sidney Bell d5d56d89d1 Change text color 2019-11-21 17:01:33 -08:00
Sidney Bell a6a2841961 Update gifs in gallery (thank you @liaprins-czi !) 2019-11-21 17:01:24 -08:00
Sidney Bell 7bf76fa557 Merge remote-tracking branch 'origin/master' into sidneymbell/docs-overhaul 2019-11-21 16:49:18 -08:00
Colin Megill c2ededfc62 Anno editing label/category error states (#1048)
* anno error states

* label validation should not treat empty string as error

* disable Submit if no label or category entered

* edit category name error states

* flex start

* pr cleanup

* label name validation fixes

* PR comment responses
2019-11-21 19:08:29 -05:00
Bruce Martin ee62dd355f [WIP] JS lint and dead code removal (#1053)
* lint and dead code removal

* fix regressions
2019-11-21 14:10:02 -08:00
bkmartinjr 0415c30f0b add security notice to readme 2019-11-20 10:42:27 -08:00
bkmartinjr 79e534e397 additional caveats/warnings for hosting 2019-11-20 10:40:00 -08:00
bkmartinjr f4919340e9 merge 2019-11-20 10:33:17 -08:00
bkmartinjr 976f9d9ff8 add hosting notes 2019-11-20 10:28:40 -08:00
Bruce Martin 9a540a4f0a fix erroneous state clearing (#1052) 2019-11-19 11:11:19 -08:00
Severiano Badajoz 5968da18b4 fix inconsistent sidebar value height(#1039) 2019-11-18 12:43:16 -08:00
Severiano Badajoz bd5f816d58 fix dataset title clipping (#1040) 2019-11-18 12:42:55 -08:00
Severiano Badajoz cf8f1a710d add padding to scatterplot shift (#1043) 2019-11-18 12:41:58 -08:00
Bruce Martin 2d0b646956 js dependency update (#1045) 2019-11-18 11:22:13 -08:00
Bruce Martin b284e6f820 Improve CLI help (#1025)
* launch option changes

* more CLI help improvements

* change plot help

* additional changes requested

* change metavars for options and subcommand
2019-11-14 13:02:40 -08:00
Bruce Martin 5ea9ac8a9e Add provenance header to labels CSV file (#1041)
* add last mod time lookup to data locator

* store data locator in Driver

* save metadata header in labels csv

* lint

* fix tests

* change datetime format to second precision
2019-11-14 12:33:48 -08:00
Colin Megill 89d748b007 state (#1032) 2019-11-13 15:13:15 -05:00
Severiano Badajoz 68ad4b1228 bump @blueprintjs dependencies (#1038) 2019-11-13 11:56:48 -08:00
Bruce Martin a52e86a69b improve category and label name validation (#1034) 2019-11-13 09:40:37 -08:00
Severiano Badajoz a5d343aaa4 add list of wild hosted cellxgene instances 2019-11-12 15:39:04 -08:00
Bruce Martin 04c83993d2 clear color-by mode when current category is deleted (#1033) 2019-11-12 15:31:06 -08:00
bkmartinjr 54d144bb75 add build dependencies for igraph and louvain 2019-11-12 07:56:03 -08:00
bkmartinjr bf3bf58d6e improve launch docs 2019-11-09 13:03:47 -08:00
bkmartinjr 611c8b98b1 add --backed info to troubleshooting 2019-11-09 12:59:33 -08:00
bkmartinjr d08506ae12 improve install docs 2019-11-09 12:49:22 -08:00
Bruce Martin 238b7420f7 update deprecated Pandas API (#1021)
* update use of deprecated API

* update pandas depencency
2019-11-08 09:49:52 -08:00
Colin Megill cadf710bf8 remove duplicate handler (#1027) 2019-11-08 11:18:29 -05:00
Sidney Bell 9c8c28e1cb Overhaul the whole documentation site 2019-11-07 17:03:33 -08:00
Colin Megill 84900bad0e Warnings on anno textfields (#1023)
*  Warning for creating new label with an existing name

* category dup

* italics
2019-11-07 18:57:15 -05:00
Colin Megill 4c3d783fc8 Continuous data in right sidebar (#985)
* Create right sidebar files, move histos over

* restructure continuous legend + all padding

* right sidebar file

* proper graph positioning and resize

* new lasso values since right sidebar

* fix autosave position
2019-11-05 20:29:21 -05:00
Bruce Martin 461084eca1 Pin the 'unassigned' annotation value/label to bottom of list (#1018)
* pin unassigned label to bottom of category list

* pin unassigned label to bottom of category list
2019-11-05 09:22:39 -08:00
Bruce Martin 341d3015f0 fix typo (#1016) 2019-11-05 09:21:48 -08:00
Prete 6a95d956a9 Update Dockerfile (#1019)
Added python-requests package to the Dockerfile so cellxgene can use URLs as a data source.
2019-11-05 08:57:29 -08:00
Bruce Martin 7599af451f add context sensitivity to annotation "label" menu item (#1015)
* cache common results to reduce jank

* add util function in support of annotation labelling

* enable/disable annotation edit menus per issue 972; improve formatting

* PR review requests
2019-11-04 14:22:38 -08:00
Bruce Martin 823d819eee allow user to save an empty label dataframe (#1012) 2019-11-04 14:14:49 -08:00
Bruce Martin a16c2e2747 crossfilter immutability error (#1009)
* add rename immutability test

* make rename immutable
2019-11-04 14:14:17 -08:00
Colin Megill e50fd8b881 Animate list items on name change (#1008)
* animate list items

* fix comment
2019-11-04 14:09:16 -05:00
Charlotte Weaver 3e61bb807d e2e testing documentation (#1000) 2019-11-03 10:56:28 -08:00
dependabot[bot] 345e2c344b Bump lodash.template from 4.4.0 to 4.5.0 in /client (#1002)
Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-03 10:56:04 -08:00
Colin Megill 3f0567c59f disable editing if already editing (#999) 2019-10-31 13:46:59 -04:00
Colin Megill 934f61f2a1 categoryAddingNewLabel (#995) 2019-10-24 22:02:38 -04:00
Bruce Martin 60a23b681c QOL documentation: csweaver contributions from PR #912 (#977)
* csweaver changes from PR #912

* fix formatting error
2019-10-24 08:44:36 -07:00
Bruce Martin e581dec8f5 release 0.12.0 (#981) 2019-10-08 12:34:21 -07:00
Bruce Martin 711f3b7048 Add support for anndata backed mode (#943)
* initial cut at backed mode

* make flask multithreading conditional on debug flag

* update X access to support backed mode

* lint

* improve help message for backed mode

* fix tests

* add MatrixProxy to normalize supported matrix types

* add FAQ entry for --backed

* remove use of matrix.T

* clean up

* add ability to disable diffexp from CLI; add hueristic to detect likely slow diffexp calculation, and warn user

* fix tests

* do not print diffexp speed warning if diffexp is disabled

* tweak wording of diffexp speed messages

* add FAQ entry on --disable-diffexp

* revise heuristic for warning about slow diffexp

* use quick tooltip delay on diffexp button
2019-10-08 11:16:07 -07:00
Colin Megill 1467357db5 re-label (#973) 2019-10-07 16:02:09 -04:00
Bruce Martin de6721b682 fix broken link (#978) 2019-10-07 11:08:18 -07:00
Colin Megill 296352b620 remove modal for inline edit category name, and autofocus (#962)
* remove modal for inline edit category name, and autofocus

* remove comment, rename to categorybeingedited
2019-10-04 13:41:05 -04:00
Bruce Martin 3f2811d9da rendering performance improvements (#968)
* freeze objects

* component rendering perf work

* use PureComponent where safe

* remove obsolete WorldUtil code

* make brushable histogram a pure component
2019-10-04 07:59:22 -07:00
Bruce Martin 700c871e6d reduce dot scaling upon zoom (#963)
* remove obsolete code

* change point scaling with zoom distance
2019-10-04 07:58:38 -07:00
Severiano Badajoz f865187a5d fix long values in sidebar (#911)
* tweak overflow and add truncation

* remove log

* alter char limit to evenly space around occupancy

* use global hover delay

* create constants for truncation lengths and fix truncation on self color

* create globals for short and long sidebar value length

* fix alignment

* create quick delay global
2019-10-02 11:01:22 -07:00
Bruce Martin ad290b3630 scale scatterplot point rendering size with overall app size, not scatterplot canvas size (#956) 2019-10-01 12:25:21 -07:00
Bruce Martin 21218f1c59 update contact and core team metadata (#947)
* update contact and core team metadata

* fix spelling error
2019-10-01 10:26:38 -07:00
Sidney BellandSeveriano Badajoz f41f47cf1b Fix mistakenly removed badges (oops) (#955)
* Fix mistakenly removed badges (oops)

* Update README.md

Co-Authored-By: Severiano Badajoz <sbadajoz@chanzuckerberg.com>
2019-09-30 16:00:29 -07:00
Severiano Badajoz d94508d534 fix bulk add failing on first gene in world (#954) 2019-09-30 15:21:21 -07:00
Severiano Badajoz 44cb276cdf Add configurable about this dataset links (#907)
* add about arg

* add simple url validator

* attach about link to config api

* add links to configDefaults

* add conditional link in top left and menu item

* whitespace

* change to lower case

* move --about arg before click.command()

if this fixes it I have no idea why

* change link>URL

* be more descriptive about URL

* Make error more explicit

* refactor attach_data to accept about

* format

* change icon

* add trailing parenthesis

* whitespace
2019-09-25 19:23:49 -07:00
Colin Megill e64f4f06fb enter submits modals (#945) 2019-09-25 15:18:33 -07:00
Severiano Badajoz 32feb36172 better handle long dataset titles (#909)
* truncate middle of long title names

* remove width

* In the case of VERY long titles, hide overflow

* align bottom of title with cellxgene

* add character length global and shrink length
2019-09-23 16:31:54 -07:00
Severiano Badajoz c983e878e9 fix incorrect gene add error on empty query (#910)
* fix add gene button

* make bulk add case insensitive

* increase performance, incl memoizing

* change which lists are memoized

* improve hash to use ID from dataframe column

* Add requested fixes, and actually make bulk add work

* add default result to empty gene query

* replace with unicode ellipses

* change empty message
2019-09-23 16:31:07 -07:00
Severiano Badajoz 077541c762 fix bugs surrounding gene adding (#919)
* fix add gene button

* make bulk add case insensitive

* increase performance, incl memoizing

* change which lists are memoized

* improve hash to use ID from dataframe column

* Add requested fixes, and actually make bulk add work
2019-09-23 16:30:53 -07:00
Bruce Martin 3d15db7c5e parameterize pbmc3k scanpy engine test (#939) 2019-09-20 17:41:32 -07:00
Bruce Martin 109c9e70ec Add obs index to label file (#928)
* remove debug logging

* load and save obs/row index in label file

* lint

* update tests
2019-09-19 09:16:01 -07:00
Colin Megill 197ea9214a fix italic (#935) 2019-09-18 19:36:49 -04:00
Colin Megill 81e57afe29 Anno fixes (easy) (#925)
* change wording from value to label

* tooltip position

* Padding for text & code whitespace
2019-09-18 19:22:51 -04:00
Bruce Martin 3660a6cc27 Experimental - manual annotations (#837)
* icons, partway

* redux for values

* onChange

* cancel

* annotations lifecycle for category names

* copy categorical

* edit category

* add Dataframe.withColsFrom

* render user annotations; default add/delete annotation category

* add label name to actions

* category name edit

* error checking improvements

* change schema field isUserAnnotation to writable

* always have an unassigned label; implement delete label

* implement add new label and edit label name

* label current cell selection

* fix select exact bug in crossfilter

* clean up categorical reducer

* fix tests

* remove debugging printf

* implement subset/reset for user annotations

* undo redo support for user annotations

* remove duplicate button from categories

* add modal

* remove obsolete duplicate annotation reducers

* remove old debugging printf

* connect modal to annotation create and dup

* initial full-stack wiring

* finish up end-to-end wiring

* fix existing unit tests

* fix pytests to match new schema API

* remove debugging printfs

* add label file rotation

* remove obsolete comment

* add fbs encode/decode tests

* add tests for writable annotations

* simplify code

* fix hashing bug with FBS encoding

* lint

* fix smoke tests

* improve error checking in Dataframe.withColsFrom

* add unit test for Dataframe.withColsFrom

* add unit test for Dataframe.columns and Dataframe.renameCol

* fix bug in FBS encode, add better error checks, refactor

* add FBS encode/decode test

* add clarifying comment

* clean up action type names; fix state inconsistency in crossfilter update

* change autosave timer to 2.5sec

* sort categorical metadata render order so it remains consistent

* add temporary autogenerated label for add-new-label operation

* fix hover-over label menu interference with cell highlighting

* remove debugging code

* add missing reducer cases & fix typo

* make dataframe memoize more general purpose

* add dev mode for annos

* fix error on select duplicate

* handle zero occupancy categories

* correctly maintain unclipped AND clipped world

* correctly handle zero length FBS matrix and label files

* ensure all writable categorical schema contains an unassigned category

* handle case where building occupancy stack for category with no members

* dialog for creating label, disable button if duplicate or empty

* visually separate writeable

* edit category

* fix edit category name

* remove debugging code

* fix edit annotation label

* visually define unassigned, change options

* Pull in requirements.txt from `master`

* label currently selected cells

* duplicate label

* lint

* fix pytest merge issues

* rename --label-file to --experimental-label-file

* remove debugging console log

* spelling error fix; fix bug found in PR review.

* lint
2019-09-18 07:33:41 -04:00
Bruce Martin ab2c423006 JS package updates (#922) 2019-09-17 09:36:47 -07:00
Bruce Martin 922c375333 Rename "layout" to "embedding" in the UI (#921)
* rename "layout" to "embedding" in the UI

* docs changes for layout

* fix typo in data docs

* new help text, per PR review
2019-09-16 15:34:14 -07:00
Colin Megill ec4da63408 Format histogram axes: 200,000 --> 200k (#913)
* remove dead file

* tickformat
2019-09-16 17:07:51 -04:00
Bruce Martin 20317fd08f Add URL data locators to launch sub-command (#920)
* initial commit of URL support for launch

* lint

* modify tests to use new data locator

* add locator unit tests

* fix typo in faq

* more lint

* update faq per PR review
2019-09-15 09:01:53 -07:00
Charlotte Weaver 0f520f2fd4 [Merge on next release] Document how to install cellxgene prepare (#889)
* Document how to install cellxgene prepare

after pr #887 merged

* formatting

* remove reference to cellxgene[louvain]
2019-09-13 18:15:06 -07:00
Bruce Martin 84c815cc04 pin h5py to 2.9.0 to temporarily work around regression (#916) 2019-09-11 13:20:15 -07:00
Charlotte Weaver 77de22d1ed merge louvain and prepare extra requires (#897) 2019-08-29 12:49:19 -07:00
Bruce Martin c35f94fe6d add constraints on pan and zoom-over point (#904) 2019-08-28 17:56:46 -07:00
Bruce Martin 78a43402cb package dep update (#905) 2019-08-28 09:11:10 -07:00
Bruce Martin b0562a11fd scale point size for graph and scatter plots (#903)
* scale point size for graph and scatter plots

* improve comments
2019-08-27 14:12:53 -07:00
Bruce Martin b309367bb0 Add hover-over highlighting to the scatterplot (#900)
* small perf improvement - do not recreate functions each time component is updated

* add hover-over highlighting to scatterplot
2019-08-27 14:01:30 -07:00
Bruce Martin b359a610da tooltips improvements (#896)
* add and fix tooltips on toolbar

* move tooltips below buttons

* add global definition for tooltip hover over delay
2019-08-22 20:56:33 -07:00
Bruce Martin 0f36b49840 Graph/scatterplot improvements (#893)
* many graph/scatterplot fixes: 722, 882, 758, 886

* fix e2e test breakage caused by graph transform work

* convert webgl point shaders to use flags rather than explicit point size

* remove unused packages from dependencies

* fix typo in regl prop name

* factor common code into util helper file

* legibility tweaks
2019-08-22 20:56:05 -07:00
Charlotte Weaver 28c4d28308 extract prepare from main install (#887)
* extract prepare from main install

* add requirements-prepare to manifest
2019-08-21 14:26:01 -07:00
Charlotte Weaver b8c05763fe sparse tests (#894) 2019-08-21 14:09:36 -07:00
Charlotte Weaver 613f1a911f pin matplotlib dependency (#885)
reordering packages does not work from dist install
2019-08-14 14:21:07 -07:00
bbb521537b create single-click Heroku hosting and documenation (#834)
* add deploy button

* Fix info

* Create heroku.yml

* Remove unnecessary files

* add buildpack

* Add container stack

* Remove comment

* Remove comment

* fix stack name?

* Move run to heroku.yml with port var

* Add dataset env var

* Add dataset env var

* Revert "Remove unnecessary files"

This reverts commit b2cb34dccb.

* move Dockerfile

* create readme

* add back preexisting Dockerfile

* remove button

* Add quick start and caveats

* add what and why section

* remove README

* add deploy button

* Fix info

* Create heroku.yml

* add buildpack

* Add container stack

* Remove comment

* Remove comment

* fix stack name?

* Move run to heroku.yml with port var

* Add dataset env var

* Add dataset env var

* move Dockerfile

* create readme

* add back preexisting Dockerfile

* remove button

* Add quick start and caveats

* add what and why section

* remove README

* wording fix

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* fix link

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* replace github links with S3 buckets

* small fixes

* grammar and spelling changes

* add hibernation caveat

* more spelling

* revise what is heroku

* rewording

* rename header

* revise caveats

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* various changes

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>

* Update experiments/heroku/README.md

Co-Authored-By: Sidney Bell <sidneymbell@users.noreply.github.com>

* Update experiments/heroku/README.md

Co-Authored-By: Sidney Bell <sidneymbell@users.noreply.github.com>

* revisions/additions
2019-08-14 13:16:49 -07:00
Charlotte Weaver 79a503e436 fix bad matplotlib library by installing scanpy first (#881) 2019-08-12 16:42:26 -07:00
Sidney Bell 69e69f4dae Add logo and favicon to docs (#880) 2019-08-12 16:03:41 -07:00
Sidney Bell fcaa30d7cc readme updates (#878)
* Add Lia to core team in readme

* Update readme

* Add logo to readme
2019-08-12 12:56:26 -07:00
Colin Megill 03baa1ac0b overflow title (#858) 2019-08-05 14:08:41 -04:00
Bruce Martin 906d65c06f fix binning error in continuous data histogram (#869) 2019-08-05 10:40:56 -07:00
Justin Kiggins 57b1c3cbb4 put badges on one line in README (#862) 2019-07-31 16:01:09 -07:00
Marcio von Muhlen 40c533a45b Update README.md (#856)
Full URLs for roadmap, contributor guide to fix 404s on Pypy.
2019-07-26 13:35:46 -07:00
Severiano BadajozandJustin Kiggins 9eb8185e82 add badges to readme (#857)
* prettier

* add pypi and commit badges

* formatting

* revert DOI badge

* Update README.md

Co-Authored-By: Justin Kiggins <justin.kiggins@chanzuckerberg.com>
2019-07-25 16:52:38 -07:00
Charlotte Weaver 6a86883c2d Remove stray quote (#854) 2019-07-19 12:40:54 -07:00
Colin Megill d16a69e158 bump version to 0.11.0 (#850)
* bump version to 0.11.0

* manually fix version number
2019-07-16 14:27:41 -04:00
Severiano Badajoz 3d6bb88556 improve performance around category highlighting (#849)
* remove function call and add comment

* separate crossfilter size calc into memoized function
2019-07-15 16:00:23 -07:00
Bruce Martin e199b14259 correctly handle float columns which only contain NaN (#848) 2019-07-15 14:49:33 -07:00
Severiano Badajoz 9c05aa6766 remove selection interaction from categorical value label (#847) 2019-07-15 11:32:42 -07:00
Severiano Badajoz 3833bada54 style mini-histogram popups (#845)
* popup styling changes

* change to target hover only and change delay

* change popover wording

* change popover wording
2019-07-13 16:52:00 -07:00
Charlotte Weaver 3d98797d8c Installer (#840) 2019-07-12 12:29:22 -07:00
Colin Megill 777214cc14 upgrade lodash (#844) 2019-07-11 18:35:57 -04:00
Severiano Badajoz 9f0f60b5eb add point dilatation on hover (#841)
* enable centroid

* introduce new sizing

* scale point size based off hovered category

* add styling

* fix margins

* disable centroid labels

* remove unused code and add detail to comment

* remove cell dilation on selection toggle

* move hover to name label

* hover on value except for checkbox

* add border radius to value
2019-07-11 15:33:44 -07:00
Severiano Badajoz ca20add577 reset colorAccessor and colorMode if colored diffexp gene is removed (#843)
* add action to clear colorMode and colorAccessor if diffexp is removed

* create new colorHelper function

* creater colorHelper for conditionally setting state

* add abbr

* revert abbr
2019-07-11 14:12:38 -07:00
Sidney Bell 98b07b1284 Add code of conduct; move contributing guidelines to CONTRIBUTING.md (#842) 2019-07-11 10:09:35 -07:00
Severiano Badajoz 941c297363 categorical vs continuous mini histograms (#827)
* comment

* add histogram functionality to Dataframe; port category occupancy to use it

* fix binning and create histogram for continous by catagorical

* Remove unnecessary logs

* Begin work on KDE

* Replace broken KDE with working histogram

* Define domain and range based on data from histogram

* Fix occupancy

* Add continuous obs and switch to canvas

* Stop value from always rerendering

* clear before render

* Clear canvas on render

* refactor categorical occupancy to canvas

* Remove log

* simplify finding max

* refactor kde->histogram and occupancy->bins

* refactor svg -> canvas

* rename to occupancy stack

* create popup

* add metadata and categorical values to popup

* fix overflow

* remove zeros info

* style graph

* fix shouldComponentUpdate to look for world changes

* change categorySelected -> categoryValueSelected

* refactor out render

* remove comment

* conditionally have bottom border

* remove diff comp

* remove comments

* remove unnecessary mapping

* Add comments describing drawing functions

* comments

* flip comparison order

* remove logging

* move default to parameter

* move defaults to parameter

* disable popover if not showing histogram

* fix wording and styling

* add line break
2019-07-09 11:19:01 -07:00
Charlotte Weaver 722a91f1d2 remove options widget + launch on file selection (#839)
* remove options widget + launch on file selection

* extend drop area width
2019-07-09 10:46:13 -07:00
Severiano Badajoz acdc810f82 disable centroids (#833)
* disable hover actions

* Remove action firing

* Remove disabled on action type and leave function call commented
2019-07-08 15:15:50 -07:00
Charlotte Weaver 8afb22a017 GUI UI Elements (#816) 2019-06-27 17:30:57 -07:00
Charlotte Weaver 5effe4bbbb removed --diffexp cli param (#826) 2019-06-26 14:46:58 -07:00
Justin Kiggins df1109e920 updating roadmap (#825) 2019-06-26 09:47:37 -07:00
Sidney Bell 2df93d6d94 Add prepare example and update demo datasets (#810)
* Update example datasets w/ pbmc3k and tabula muris

* Add `prepare` overview and example

* Add S3 data links

* Incorporate PR feedback & copyedits

* Switch to letter pointers

* unix line endings

* path
2019-06-13 16:55:09 -07:00
Charlotte Weaver d5deb1579f [EASY] fix max-category-items (#813)
* fix max-category-items

* match default for max category items

fe had 1000, be had 100
2019-06-13 15:45:43 -07:00
Severiano Badajoz 334b8bb8da draw labels marking the centroids of category value clusters (#809)
* Connect mouse over events to reducer actions

* Change Styling on hover

* Rename reducer actions to be more descriptive

* Reorder reducer in cascade

* Create centroid calculation util

* Whitespace

* Typo fix, use correct action

* Create centroid calc util

* Create centroid svg setup

* Refactor existing svg layer to toolSVG

* Change calcCentroid signature and centroidXY to match mapPointToScreen

* Add id and styling

* Run prettier

* Set z-index to 999

* Draw the label

* Introduce the centroid SVG, refactor code to allow both SVG layers

* Add text label and compute radius based on population

* Implement optional chaining

* Update font family

* Optimize calcMeanCentroid()

* Create and utilize calcMedianCentroid()

* Remove mass circle from label

* Remove styling change on hover

* Remove reducer action logs

* Prettier

* Swap out binds for arrow functions

* Style text

* switch from selectAll() to select()

* Reflect centroid container's purpose in id

* Remove mass from the output

* Swap to obj

* Add finite check

* Don't draw centroid if no finite values

* Fix finite check

* Remove log

* Toggle label coloring based on colorBy state

* Pass cursor events through centroid svg
2019-06-13 15:40:57 -07:00
Colin Megill 6aeefb0fe6 menubar (#804)
* menubar 1

* zoom switching

* centering, pixel perfect canvas

* remove dead args and code

* clipping

* remove log

* if

* connect props

* lint

* undo

* logo left, componetize

* graph back to full height

* shadow to top

* do not prematurely call event handlers during render

* change test to deal with async histogram creation

* left section padding

* lint

* adjust graph to account for top bar,

* lasso tests

* refine histogram tests

* remove testing (onlys)
2019-06-13 15:15:43 -07:00
Charlotte Weaver eac514e04d update favicon (#814) 2019-06-13 11:29:27 -07:00
Charlotte Weaver afeddad343 windows fixes (#767) 2019-06-11 11:41:27 -07:00
Charlotte Weaver 0f17b84dc1 Add reload back in (#808) 2019-06-06 13:49:15 -07:00
Charlotte Weaver 3edb87d125 gui multiprocess - experimental feature (#780) 2019-06-06 10:52:05 -07:00
Justin KigginsandCharlotte Weaver 9f9393a486 adds section on risks of hosted instances (#807)
Co-Authored-By: Charlotte Weaver <charlottesweaver@gmail.com>
2019-06-04 09:16:11 -07:00
Charlotte Weaver 1d6bb032a3 remove async from describe blocks (#805) 2019-06-03 14:59:16 -07:00
Justin Kiggins 3152de4b7f fixes URL to getting started (#801) 2019-05-31 19:34:51 -07:00
Bruce Martin ca9a6796d8 release 0.10.1 (#797) 2019-05-30 13:44:42 -07:00
Bruce Martin a6142bdf93 improve graph scale and centering (#796)
* add gutter to embedding canvas

* improve layout scale and translate

* fix lint

* pin tables to version 3.5.1

* fix lasso coordinate smoke tests
2019-05-30 13:31:41 -07:00
Justin Kiggins ffd7f0db49 adds zenodo badge (#795) 2019-05-30 10:23:49 -07:00
Bruce Martin c6252825f3 release 0.10.0 (#794) 2019-05-29 16:55:43 -07:00
Bruce Martin 4b96b3a635 fix incompatibility of flask reload and port searching (#793)
* WIP

* add --developer; fix incompatibility of --port and --debug

* put REST tests on separate ports

* PR review
2019-05-29 16:38:57 -07:00
Colin Megill 862d8feb5e x (#792) 2019-05-29 12:23:30 -04:00
Bruce Martin 1ef77d1596 fix misconfiguration for history management (#787) 2019-05-24 21:01:11 -07:00
Bruce Martin 3dc45d6330 do not hard-wire column names in annotations (#785)
* enforce column name uniqueness for obs and var

* parameterize the column name containing obs and var user-readable names

* use the new annotation index value from schema

* update f/e unit tests

* PR review suggestions

* lint
2019-05-24 21:00:54 -07:00
Bruce Martin a8c2e408d1 update to latest anndata and remove restriction on scipy (#790) 2019-05-24 11:23:35 -07:00
Bruce Martin e941c1a496 scaling omitted from event handlers (#789)
* scaling omitted from event handlers

* fix smoke tests
2019-05-24 07:00:04 -07:00
Colin Megill a657eb3152 Logo (#782)
* logo, black

* fixes

* remove template, move header
2019-05-23 11:47:24 -04:00
Bruce Martin ef7c26e799 correctly handle selection of trunctated categories (#781) 2019-05-23 08:46:22 -07:00
Bruce Martin 49af278de7 cleanup memoiziation in graph component (#783) 2019-05-22 17:37:18 -07:00
Bruce Martin 2357d0c1b8 layout change UI (#776)
* add layout to schema

* add layout choice action and reducer

* multi layout UI

* update unit tests

* add missing file

* update test schema

* fix duplicate test id

* fix tabs

* PR lint

* fix pytest
2019-05-22 13:21:33 -07:00
Charlotte Weaver 63af79d3f8 Add developer guidelines (#769)
* Add developer guidelines

* minor formatting

* PR clarifications/lint

* more pr fixes

* link fix

* below->above

* pr suggestions
2019-05-21 13:57:34 -07:00
Bruce Martin fcc05f6a00 coordinate system fixes for embedded graph (#768)
* change pan speed to 1 per issue #722

* correct handle scaling of graph when aspect ratio less than one

* add package lock

* add invert to our scale functions

* correctly transform to/from gl coordinates

* remove unused import

* fix naming of import

* update smoke tests
2019-05-20 14:22:41 -07:00
Bruce Martin de3407d875 change scripts to support windows (#775) 2019-05-20 11:42:57 -07:00
Charlotte Weaver 2d4e827bea wait for element before getting text/html (#777) 2019-05-20 11:35:53 -07:00
Bruce Martin 1fa4838863 npm (js) package dependency updates (#765)
* JS package dependency updates

* additional package updates

* more package version updates

* more js package updates

* more JS dependency updates
2019-05-20 10:11:49 -07:00
Charlotte Weaver ab4c74a321 remove psutil (#773) 2019-05-18 10:53:45 -07:00
Charlotte Weaver 82d65addec always run smoke tests (#772) 2019-05-18 10:50:17 -07:00
Charlotte Weaver e2ad28a510 exclude recent scipy versions (#770) 2019-05-17 15:13:22 -07:00
Bruce Martin efa1709158 add multi-layout support to back-end (#766)
* add multi-layout support to back-end

* remove obsolete code

* temporary code to apply heuristic choice of default layout

* fix tests

* update python tests

* more py lint

* PR review changes

* more PR lint

* PR lint
2019-05-16 14:49:22 -07:00
Charlotte WeaverandTony Tung d6040f687a port retry (#761)
* WIP

* import find_available_port method

* move method to utils

so I can add to eventually add to gui

* add fixed-port flag to tests

* Update server/utils/utils.py

Co-Authored-By: Tony Tung <tonytung@merly.org>

* pr review suggestions

* pr review suggestions

* fix outdated package.json

* update error message

* simplify find_available_port function

* Auto scan for ports unless port is specified.

* fix tests

* fix comment for find_available_port

* lint error

* differentiate port error from generic os error

* add errno to OSerror

* pr review fixes

* raise e -> raise

* oserror -> socket error
2019-05-14 14:04:13 -07:00
Bruce Martin b9a1e30652 large file size guardrails (#763)
* large file guardrails

* fix lint

* PR review

* remove unused import

* use standard slice for CSR

* revert change
2019-05-13 18:13:16 -07:00
Bruce Martin 7adac5d004 create occupancy stacks for all category values, not just top N values (#764) 2019-05-13 11:22:46 -07:00
Charlotte Weaver 2354731083 install from dist instead of build on travis (#760) 2019-05-09 15:32:06 -07:00
Charlotte Weaver d522cc8f91 Add clipping test to smoke tests (#757)
* Add clipping test to smoke tests

* devtools on in debug
2019-05-09 15:31:55 -07:00
Charlotte Weaver 86eb01eb2c improve release process (#752)
* Add --no-cache-dir to make release-install target

Prevents installing from cache so you get the freshest release

* Testing releases is not optional

* Updated release documentation
2019-05-08 09:36:00 -07:00
Charlotte Weaver 8a94b1e086 fix #754 (#755) 2019-05-07 12:43:20 -07:00
Bruce Martin 846b8d15bd lodash cleanup (#747)
* add own range() function

* lodash cleanup

* remove redundant fill range implementations

* remove use of _.get

* sync test babel config with build

* update tests to match new range implementation
2019-05-06 20:28:32 -04:00
Charlotte Weaver c12cb2424a release bugfix (#749)
* add __init__.py

* bump version
2019-05-06 10:16:36 -07:00
Bruce Martin 1471d6b214 release 0.9.0 (#746) 2019-05-04 08:41:56 -07:00
Charlotte Weaver 98b63fa9ea Moved to python threads (#745)
So we could use daemon threads
2019-05-03 14:48:48 -07:00
Bruce Martin 0aa0f641ab improve selection interaction with clip changes (#744)
* brush interactions with underlying dataframe updates improved

* improve comment

* reset selection state upon clip
2019-05-03 11:14:30 -07:00
Charlotte Weaver 77a4495b28 Update README.md (#743)
Fixes #702
2019-05-02 16:54:39 -07:00
Bruce Martin b521a17ffd improve column access speed for sparse matrices (#742)
* improve column access speed for sparse matrices

* add FAQ entry about data format performance

* add note about using --sparse flag for prepare command

* clean up for PR review

* Update docs/faq.md

Co-Authored-By: bkmartinjr <bruce@chanzuckerberg.com>

* improvements to big data faq
2019-05-01 15:44:01 -07:00
Colin Megill 86bf64e793 avoid overflow on clip dialogue (#740) 2019-05-01 15:41:54 -04:00
Charlotte Weaver 8a72010768 PyQt5 -> PySide2 (#738) 2019-05-01 09:03:16 -07:00
Sidney Bell a08e19bbd0 Clip continuous values based on percentile cutoffs (#672)
* Add numeric inputs for percentiles

* Define initial values for percentile cutoffs in world reducer

* add percentil to crossfilter dimensions

* worldEqUniverse now handles cloned worlds

* add Dataframe.mapColumns

* Wire up handlers for percentile inputs

* World reducer and stateManager know about continuousPercentileMin/Max

* Create world as universe clone (not pointer) to avoid clobbering vals

* Define basic actions for setting continuousPercentileMin/Max

* Under the hood, deal with percentiles between 0 and 1

* Move percentile inputs to visualization settings menu

* Fix padding for undo/redo buttons

* Trigger world rebuild from percentile actions

* BROKEN - pseudocode for clamping dataframe by percentiles upon world rebuild

* fix error handling on clip quantiles; start world clipping implementation

* more unclipped reorg

* rename crossfilter.percentile to quantile

* simplify schema access

* update continuous legend when scale changes

* update color cache when clip changes

* clip obs annotations and var data when clip quantile changes

* use own fromEntries

* fix tests

* stable non-finite float sort/search

* clarify comments

* fix syntax typo

* use new stand-alone clip

* clip expresssion data

* add select tests for non-finite scalars

* basic styles

* clip UI now requires explicit commit

* reset enable/disable accounts for clip percentiles

* better error messages

* fix bug in undo interaction with programatic min brush selection

* small refactoring

* support clipping of int data

* do not perform unnecessary summarizations

* improve caching of dataframe compiled columns

* add percentile precompute to Dataframe.summarize

* use Dataframe.summarize for clip percentiles

* remove obsolete quantile code from corssfilter

* histogram scale and label Y axis, add unclipped X range labels

* layout tweaks

* scatterplot now updates when clip changes

* improve comments

* remove debugging comment

* rework clip number entry validation for usability

* ui tweaks to histogram colors and layout

* enable undo/redo for clip user action

* refine UI on clip value entry

* api cleanup

* update confusing comment

* clarify purpose of isValidDigitKeyEvent

* fix misleading comment

* apply appropriate button-group classes; do not mix span and div

* variable name and comment changes suggested in PR review

* rename sort to sortArray; remove unused and dead code path

* naming changes suggested in PR review

* code review improvements for clarity

* more small changes from PR review

* lint fixes for PR review

* fix spelling error

* clarify that function performs in-place modification of world

* add comment to clarify intent of range operation

* fix bad indents in comments

* clean up __columnsAccessor comments and code

* improve comments around clipPredicate

* field name consistency

* improve comment on quantiles params
2019-04-30 16:20:10 -07:00
Charlotte Weaver 9f10d8095a GUI app (experimental) (#730)
* add default config

* first pass

* flake8

* cleanup

* first pass at using qthreads

* cleanup

* WIP

* better error handling

* improved UI

* bugfix

* fix merge bugs

* import order

* cleanup

* make gui requirements optional

* pr review requested changes

* Update server/gui/main.py

Co-Authored-By: csweaver <charlottesweaver@gmail.com>

* pr review request

* qt child class methods -> camelCase

* whitespace
2019-04-30 12:52:15 -07:00
Charlotte Weaver ea187f48e0 add scripts from cli (#680)
* add scripts from cli

* add warning when including scripts

* flake8 fixes

* confirm scripts injection
2019-04-22 12:30:57 -07:00
Charlotte Weaver 9c6273eb94 core library (#711)
* move app creation to function

* create engine without load

* flake 8 fixes

* cleanup original scanpy test

* add default config

* handle missing data

* test data changes

* unify update

* load data isn't static anymore

* make app a class
2019-04-22 12:24:07 -07:00
Justin Kiggins 1f735abe2b updates the roadmap & reorganizes the README (#712) 2019-04-18 13:10:03 -07:00
Colin Megill b878b0f93c Gene typeahead stale state (#714)
* reimplementing suggest

* resolve stale state
2019-04-17 14:03:10 -04:00
Charlotte Weaver ad0a3c939c faster ci (#713)
* parallelize docker build

* cache npm too

* testing skip install
2019-04-15 16:34:40 -07:00
Charlotte Weaver d581a0d460 color by gene smoketest (#707)
* add test for color by gene expression

gene expression and metadata color by are handled differently

* error on console.error

not just on thrown errors
2019-04-11 17:06:36 -07:00
Bruce Martin 9044b8d85d fix color-by regression in toggle (#705)
* fix color-by regression in toggle

* fix incorrect field reference
2019-04-11 12:47:38 -07:00
Charlotte Weaver 73852dd6f4 use flask's json (#703) 2019-04-09 12:59:09 -07:00
Bruce Martin 5e02408732 latent bug in color toggle (#701) 2019-04-09 10:50:35 -07:00
Sidney Bell 34e5a91dc6 Add calculate_qc_metrics to `prepare (#697)
* Calculate QC metrics

* Add QC metrics to prepare section of readme

* Add pointer to scanpy qc metrics function

* Don't explicitly pass qc flag as arg

* Add explicit toggle for run-qc/skip-qc

* Move qc metrics calculation to separate step/function
2019-04-09 08:56:07 -07:00
Bruce Martin 7275d9d4dc Graph selection state management and history bug fixes (#679)
* save graph selection in redux state

* fix old graph brush select regressions

* refactor graph brush selection to work with undo/redo

* update tests to match new crossfilter spatial select API

* graph selection state now in redux

* remove dead code

* sync graph selection with redux state; improvements to undoable machinery

* fix regression in undoable

* differentiate graph selection cancel from deselect action

* simplify calculation

* remove debugging code

* fix responsive repaint bug in graph selection tool

* undoable debugging and code cleanliness

* undoable action filter state now merges, rather than replaces

* improve comments

* add debounce to undoable action filter; improve comments and debug sanity check code

* comments

* fix undoable bug with clear scatterplot actions

* disable undoable debug flag

* cleanup API and comments around statemachine

* add test id attribute to lasso

* add better error handling for gene fetch requests
2019-04-08 15:53:12 -07:00
Charlotte Weaver c9a8e3ea42 Add manual UX tests (#699) 2019-04-08 13:23:17 -07:00
Bruce Martin 71505abe4c toggle color-by when repeatedly picked by the user (#696)
* toggle color-by when repeatedly picked

* remove debugging code
2019-04-08 10:57:47 -07:00
Sidney Bell c9a56fa73a Update scanpy version (#688)
Update to version 1.3.7
2019-04-04 12:16:30 -07:00
Charlotte Weaver 3167e38993 update docs to reflect python 3.7 support (#685) 2019-04-02 09:24:50 -07:00
Charlotte Weaver ce8326ddea bump version (#684) 2019-04-01 14:42:22 -07:00
Charlotte Weaver 5d4b11d287 Better smoke tests (#669) 2019-03-28 16:47:42 -07:00
Charlotte Weaver 096d477893 use Math.abs when calculating lasso area (#673) 2019-03-27 21:02:31 -07:00
Bruce Martin 153f240c43 brushable histogram brush selection responds to state updates (#670)
* add continuousSelection reducer and make histo respond to state changes

* correctly handle uninitialized state for brush move

* PR review tweaks
2019-03-25 15:02:39 -07:00
Gökçen Eraslan df46db965a bin/build-client.sh does not exist. (#671) 2019-03-25 10:26:27 -07:00
Bruce Martin 7ef5203564 Undo/redo (#659)
* immutable crossfilter

* first cut at reducer refactor with cascade model

* add initial redo/undo implementation

* small optimization

* integrate expression with history

* add tests for new reducers and fix a couple of small initialization bugs

* treat tiny lasso selections as a clear

* better function name for clarity

* fix undo for differential expression

* remove logging

* fix regression due to bad merge

* cleanup and comments for clarity

* improve undoable configuration for flexibility

* fix stale comments

* remove debugging code from production build

* rename categoricalSelectionState

* rename file

* improve comments
2019-03-22 14:53:48 -07:00
Bruce Martin 9420abfacc add Babel support for JS optioning chaining and nullish coallescing operators (#664) 2019-03-22 14:51:51 -07:00
Sidney Bell 16f93397ae Support diffmap and phate layouts. Explicitly handle embeddings with >2 components. (#662)
* Add diffmap and phate to supported embeddings

* Explicitly pull the first two components of any given layout
2019-03-21 19:06:05 -07:00
Colin Megill 016a4a422a Procedurally resize brush (#650)
* add on brush end event

* brush snap move

* resize brush
2019-03-20 14:46:20 -07:00
Bruce Martin 996b06cecc refactoring - immutable crossfilter (#647)
* immutable crossfilter

* PR review changes
2019-03-20 14:32:35 -07:00
Bruce Martin 571b7387e7 remove dead code from globals (#651) 2019-03-19 06:22:39 -07:00
Charlotte Weaver b6d468376a py37 fixes (#646)
* add python version for nightly build

* update numpy version required

older versions interact poorly on anaconda + python 3.7
2019-03-14 14:51:55 -07:00
Charlotte Weaver d8fc7e40a1 Support python3.7 (#645)
* Support python3.7

* add 3.7 env to travis
2019-03-14 12:08:46 -07:00
Charlotte Weaver e875ed739b More informative out-of-memory error (#644)
* more informative error message: memory error

* flake fix
2019-03-12 14:19:00 -07:00
Bruce Martin aa5ce4a2f1 correctly toggle group selection in categorical metadata (#640) 2019-03-12 10:49:29 -07:00
Bruce Martin 80969012c9 do not reset color scale when dismissing scatterplot (#637) 2019-03-12 10:49:01 -07:00
Bruce Martin caaee7e9bf do not reset color-by when subsetting world (#636)
* do not reset colors when subsetting to world

* revert diffexp state change
2019-03-12 10:48:35 -07:00
Charlotte Weaver f96fd36ecb Update test ui names (#638)
* Fixed changed testid

* Added debug mode for running tests
2019-03-12 10:17:09 -07:00
Bruce Martin 495dc55144 remove obsolete URL middleware (#639) 2019-03-11 16:03:33 -07:00
Colin Megill 8795f0f32c Add test ids and classes (#633)
* data test ids and classes

* suggest
2019-03-07 13:01:41 -08:00
Colin Megill a0f54b4871 Add menu (#631)
* add menu

* Added library versions to config

and tests

* add template version number
2019-03-06 11:56:46 -05:00
Charlotte Weaver 68dfbcc2eb Add force graph to docs and example dataset (#630) 2019-03-05 20:59:54 -08:00
Alex Wolf 82493d1019 Add forced directed graph drawing to allowed layout options (#626)
* added forced directed graph drawing layout options

* added line breaks for 120 character limit
2019-03-05 13:10:07 -08:00
Bruce Martin 3d6df0c044 implement improved disable/enable of Reset UI (#628) 2019-03-05 08:38:55 -08:00
Charlotte Weaver 2583016693 Fix bug where prod would automatically run after release stage 2 (#625)
* Fix bug where prod would automatically run after release stage 2

* fixed make release-burned
2019-03-01 20:20:16 -08:00
Bruce Martin d8e3721846 bumpversion to 0.7.0 (#623) 2019-03-01 12:25:30 -08:00
Bruce Martin a876740a3c create helper file for controls reducer (#615)
* initial dataframe commit

* initial dataframe port of core app

* rename variables for clarity

* remove unused import

* comment out unused code

* fix array handling bug in crossfilter dimension creation

* allow creation of empty dataframes

* handle non-existent columns

* handle non-existent columns

* revise tests for new dataframe

* comments for clarity

* comments for clarity

* generate bulk add placeholder with real gene names

* fix bug in gene name adding

* more dataframe unit tests

* fix bug - subset from current world, not universe

* put cut and pasted code into a single function

* improve caching of crossfilter

* remove cascading update bug from graph

* more performance work

* improve state handling for scatterplot

* performance optimization of critical path

* add column summarization

* dataframe utils

* add callOnceLazy

* fix tests

* minor updates found during review

* fix misspelling

* remove RESTv02 from function names

* comment cleanup

* cut/icut col parameter defaults to null

* break up large test

* improve tests and comments on dataframe at/has functions

* add Dataframe withCol/dropCol

* expression varData now stored in a dataframe

* dead code cleanup

* use dataframe.summarize()

* test cases for Dataframe.col.summarize

* update test cases for new dataframe summarize

* improve naming

* use new hasCol API

* add comments

* add more Dataframe.withCol tests

* add ability to specify row index in cut operation

* retire subsetVarData function

* correctly handle expression subsetting

* lint and improve comments

* rename cut to subset

* create helper file for controls reducer
2019-02-28 09:21:19 -08:00
Bruce Martin e7ad6f5d1c [WIP DO NOT MERGE] correctly display graph legend for negative continuous metadata (#620)
correctly display graph legend for negative continuous metadata
2019-02-28 09:08:19 -08:00
Bruce Martin 2f1facaafb [WIP DO NOT MERGE] suppress display of continous annotation without a finite extent (#618)
* initial dataframe commit

* initial dataframe port of core app

* rename variables for clarity

* remove unused import

* comment out unused code

* fix array handling bug in crossfilter dimension creation

* allow creation of empty dataframes

* handle non-existent columns

* handle non-existent columns

* revise tests for new dataframe

* comments for clarity

* comments for clarity

* generate bulk add placeholder with real gene names

* fix bug in gene name adding

* more dataframe unit tests

* fix bug - subset from current world, not universe

* put cut and pasted code into a single function

* improve caching of crossfilter

* remove cascading update bug from graph

* more performance work

* improve state handling for scatterplot

* performance optimization of critical path

* add column summarization

* dataframe utils

* add callOnceLazy

* fix tests

* minor updates found during review

* fix misspelling

* remove RESTv02 from function names

* comment cleanup

* cut/icut col parameter defaults to null

* break up large test

* improve tests and comments on dataframe at/has functions

* add Dataframe withCol/dropCol

* expression varData now stored in a dataframe

* dead code cleanup

* use dataframe.summarize()

* test cases for Dataframe.col.summarize

* update test cases for new dataframe summarize

* improve naming

* use new hasCol API

* add comments

* add more Dataframe.withCol tests

* add ability to specify row index in cut operation

* retire subsetVarData function

* correctly handle expression subsetting

* lint and improve comments

* rename cut to subset

* suppress display of continous annotation withont a finite extent

* fix botched merge

* more fix of botched merged
2019-02-28 09:05:48 -08:00
Bruce Martin ffd6273419 Dataframe, part deux - add varData and summarize() (#608)
* initial dataframe commit

* initial dataframe port of core app

* rename variables for clarity

* remove unused import

* comment out unused code

* fix array handling bug in crossfilter dimension creation

* allow creation of empty dataframes

* handle non-existent columns

* handle non-existent columns

* revise tests for new dataframe

* comments for clarity

* comments for clarity

* generate bulk add placeholder with real gene names

* fix bug in gene name adding

* more dataframe unit tests

* fix bug - subset from current world, not universe

* put cut and pasted code into a single function

* improve caching of crossfilter

* remove cascading update bug from graph

* more performance work

* improve state handling for scatterplot

* performance optimization of critical path

* add column summarization

* dataframe utils

* add callOnceLazy

* fix tests

* minor updates found during review

* fix misspelling

* remove RESTv02 from function names

* comment cleanup

* cut/icut col parameter defaults to null

* break up large test

* improve tests and comments on dataframe at/has functions

* add Dataframe withCol/dropCol

* expression varData now stored in a dataframe

* dead code cleanup

* use dataframe.summarize()

* test cases for Dataframe.col.summarize

* update test cases for new dataframe summarize

* improve naming

* use new hasCol API

* add comments

* add more Dataframe.withCol tests

* add ability to specify row index in cut operation

* retire subsetVarData function

* correctly handle expression subsetting

* lint and improve comments

* rename cut to subset

* changes based on PR review
2019-02-28 08:34:22 -08:00
Charlotte Weaver 2bae696986 Smoke tests (#604)
smoke tests
2019-02-27 15:46:58 -08:00
Bruce Martin 6b33315cbe Dataframe (#576)
* initial dataframe commit

* initial dataframe port of core app

* rename variables for clarity

* remove unused import

* comment out unused code

* fix array handling bug in crossfilter dimension creation

* allow creation of empty dataframes

* handle non-existent columns

* handle non-existent columns

* revise tests for new dataframe

* comments for clarity

* comments for clarity

* generate bulk add placeholder with real gene names

* fix bug in gene name adding

* more dataframe unit tests

* fix bug - subset from current world, not universe

* put cut and pasted code into a single function

* improve caching of crossfilter

* remove cascading update bug from graph

* more performance work

* improve state handling for scatterplot

* performance optimization of critical path

* add column summarization

* dataframe utils

* add callOnceLazy

* fix tests

* minor updates found during review

* fix misspelling

* remove RESTv02 from function names

* comment cleanup

* cut/icut col parameter defaults to null

* break up large test

* improve tests and comments on dataframe at/has functions
2019-02-22 11:31:34 -08:00
Bruce Martin 57c4e9ff33 Flatbuffer cleanup (#598)
* dead code and route removal

* more dead code cleanup

* fix scanpy_engine tests

* lint

* add missing catch in filter parsing

* update scanpy NaN tests

* more fbs tests and dead test removal

* remove forced default for content type negotiation

* bit of cleanup

* more fbs test cleanup

* lint

* remove swagger

* swagger cleanup

* lint

* correctly handle lack of templates

* more dead code removal

* remove unused files

* fix dev build

* lint
2019-02-19 08:50:29 -08:00
Charlotte Weaver 4e67c645f8 bumped version (#602)
0.6.0 was burned on pypi
2019-02-14 10:00:16 -08:00
Charlotte Weaver 8b28d51dfa bump version (#601) 2019-02-13 16:29:14 -08:00
Charlotte Weaver 40ad283107 create server testing doc (#592) 2019-02-13 15:35:45 -08:00
Isaac Virshup 0f8d7a55de Set API path based on access address (#568)
* Make api paths relative

* Remove request import

* Set publicPath to be relative
2019-02-11 09:01:37 -08:00
Sidney Bell b6f946ec8a Add note about installing hdf5 to FAQ (#581) 2019-02-08 14:56:16 -08:00
Colin Megill dbb3a309a9 Lasso (#586)
* lasso working

* break out invert into own function

* action

* add spatial dimension to crossfilter, in support of polygon lasso

* improve comments on new dimension API

* lasso vs zoom
2019-02-08 11:48:51 -08:00
Bruce Martin 2e9525741f doc divergence warning (#591) 2019-02-08 11:00:51 -08:00
Charlotte Weaver 585a5808b9 check if accept type in content type string (#589) 2019-02-08 09:41:24 -08:00
Bruce Martin 6f464f4f92 package dependency updates (#585)
* lint

* update dev-related package dependencies
2019-02-06 12:57:09 -08:00
Colin Megill 08ea7d5137 Better input validation (#580) 2019-02-05 10:44:59 -05:00
Charlotte Weaver ad9be3cdd7 remove build-dev from .gitignore (#583) 2019-02-04 14:47:28 -08:00
Charlotte Weaver f737cc4ee4 Build improvements (#577) 2019-02-04 14:15:35 -08:00
Charlotte Weaver 1103272b95 De-dupe -d CLI option alias (#575)
removed from debug, diffexp gets to keep it
2019-01-30 16:11:21 -08:00
Colin Megill 2df7161cd8 Bulk add genes (#567)
* bulk add

* cleanup
2019-01-29 16:26:09 -05:00
Charlotte Weaver d31c05c970 Add backed script to package.json (#566)
* QOL script for FE devs to get & launch the backend

* ensure python3.6

* changed name to backend-dev
2019-01-29 11:41:05 -08:00
fionagriffin 07db2eb3ee Update data.md (#544) 2019-01-29 10:31:31 -08:00
Justin Kiggins a6d2a2e119 updates link to scanpy recipe docs. fixes #564 (#565) 2019-01-28 10:00:29 -08:00
Justin Kiggins f87e4bfbd3 home page fix (#551)
* explicit site.url in config

* infra for custom css

* moves home item to html layout

* adds baseurl to site config
2019-01-28 09:13:09 -08:00
Charlotte Weaver ecaa32cfb2 bump version (#563) 2019-01-17 17:14:44 -08:00
Charlotte Weaver 10693b08cc Add __init__ file so fbs can be imported (#562) 2019-01-17 17:11:31 -08:00
Charlotte Weaver b02361a6bf bump version (#560) 2019-01-17 16:54:25 -08:00
Colin Megill 3fa888c6d4 user defined gene sets scatterplot, remove works (#556) 2019-01-16 03:37:08 -05:00
Colin Megill a42c9aca1c resetting interface spinner but locked interface (#555) 2019-01-16 03:36:35 -05:00
Charlotte Weaver 394da40bea Doc formatting improvements (#549) 2019-01-10 15:09:02 -08:00
Bruce Martin 5d60505407 remove --nan-to-num CLI parameter (#548)
* remove --nan-to-num CLI parameter

* factor tests better

* lint - remove unused variables
2019-01-10 15:03:03 -08:00
Colin Megill f876a0091a Set theme jekyll-theme-cayman 2019-01-10 12:12:58 -05:00
Colin Megill 528eb5d172 Set theme jekyll-theme-minimal 2019-01-10 12:10:11 -05:00
Bruce Martin b90447c387 binary wire format with flatbuffers (#509)
* first flatbuffer schema

* do not lint auto-generated files

* add flatbuffers package

* add flatbuffer module

* wire up /data/X/T route

* use flatbuffers for matrix data fetc

* clarity and comments

* add flatbuffer layout route

* clean up obsolete code

* fix tests

* move flake8 config to setup.cfg

* add comments

* lint

* rework layout routes for fbs

* add more type support to fbs

* lint

* add flatbuffer support for annotations

* function name improvements

* fix botched merge with master

* remove unused import

* route cleanup for flatbuffers

* rename function for clarity

* add missing globals to Jest tests

* fix client JS tests

* fix routes for Python tests

* comments for clarity

* non-finite floating point hardening

* more non-finite number handling

* lint

* fix tests for summarizeAnnotations

* harden diffexp calculation against FP errors

* cleanup unused code

* lint

* add encoding tests for flatbuffers

* application type specified as strings

* fix spelling error

* improve variable names

* add note about documentation gap

* rename FBS DataFrame to Matrix
2019-01-09 14:26:05 -08:00
Colin Megill 42e25a1a1f Async on long running operations (#543)
* compute differential button

* add user gene loading state to reducer

* create input group, consume usergeneloading

* add loading state
2019-01-07 16:45:47 -05:00
Justin Kiggins eceab377f7 cleans up landing page (#541) 2019-01-04 14:20:23 -08:00
Colin Megill b0daeb3a9b disable cell set buttons (#538) 2019-01-03 20:05:09 -05:00
fionagriffin 7499fca251 add datasets to data.md (#540)
added links and general info for interesting example datasets to use with cellxgene
2019-01-03 16:35:37 -08:00
GenevieveHaliburton 05a5a945bb Landing page faq comments (#536)
* add to faq, clean up titles

* h4 headings for readibility

* Ordering, add section on errors
2019-01-03 14:07:38 -08:00
Colin Megill d3c96087b2 Add GIFs to docs (#537)
* Add gif files

* rename gif for clarity

* add gifs

* reorder

* resize
2019-01-03 15:24:33 -05:00
Bruce Martin cfcf16aa69 [DO NOT MERGE] performance improvements in color-by feature (#535)
performance improvements in color-by feature
2018-12-27 14:05:49 -08:00
Justin Kiggins bc59074300 new docs (#525)
* new docs

* moves development docs to dev_docs
2018-12-20 14:28:05 -08:00
Colin Megill 56f9bc543e Set theme jekyll-theme-minimal 2018-12-20 12:05:08 -05:00
James Taylor f2eb2cad82 Use url_root from Flask rather than hardcoding hostname. (#520)
Allows running in a container on a remote host, should also allow running
behind a proxy with a url prefix.
2018-12-19 16:20:26 -08:00
Justin Kiggins bd523280a4 improved quickstart (#523) 2018-12-19 13:38:30 -08:00
Justin Kiggins fc45fb3899 creates FAQ page in docs (#522)
* creates FAQ page

* revise index.md

* tweaks
2018-12-19 13:18:16 -08:00
Charlotte Weaver f0f7200f0b Move jsonification to engine level (#511) 2018-12-18 21:14:24 -08:00
fionagriffin 6af7708d62 update index.md (#519)
ready for features
2018-12-18 11:10:29 -05:00
Colin Megill a297ea30e2 bump version (#518) 2018-12-17 14:30:34 -05:00
Colin Megill 0ef6c36f4c Cluster Occupancy (n dim cube) (#513)
* create occupancy component

* occupancy working

* centering, width, flex spacing

* add is-number

* extend sort to cover string + int

* proof of concept sorted occupancy

* handle undefined occupancy entry, cleanup

* only render occupancy when colorby is cat

* cleanup unused vars

* cleanup
2018-12-17 14:19:48 -05:00
Justin Kiggins aaa60bc303 new signup url (#516) 2018-12-14 16:32:41 -08:00
fionagriffin 7d40d89fd8 Update index.md (#507)
Added content from readme
2018-12-14 15:19:21 -08:00
Charlotte Weaver 46ad346df1 Clarify that we do not support python 3.7 (#512)
* Update setup.py

* Readme: no python 3.7

* specify 3.6 in venv

* clarify 3.7 error

we will support it eventually
2018-12-13 15:15:56 -08:00
Charlotte Weaver 83154577e4 Black -- formatter for python (#508)
* Add black

* use black to format code

* Black version
2018-12-12 09:44:47 -08:00
Charlotte Weaver a847951658 Update slack link (#506) 2018-12-11 10:20:22 -08:00
Charlotte Weaver 24af6efbcb Add Docker file (#505)
* Add docker support

* Add readme

* minor fixes
2018-12-11 10:18:10 -08:00
Colin Megill 126cac833a Rename README.md to index.md 2018-12-11 12:09:16 -05:00
Colin Megill fc272dc42e github pages test 2018-12-11 12:05:14 -05:00
Colin Megill d3a0d66139 Set theme jekyll-theme-architect 2018-12-10 18:16:40 -05:00
Charlotte Weaver 9604231a2a Add alternative to install [louvain] extra requires (#500)
* louvain install by hand

* reworded
2018-12-10 14:46:55 -08:00
Jeremy Freeman 6ea3b7f3cf use collapsable details to improve FAQ formatting (#503) 2018-12-07 21:07:57 +01:00
Charlotte Weaver 0d0a32f272 Added PR guidelines (#492)
* Added PR guidelines

Added document to cover guidelines for creating, reviewing and merging PRs.

* fixed typo

* another typo

* grammar

* Remove 5's title

The subheading is clearer as a top-level
2018-12-06 13:43:10 -08:00
Bruce Martin 48e0ea542b clarifications to release recipe (#499)
* clarifications to release recipe

* fix spelling errors
2018-12-05 16:58:53 -08:00
Bruce Martin 95fd20b405 bump version to 0.3 (#498) 2018-12-05 14:35:17 -08:00
Charlotte Weaver 5c6cc597c8 Default warning in debug mode (#497) 2018-12-05 13:38:34 -08:00
Bruce Martin 3bfeadc2b9 improve handling of non-finite floating point values (#490)
* improve HTTP error reporting

* generate standards-compatible JSON

* add --nan-to-num work-around for non-finite floating point values

* lint

* update tests

* correctly set Infinities to min/max

* REAMDE update for --nan-to-num

* define constant for repetitive warning message

* clarify where NaN errors will occure
2018-12-04 14:56:16 -08:00
Bruce Martin 296ed752fa Improved summary counts of annotation values (#478)
* convert annotation summary to a Map

* add 2d annotation count summary

* add memoization on 2D annotation counting

* add tests for annotation summarization

* fix import/exports

* rename WorldOps to WorldUtil

* rename WorldOps to WorldUtil

* add comment
2018-12-03 09:15:03 -08:00
Charlotte Weaver 1e66ec2b89 Update scikit learn (#487)
They finally fixed their cloud pickle issue
2018-11-30 12:02:28 -08:00
Charlotte Weaver 4a50f0ed46 External host option (#481)
* allow specifying host ip instead of listen all

* flask serving

* Limit to ip addresses only
2018-11-29 17:01:49 -08:00
Bruce Martin af0d1f6fb2 issue #480 workaround (#484)
* only load annotation var names

* remove incorrect usage of var annotation data

* temporary workaround for issue #480

* lint

* issue warnings only once per item
2018-11-29 16:33:21 -08:00
fionagriffin a83ec60308 add FAQs to readme (#482)
Added question and answer to FAQ; added link to scanpy documentation on scientific recipes.
2018-11-29 13:59:10 -08:00
Charlotte Weaver 76c0cbf16b loose -> lose (#479) 2018-11-28 13:07:10 -08:00
Charlotte Weaver 455d987398 Add custom format for warnings (#477)
[cellxgene] Warning: <message>
2018-11-28 12:47:11 -08:00
Colin Megill 00b9a8fa72 Fuzzy search on genes (#473)
* proof of concept blueprint typeahead

* poc with fuzzysearch lib

* typeahead adds gene on enter

* add gene on menu click

* typeahead clears correctly

* cleanup
2018-11-28 09:50:02 -08:00
Charlotte Weaver 28f5263c88 bump version (#475) 2018-11-27 15:43:19 -08:00
Bruce Martin 933fac5fff Allow float64 to down-cast to float32 (#472)
* warn if annotations will down-cast to float32

* correctly handle falsey data values
2018-11-27 11:17:30 -08:00
523 changed files with 76051 additions and 19527 deletions
+15 -5
View File
@@ -1,15 +1,25 @@
[bumpversion] [bumpversion]
current_version = 0.2.2 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] [bumpversion:file:setup.py]
search = version="{current_version}" search = version="{current_version}"
replace = version="{new_version}" replace = version="{new_version}"
[bumpversion:file:server/cli/cli.py]
search = version="{current_version}"
replace = version="{new_version}"
[bumpversion:file:client/package.json] [bumpversion:file:client/package.json]
search = "version": "{current_version}" search = "version": "{current_version}"
replace = "version": "{new_version}" replace = "version": "{new_version}"
[bumpversion:file:server/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
+8
View File
@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true
+5
View File
@@ -0,0 +1,5 @@
bin
client
dist
docs
server
+32
View File
@@ -0,0 +1,32 @@
---
name: "\U0001F41E Bug report"
about: Create a report to identify a bug in cellxgene
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version (please complete the following information):**
- Desktop or hosted?:
- Browser (if hosted) [e.g. chrome, safari]:
- Version [e.g. 0.13.0]:
**Additional context**
Add any other context about the problem here.
@@ -0,0 +1,20 @@
---
name: "\U0001F4A1 Feature request"
about: Suggest an idea for this project
title: "[FEATURE REQUEST]"
labels: user request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,10 @@
---
name: "\U0001F9D0 Question/Clarification"
about: Ask a question or for a clarification
title: "[QUESTION]"
labels: question
assignees: ''
---
+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
+112
View File
@@ -0,0 +1,112 @@
name: Compatibility Tests
on:
schedule:
- cron: "0 8 7 * 2"
push:
branches:
- main
env:
JEST_ENV: prod
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build docker image
run: docker build .
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:
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@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
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: ${{ env.PIP_CACHE }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Node cache
uses: actions/cache@v4
with:
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: |
pip install -r server/requirements-dev.txt
make pydist install-dist
- name: Install cellxgene from latest release (pypi.org)
if: matrix.cellxgene_build == 'latest'
run: |
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
@@ -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 }}
+147
View File
@@ -0,0 +1,147 @@
name: Push Tests
on:
push:
branches: main
pull_request:
branches: "*"
env:
JEST_ENV: prod
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
git fetch --depth=1 origin +${{github.base_ref}}
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Node cache
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: |
pip install flake8
pip install black
cd client
npm install
- name: Format with black and lint with flake8
run: |
make lint-server
- name: Lint src with eslint
working-directory: ./client
run: |
npx eslint src __tests__
unit-test:
runs-on: ubuntu-latest
steps:
- 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:
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@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Node cache
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 dev-env-server
- name: Unit tests
run: make unit-test-server unit-test-client
- name: Generate server coverage XML
run: coverage xml -o server/coverage.xml
- name: Upload server coverage
uses: codecov/codecov-action@v5
with:
flags: server,python,unitTest
files: ./server/coverage.xml
fail_ci_if_error: false
- name: Upload client coverage
uses: codecov/codecov-action@v5
with:
flags: frontend,javascript,unitTest
files: ./client/coverage/lcov.info
fail_ci_if_error: false
smoke-tests:
runs-on: macos-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Python cache
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@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: |
pip install setuptools
make pydist install-dist
- name: Smoke tests (without annotations feature)
run: cd client && make smoke-test
# 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
+28 -6
View File
@@ -3,6 +3,8 @@ node_modules/
# coverage # coverage
coverage coverage
coverage.xml
.coverage
.nyc_output .nyc_output
.pytest_cache/ .pytest_cache/
@@ -13,20 +15,26 @@ dist/
*.egg-info *.egg-info
# Environments # Environments
venv/ venv*/
cellxgene/ cellxgene/
# client build # client build
server/app/web/static/css/ server/common/web/static/*
server/app/web/static/img/ server/common/web/templates/
server/app/web/static/media/ server/common/web/csp-hashes.json
server/app/web/static/js/
server/app/web/templates/index\.html # eb build
artifact.dir
artifact.zip
customize
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints .ipynb_checkpoints
*.ipynb *.ipynb
# cefpython
error.log
# misc # misc
.DS_Store .DS_Store
npm-debug.log npm-debug.log
@@ -35,3 +43,17 @@ npm-debug.log
__pycache__ __pycache__
*.DS_Store* *.DS_Store*
data data
tags
myconfig.yaml
# Jekyll
docs/_site/
docs/Gemfile.lock
client/.eslintcache
# E2E Testing
ignoreE2E*
# annotate subcmd
.models_cache
-20
View File
@@ -1,20 +0,0 @@
language: python
python:
- "3.6"
node_js:
- "8"
cache:
pip: true
install:
- set -eo pipefail
- pip install flake8
- ./bin/build-client
- pip install -e .
- pip install -r server/requirements-dev.txt
script:
- set -eo pipefail
- flake8 server/app/
- flake8 server/cli/
- npm run --prefix client/ build
- npm run --prefix client/ test
- pytest -s server/test
+3
View File
@@ -0,0 +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://github.com/chanzuckerberg/cellxgene-documentation/blob/main/contribute.md).
+11
View File
@@ -0,0 +1,11 @@
FROM ubuntu:bionic
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
RUN apt-get update && \
apt-get install -y build-essential libxml2-dev python3-dev python3-pip zlib1g-dev python3-requests python3-aiohttp && \
python3 -m pip install --upgrade pip && \
pip3 install cellxgene
ENTRYPOINT ["cellxgene"]
+3 -3
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) # The MIT License (MIT)
Copyright (c) 2013 Copyright (c) 2017-2026 Chan Zuckerberg Initiative
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in
@@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+7 -3
View File
@@ -1,4 +1,8 @@
recursive-include server/app/web/templates * recursive-include server/common/web/templates *
recursive-include server/app/web/static * recursive-include server/common/web/static *
include server/requirements.txt 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/*
+220
View File
@@ -0,0 +1,220 @@
include common.mk
BUILDDIR := build
CLIENTBUILD := $(BUILDDIR)/client
SERVERBUILD := $(BUILDDIR)/server
CLEANFILES := $(BUILDDIR)/ client/build build dist cellxgene.egg-info
PART ?= patch
# CLEANING
.PHONY: clean
clean: clean-lite clean-server clean-client
# cleaning the client's node_modules is the longest one, so we avoid that if possible
.PHONY: clean-lite
clean-lite:
rm -rf $(CLEANFILES)
.PHONY: clean-client
clean-client:
cd client && $(MAKE) clean
.PHONY: clean-server
clean-server:
cd server && $(MAKE) clean
# BUILDING PACKAGE
.PHONY: build-client
build-client:
cd client && $(MAKE) ci build
.PHONY: build
build: clean build-client
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 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
.PHONY: unit-test
unit-test: unit-test-server unit-test-client
.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:
cd client && $(MAKE) smoke-test
.PHONY: smoke-test-annotations
smoke-test-annotations:
cd client && $(MAKE) smoke-test-annotations
# FORMATTING CODE
.PHONY: fmt
fmt: fmt-client fmt-py
.PHONY: fmt-client
fmt-client:
cd client && $(MAKE) fmt
.PHONY: fmt
fmt-py:
black .
.PHONY: lint
lint: lint-server lint-client
.PHONY: lint-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:
cd client && $(MAKE) lint
# CREATING DISTRIBUTION RELEASE
.PHONY: pydist
pydist: build
cd $(BUILDDIR); python setup.py sdist -d ../dist
@echo "done"
# RELEASE HELPERS
# 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"
# 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"
# 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 accidentally burned a test release version number,
.PHONY: release-directly-to-prod
release-directly-to-prod: pydist twine-prod
@echo "Dist built and uploaded to pypi.org"
@echo "Test the install:"
@echo " make install-release"
.PHONY: dev-env
dev-env: dev-env-client dev-env-server
.PHONY: dev-env-client
dev-env-client:
cd client && $(MAKE) ci
.PHONY: dev-env-server
dev-env-server:
pip install -r server/requirements-dev.txt
# 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:
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
.PHONY: twine-prod
twine-prod:
twine upload dist/*
# quicker than re-building client
.PHONY: gen-package-lock
gen-package-lock:
cd client && $(MAKE) install
# INSTALL
# install from source tree for development
.PHONY: install-dev
install-dev: uninstall
pip install -e .
# 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==$(VERSION)
@echo "Installed cellxgene from test.pypi.org, now run and smoke test"
# install from pypi to test your release
.PHONY: install-release
install-release: uninstall
pip install --no-cache-dir cellxgene
@echo "Installed cellxgene from pypi.org"
# install from dist
.PHONY: install-dist
install-dist: uninstall
pip install dist/cellxgene*.tar.gz
.PHONY: uninstall
uninstall:
pip uninstall -y cellxgene || :
+11
View File
@@ -0,0 +1,11 @@
#### Reviewers
**Functional:**
**Readability:**
---
## Changes
- add
- remove
- modify
+64 -188
View File
@@ -1,228 +1,104 @@
# cellxgene <img src="./docs/cellxgene-logo.png" width="300">
> an interactive explorer for single-cell transcriptomics data _an interactive explorer for single-cell transcriptomics data_
`cellxgene` is an interactive data explorer for single-cell transcriptomics datasets, such as those coming from the [Human Cell Atlas](https://humancellatlas.org). Leveraging modern web development techniques to enable fast visualizations of at least 1 million cells, we hope to enable biologists and computational researchers to explore their data, and to demonstrate general, scalable, and reusable patterns for scientific data visualization. [![DOI](https://zenodo.org/badge/105615409.svg)](https://zenodo.org/badge/latestdoi/105615409) [![PyPI](https://img.shields.io/pypi/v/cellxgene)](https://pypi.org/project/cellxgene/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/cellxgene)](https://pypistats.org/packages/cellxgene) [![GitHub last commit](https://img.shields.io/github/last-commit/chanzuckerberg/cellxgene)](https://github.com/chanzuckerberg/cellxgene/pulse)
[![Push Tests](https://github.com/chanzuckerberg/cellxgene/workflows/Push%20Tests/badge.svg)](https://github.com/chanzuckerberg/cellxgene/actions?query=workflow%3A%22Push+Tests%22)
[![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)
<img src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-demo-1.gif" width="200" height="200" hspace="30"><img src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-demo-2.gif" width="200" height="200" hspace="30"><img src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-demo-3.gif" width="200" height="200" hspace="30"> 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.
## getting started Whether you need to visualize one thousand cells or one million, CELLxGENE Annotate helps you gain insight into your single-cell data.
You'll need **python 3.6** and **Google Chrome**. The web UI is tested on OSX and Windows using Chrome, and the python CLI is tested on OSX and Ubuntu (via WSL/Windows). It should work on other platforms, but if you run into trouble let us know (see [help](#help-and-contact) below). <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">
To install run # Getting started
``` ### The comprehensive guide to CZ CELLxGENE Annotate
[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 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 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.
```bash
pip install cellxgene pip install cellxgene
``` ```
To start exploring a dataset call Launch Annotate with an example [anndata](https://anndata.readthedocs.io/en/latest/) file
``` ```bash
cellxgene launch dataset.h5ad --open cellxgene launch https://cellxgene-example-data.czi.technology/pbmc3k.h5ad
``` ```
If you want an example dataset download [this file](https://github.com/chanzuckerberg/cellxgene/raw/master/example-dataset/pbmc3k.h5ad) and then call 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 launch pbmc3k.h5ad --open
```
You should see your web browser open with the following
<img width="450" src="https://raw.githubusercontent.com/chanzuckerberg/cellxgene/master/docs/cellxgene-opening-screenshot.png" pad="50px"> CELLxGENE Annotate currently supports the following browsers:
**Note**: automatic opening of the browser with the `--open` flag only works on OS X, on other platforms you'll need to directly point to the provided link in your browser. - Google Chrome 61+
- Edge 15+
- Firefox 60+
There are several options available, such as: Please [file an issue](https://github.com/chanzuckerberg/cellxgene/issues/new/choose) if you would like us to add support for an unsupported browser.
- `--layout` to specify the layout as `tsne` or `umap` ### Finding help
- `--title` to show a title on the explorer
- `--open` to automatically open the web browser after launching (OS X only)
To see all options call We'd love to hear from you!
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).
cellxgene launch --help
```
There is an additional subcommand called `cellxgene prepare` that takes an existing dataset in one of several formats and applies minimal preprocessing and reformatting so that `launch` can use it (see [the next section](##data-formatting) for more info on `prepare`). # Developing with CZ CELLxGENE Annotate
## data formatting ### Contributing
### assumptions 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.
The `launch` command assumes that the data is stored in the `.h5ad` format from the [`anndata`](https://anndata.readthedocs.io/en/latest/index.html) library. It also assumes that certain computations have already been performed. Briefly, the `.h5ad` format wraps a two-dimensional `ndarray` and stores additional metadata as "annotations" for either observations (referred to as `obs` and `obsm`) or variables (`var` and `varm`). `cellxgene launch` makes the following assumptions about your data (we recommend loading and inspecting your data using `scanpy` to validate these assumptions) 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.
- an `obs` field has a unique identifier for every cell (you can specify which field to use with the `--obs-names` option, by default it will use the value of `data.obs_names`) ### Reuse
- a `var` field has a unique identifier for every gene (you can specify which field to use with the `--var-names` option, by default it will use the value of `data.var_names`)
- an `obsm` field contains the two-dimensional coordinates for the layout that you want to render (e.g. `X_tsne` for the `tsne` layout or `X_umap` for the `umap` layout)
- any additional `obs` fields will be rendered as per-cell continuous or categorical metadata by the app (e.g. `louvain` cluster assignments)
### prepare 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).
The `prepare` command is included to help you format your data. It uses `scanpy` under the hood. This is especially useful if you are starting with raw unanalyzed data and are unfamiliar with `scanpy`. 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.
To prepare from an existing `.h5ad` file use 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
cellxgene prepare dataset.h5ad --output=dataset-processed.h5ad
```
This will load the input data, perform PCA and nearest neighbor calculations, compute `umap` and `tsne` layouts and `louvain` cluster assignments, and save the results in a new file called `dataset-processed.h5ad` that can be loaded using `cellxgene launch`. Data can be loaded from several formats, including `.h5ad` `.loom` and a `10-Genomics-formatted` `mtx` directory. Several options are available, including running one of the preprocessing `recipes` included with `scanpy`, which include steps like cell filtering and gene selection. CZ CELLXGENE, CZ CELLXGENE DISCOVER, and CZ CELLXGENE ANNOTATE are trademarks of the Chan Zuckerberg Initiative. All rights reserved.
Depending on the options chosen, `prepare` can take a long time to run (a few minutes for datasets with 10-100k cells, up to an hour or more for datasets with >100k cells). If you want `prepare` to run faster we recommend using the `sparse` option and only computing the layout for `umap`, using a call like this 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.
```
cellxgene prepare dataset.h5ad --output=dataset-processed.h5ad --layout=umap --sparse
```
To see all options call ### Security
``` If you believe you have found a security issue, we would appreciate notification. Please send email to <security@chanzuckerberg.com>.
cellxgene prepare --help
```
**Note**: `cellxgene prepare` will only perform `louvain` clustering if you have the `python-igraph` and `louvain` packages installed. To make sure they are installed alongside `cellxgene` use # Inspiration
``` 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.
pip install cellxgene[louvain]
```
## conda and virtual environments
If you use conda and want to create a conda environment for `cellxgene` you can use the following commands
```
conda create --yes -n cellxgene python=3.6
conda activate cellxgene
pip install cellxgene
```
Or you can create a virtual environment by using
```
ENV_NAME=cellxgene
python3 -m venv ${ENV_NAME}
source ${ENV_NAME}/bin/activate
pip install cellxgene
```
## FAQ
> Someone sent me a directory of `10X-Genomics` data with a `mtx` file and I've never used `scanpy`, can I use `cellxgene`?
Yep! This should only take a couple steps. We'll assume your data is in a folder called `data/` and you've successfully installed `cellxgene` with the `louvain` packages as described above. Just run
```
cellxgene prepare data/ --output=data-processed.h5ad --layout=umap
```
Depending on the size of the dataset, this may take some time. Once it's done, call
```
cellxgene launch data-processed.h5ad --layout=umap --open
```
And your web browser should open with an interactive view of your data.
> In my `prepare` command I received the following error `Warning: louvain module is not installed, no clusters will be calculated. To fix this please install cellxgene with the optional feature louvain enabled`
Louvain clustering requires additional dependencies that are somewhat complex, so we don't include them by default. For now, you need to specify that you want these packages by using
```
pip install cellxgene[louvain]
```
> I ran `prepare` and I'm getting results that look unexpected
You might want to try running one of the preprocessing recipes included with `scanpy` (read more about them [here](https://scanpy.readthedocs.io/en/latest/api/index.html#recipes)). You can specify this with the `--recipe` option, such as
```
cellxgene prepare data/ --output=data-processed.h5ad --recipe=zheng17
```
It should be easy to run `prepare` then call `cellxgene launch` a few times with different settings to explore different behaviors. We may explore adding other preprocessing options in the future.
> I have extra metadata that I want to add to my dataset
Currently this is not supported directly, but you should be able to do this manually using `scanpy`. For example, this [notebook](https://github.com/falexwolf/fun-analyses/blob/master/tabula_muris/tabula_muris.ipynb) shows adding the contents of a `csv` file with metadata to an `anndata` object. For now, you could do this manually on your data in the same way and then save out the result before loading into `cellxgene`.
> I tried to `pip install cellxgene` and got a weird error I don't understand
This may happen, especially as we work out bugs in our installation process! Please create a new [Github issue](https://github.com/chanzuckerberg/cellxgene/issues), explain what you did, and include all the error messages you saw. It'd also be super helpful if you call `pip freeze` and include the full output alongside your issue.
> How are you computing and sorting differential expression results?
Currently we use a [Welch's *t*-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) implementation including the same variance overestimation correction as used in `scanpy`. We sort the `tscore` to identify the top N genes, and then filter to remove any that fall below a cutoff log fold change value, which can help remove spurious test results. The default threshold is `0.01` and can be changed using the option `--diffexp-lfc-cutoff`. We can explore adding support for other test types in the future.
> I'm following the developer instructions and get an error about "missing files and directories” when trying to build the client
This is likely because you do not have node and npm installed, we recommend using [nvm](https://github.com/creationix/nvm) if you're new to using these tools.
## developer guide
This project has made a few key design choices
- The front-end is built with [`regl`](https://github.com/regl-project/regl) (a webgl library), [`react`](https://reactjs.org/), [`redux`](https://redux.js.org/), [`d3`](https://github.com/d3/d3), and [`blueprint`](https://blueprintjs.com/docs/#core) to handle rendering large numbers of cells with lots of complex interactivity
- The app is designed with a client-server model that can support a range of existing analysis packages for backend computational tasks (currently built for [scanpy](https://github.com/theislab/scanpy))
- The client uses fast cross-filtering to handle selections and comparisons across subsets of data
Depending on your background and interests, you might want to contribute to the frontend, or backend, or both!
If you are interested in working on `cellxgene` development, we recommend cloning the project from Gitub. First you'll need the following installed on your machine
- python 3.6
- node and npm (we recommend using [nvm](https://github.com/creationix/nvm) if this is your first time with node)
Then clone the project
```
git clone https://github.com/chanzuckerberg/cellxgene.git
```
Build the client web assets by calling this from inside the `cellxgene` folder
```
./bin/build-client
```
Install all requirements (we recommend doing this inside a virtual environment)
```
pip install -e .
```
You can start the app while developing either by calling `cellxgene` or by calling `python -m server`. We recommend using the `--debug` flag to see more output, which you can include when reporting bugs.
If you have any questions about developing or contributing, come hang out with us by joining the [CZI Science Slack](https://cziscience.slack.com/messages/CCTA8DF1T) and posting in the `#cellxgene-dev` channel.
## development roadmap
`cellxgene` is still very much in development, and we've love to include the community as we plan new features to work on. We are thinking about working on the following features over the next 3-12 months. If you are interested in updates, want to give feedback, want to contribute, or have ideas about other features we should work on, please [contact us](#help-and-contact)
- **Visualizaling spatial metadata** Image-based transcriptomics methods also generate large cell by gene matrices, alongside rich metadata about spatial location; we would like to render this information in `cellxgene`
- **Visualizing trajectories** Trajectory analyses infer progression along some ordering or pseudotime; we would like `cellxgene ` to render the results of these analyses when they have been performed
- **Deploy to web** Many projects release public data browser websites alongside their publicatons; we would like to make it easy for anyone to deploy `cellxgene` to a custom URL with their own dataset that they own and operate
- **HCA Integration** The [Human Cell Atlas](https://humancellatlas.org) is generating a large corpus of single-cell expression data and will make it available through the Data Coordination Platform; we would like `cellxgene` to be one of several different portals for browsing these data
## contributing
We warmly welcome contributions from the community! Please submit any bug reports and feature requests through [Github issues](https://github.com/chanzuckerberg/cellxgene/issues). Please submit any direct contributions by forking the repository, creating a branch, and submitting a Pull Request. It'd be great for PRs to include test cases and documentation updates where relevant, though we know the core test suite is itself still a work in progress. And all code contributions and dependencies must be compatible with the project's open-source license (MIT). If you have any questions about this stuff, just ask!
## inspiration and collaboration
We've been heavily inspired by several other related single-cell visualization projects, including the [UCSC Cell Browswer](http://cells.ucsc.edu/), [Cytoscape](http://www.cytoscape.org/), [Xena](https://xena.ucsc.edu/), [ASAP](https://asap.epfl.ch/), [Gene Pattern](http://genepattern-notebook.org/), and many others. We hope to explore collaborations where useful as this community works together on improving interactive visualization for single-cell data.
We were inspired by Mike Bostock and the [crossfilter](https://github.com/crossfilter) team for the design of our filtering implementation. We were inspired by Mike Bostock and the [crossfilter](https://github.com/crossfilter) team for the design of our filtering implementation.
We have been working closely with the [`scanpy`](https://github.com/theislab/scanpy) team to integrate with their awesome analysis tools. Special thanks to Alex Wolf, Fabian Theis, and the rest of the team for their help during development and for providing an example dataset. We have been working closely with the [scanpy](https://github.com/theislab/scanpy) team to integrate with their awesome analysis tools. Special thanks to Alex Wolf, Fabian Theis, and the rest of the team for their help during development and for providing an example dataset.
We are eager to explore integrations with other computational backends such as [`Seurat`](https://github.com/satijalab/seurat) or [`Bioconductor`](https://github.com/Bioconductor) We are eager to explore integrations with other computational backends such as [Seurat](https://github.com/satijalab/seurat) or [Bioconductor](https://github.com/Bioconductor)
## help and contact
Have questions, suggestions, or comments? You can come hang out with us by joining the [CZI Science Slack](https://cziscience.slack.com/messages/CCTA8DF1T) and posting in the `#cellxgene-users` channel. As mentioned above, please submit any feature requests or bugs as [Github issues](https://github.com/chanzuckerberg/cellxgene/issues). We'd love to hear from you!
## 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).
+3
View File
@@ -0,0 +1,3 @@
# 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).
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
CELLXGENE_DIR=$(dirname $DIR)
cd $CELLXGENE_DIR
npm install --prefix client/ client
npm run --prefix client build
rm -rf server/app/web/static
mkdir -p server/app/web/static/img
cp client/build/index.html server/app/web/templates/
cp -r client/build/static server/app/web/
cp client/build/favicon.png server/app/web/static/img
cp client/build/service-worker.js server/app/web/static/js/
-18
View File
@@ -1,18 +0,0 @@
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
CELLXGENE_DIR=$(dirname $DIR)
echo "Uninstalling cellxgene"
yes | pip uninstall cellxgene
echo "removing node_modules"
rm -rf $CELLXGENE_DIR/client/node_modules
echo "removing client_build"
rm -rf $CELLXGENE_DIR/client/build
echo "removing dist"
rm -rf $CELLXGENE_DIR/dist
echo "removing egg-info"
rm -rf $CELLXGENE_DIR/cellxgene.egg-info
echo "removing static files"
rm -f $CELLXGENE_DIR/server/app/web/templates/index.html
rm -rf $CELLXGENE_DIR/server/app/web/static
echo "cellxgene cleanup complete"
+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
+62
View File
@@ -0,0 +1,62 @@
include ../common.mk
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
clean:
rm -rf node_modules
rm -f __tests__/screenshots/*.png
.PHONY: ci
ci:
npm ci
.PHONY: install
install:
npm install
.PHONY: build
WEBPACK_CONFIG ?= configuration/webpack/webpack.config.prod.js
build:
npm run build $(WEBPACK_CONFIG)
# Development convenience methods
.PHONY: start-frontend
start-frontend:
node server/development.js
# start an instance of cellxgene and run the end-to-end tests
.PHONY: smoke-test
smoke-test:
start_server_and_test \
'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'
# start an instance of cellxgene and run the end-to-end annotations tests
.PHONY: smoke-test-annotations
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) --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)
.PHONY: unit-test
unit-test:
node node_modules/jest/bin/jest.js --testPathIgnorePatterns e2e
# pass remaining commands through to npm run
%:
npm run $(*)
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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 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
+445
View File
@@ -0,0 +1,445 @@
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
import { strict as assert } from "assert";
import {
clearInputAndTypeInto,
clickOn,
getAllByClass,
getOneElementInnerText,
typeInto,
waitByID,
waitByClass,
waitForAllByIds,
clickOnUntil,
getTestClass,
getTestId,
isElementPresent,
} from "./puppeteerUtils";
export async function drag(testId, start, end, lasso = false) {
const layout = await waitByID(testId);
const elBox = await layout.boxModel();
const x1 = elBox.content[0].x + start.x;
const x2 = elBox.content[0].x + end.x;
const y1 = elBox.content[0].y + start.y;
const y2 = elBox.content[0].y + end.y;
await page.mouse.move(x1, y1);
await page.mouse.down();
if (lasso) {
await page.mouse.move(x2, y1);
await page.mouse.move(x2, y2);
await page.mouse.move(x1, y2);
await page.mouse.move(x1, y1);
} else {
await page.mouse.move(x2, y2);
}
await page.mouse.up();
}
export async function clickOnCoordinate(testId, coord) {
const layout = await expect(page).toMatchElement(getTestId(testId));
const elBox = await layout.boxModel();
if (!elBox) {
throw Error("Layout's boxModel is not available!");
}
const x = elBox.content[0].x + coord.x;
const y = elBox.content[0].y + coord.y;
await page.mouse.click(x, y);
}
export async function getAllHistograms(testclass, testIds) {
const histTestIds = testIds.map((tid) => `histogram-${tid}`);
// these load asynchronously, so we need to wait for each histogram individually,
// and they may be quite slow in some cases.
await waitForAllByIds(histTestIds, { timeout: 4 * 60 * 1000 });
const allHistograms = await getAllByClass(testclass);
const testIDs = await Promise.all(
allHistograms.map((hist) =>
page.evaluate((elem) => elem.dataset.testid, hist)
)
);
return testIDs.map((id) => id.replace(/^histogram-/, ""));
}
export async function getAllCategoriesAndCounts(category) {
// these load asynchronously, so we have to wait for the specific category.
await waitByID(`category-${category}`);
return page.$$eval(
`[data-testid="category-${category}"] [data-testclass='categorical-row']`,
(rows) =>
Object.fromEntries(
rows.map((row) => {
const cat = row
.querySelector("[data-testclass='categorical-value']")
.getAttribute("aria-label");
const count = row.querySelector(
"[data-testclass='categorical-value-count']"
).innerText;
return [cat, count];
})
)
);
}
export async function getCellSetCount(num) {
await clickOn(`cellset-button-${num}`);
return getOneElementInnerText(`[data-testid='cellset-count-${num}']`);
}
export async function resetCategory(category) {
const checkboxId = `${category}:category-select`;
await waitByID(checkboxId);
const checkedPseudoclass = await page.$eval(
`[data-testid='${checkboxId}']`,
(el) => el.matches(":checked")
);
if (!checkedPseudoclass) await clickOn(checkboxId);
const categoryRow = await waitByID(`${category}:category-expand`);
const isExpanded = await categoryRow.$(
"[data-testclass='category-expand-is-expanded']"
);
if (isExpanded) await clickOn(`${category}:category-expand`);
}
export async function calcCoordinate(testId, xAsPercent, yAsPercent) {
const el = await waitByID(testId);
const size = await el.boxModel();
return {
x: Math.floor(size.width * xAsPercent),
y: Math.floor(size.height * yAsPercent),
};
}
export async function calcDragCoordinates(testId, coordinateAsPercent) {
return {
start: await calcCoordinate(
testId,
coordinateAsPercent.x1,
coordinateAsPercent.y1
),
end: await calcCoordinate(
testId,
coordinateAsPercent.x2,
coordinateAsPercent.y2
),
};
}
export async function selectCategory(category, values, reset = true) {
if (reset) await resetCategory(category);
await clickOn(`${category}:category-expand`);
await clickOn(`${category}:category-select`);
for (const value of values) {
await clickOn(`categorical-value-select-${category}-${value}`);
}
}
export async function expandCategory(category) {
const expand = await waitByID(`${category}:category-expand`);
const notExpanded = await expand.$(
"[data-testclass='category-expand-is-not-expanded']"
);
if (notExpanded) await clickOn(`${category}:category-expand`);
}
export async function clip(min = 0, max = 100) {
await clickOn("visualization-settings");
await clearInputAndTypeInto("clip-min-input", min);
await clearInputAndTypeInto("clip-max-input", max);
await clickOn("clip-commit");
}
export async function createCategory(categoryName) {
await clickOnUntil("open-annotation-dialog", async () => {
await expect(page).toMatchElement(getTestId("new-category-name"));
});
await typeInto("new-category-name", 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");
await typeInto("new-category-name", categoryName);
const dropdownOptionClass = "duplicate-category-dropdown-option";
await clickOnUntil("duplicate-category-dropdown", async () => {
await expect(page).toMatchElement(getTestClass(dropdownOptionClass));
});
const option = await expect(page).toMatchElement(
getTestClass(dropdownOptionClass)
);
await option.click();
await clickOnUntil("submit-category", async () => {
await expect(page).toMatchElement(
getTestId(`${categoryName}:category-expand`)
);
});
await waitByClass("autosave-complete");
}
export async function renameCategory(oldCategoryName, newCategoryName) {
await clickOn(`${oldCategoryName}:see-actions`);
await clickOn(`${oldCategoryName}:edit-category-mode`);
await clearInputAndTypeInto(
`${oldCategoryName}:edit-category-name-text`,
newCategoryName
);
await clickOn(`${oldCategoryName}:submit-category-edit`);
}
export async function deleteCategory(categoryName) {
const targetId = `${categoryName}:delete-category`;
await clickOnUntil(`${categoryName}:see-actions`, async () => {
await expect(page).toMatchElement(getTestId(targetId));
});
await clickOn(targetId);
await assertCategoryDoesNotExist();
}
export async function createLabel(categoryName, labelName) {
/**
* (thuang): This explicit wait is needed, since currently showing
* the modal again quickly after the previous action dismissing the
* modal will persist the input value from the previous action.
*
* To reproduce:
* 1. Click on the plus sign to show the modal to add a new label to the category
* 2. Type `123` in the input box
* 3. Hover over your mouse over the plus sign and double click to quickly dismiss and
* invoke the modal again
* 4. You will see `123` is persisted in the input box
* 5. Expected behavior is to get an empty input box
*/
await page.waitForTimeout(500);
await clickOn(`${categoryName}:see-actions`);
await clickOn(`${categoryName}:add-new-label-to-category`);
await typeInto(`${categoryName}:new-label-name`, labelName);
await clickOn(`${categoryName}:submit-label`);
}
export async function deleteLabel(categoryName, labelName) {
await expandCategory(categoryName);
await clickOn(`${categoryName}:${labelName}:see-actions`);
await clickOn(`${categoryName}:${labelName}:delete-label`);
}
export async function renameLabel(categoryName, oldLabelName, newLabelName) {
await expandCategory(categoryName);
await clickOn(`${categoryName}:${oldLabelName}:see-actions`);
await clickOn(`${categoryName}:${oldLabelName}:edit-label`);
await clearInputAndTypeInto(
`${categoryName}:${oldLabelName}:edit-label-name`,
newLabelName
);
await clickOn(`${categoryName}:${oldLabelName}:submit-label-edit`);
}
export async function addGeneToSearch(geneName) {
await typeInto("gene-search", geneName);
await page.keyboard.press("Enter");
await page.waitForSelector(`[data-testid='histogram-${geneName}']`);
}
export async function subset(coordinatesAsPercent) {
// In order to deselect the selection after the subset, make sure we have some clear part
// of the scatterplot we can click on
assert(coordinatesAsPercent.x2 < 0.99 || coordinatesAsPercent.y2 < 0.99);
const lassoSelection = await calcDragCoordinates(
"layout-graph",
coordinatesAsPercent
);
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
await clickOn("subset-button");
const clearCoordinate = await calcCoordinate("layout-graph", 0.5, 0.99);
await clickOnCoordinate("layout-graph", clearCoordinate);
}
export async function setSellSet(cellSet, cellSetNum) {
const selections = cellSet.filter((sel) => sel.kind === "categorical");
for (const selection of selections) {
await selectCategory(selection.metadata, selection.values, true);
}
await getCellSetCount(cellSetNum);
}
export async function runDiffExp(cellSet1, cellSet2) {
await setSellSet(cellSet1, 1);
await setSellSet(cellSet2, 2);
await clickOn("diffexp-button");
}
export async function bulkAddGenes(geneNames) {
await clickOn("section-bulk-add");
await typeInto("input-bulk-add", geneNames.join(","));
await page.keyboard.press("Enter");
}
export async function assertCategoryDoesNotExist(categoryName) {
const result = await isElementPresent(
getTestId(`${categoryName}:category-label`)
);
await expect(result).toBe(false);
}
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
+10
View File
@@ -0,0 +1,10 @@
import * as ENV_DEFAULT from "../../../environment.default.json";
export const jestEnv = process.env.JEST_ENV || ENV_DEFAULT.JEST_ENV;
export const appUrlBase =
process.env.CXG_URL_BASE || `http://localhost:${ENV_DEFAULT.CXG_CLIENT_PORT}`;
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 ?? "";
+147
View File
@@ -0,0 +1,147 @@
export const datasets = {
pbmc3k: {
title: "pbmc3k",
dataframe: {
nObs: "2638",
nVar: "1838",
type: "float32",
},
categorical: {
louvain: {
"B cells": "342",
"CD14+ Monocytes": "480",
"CD4 T cells": "1144",
"CD8 T cells": "316",
"Dendritic cells": "37",
"FCGR3A+ Monocytes": "150",
Megakaryocytes: "15",
"NK cells": "154",
},
},
continuous: {
n_genes: "int32",
percent_mito: "float32",
n_counts: "float32",
},
cellsets: {
lasso: [
{
"coordinates-as-percent": { x1: 0.1, y1: 0.25, x2: 0.7, y2: 0.75 },
count: "1131",
},
],
categorical: [
{
metadata: "louvain",
values: ["B cells", "Megakaryocytes"],
count: "357",
},
],
continuous: [
{
metadata: "n_genes",
"coordinates-as-percent": { x1: 0.25, y1: 0.5, x2: 0.55, y2: 0.5 },
count: "1537",
},
],
},
diffexp: {
cellset1: [
{ kind: "categorical", metadata: "louvain", values: ["B cells"] },
],
cellset2: [
{
kind: "categorical",
metadata: "louvain",
values: ["CD4 T cells", "NK cells"],
},
],
"gene-results": [
"HLA-DRB1",
"HLA-DPB1",
"CD79A",
"HLA-DPA1",
"HLA-DQA1",
"CD79B",
"HLA-DQB1",
"MS4A1",
"IL32",
"CD37",
],
},
genes: {
bulkadd: ["S100A8", "FCGR3A", "LGALS2", "GSTP1"],
search: "ACD",
},
subset: {
cellset1: [
{
kind: "categorical",
metadata: "louvain",
values: ["B cells", "Megakaryocytes"],
},
],
count: "357",
categorical: {
louvain: {
"B cells": "342",
"CD14+ Monocytes": "0",
"CD4 T cells": "0",
"CD8 T cells": "0",
"Dendritic cells": "0",
"FCGR3A+ Monocytes": "0",
Megakaryocytes: "15",
"NK cells": "0",
},
},
lasso: {
"coordinates-as-percent": { x1: 0.25, y1: 0.05, x2: 0.75, y2: 0.55 },
count: "331",
},
},
scatter: {
genes: { x: "S100A8", y: "FCGR3A" },
},
pan: {
"coordinates-as-percent": { x1: 0.75, y1: 0.75, x2: 0.35, y2: 0.35 },
},
features: {
panzoom: {
lasso: {
"coordinates-as-percent": { x1: 0.3, y1: 0.3, x2: 0.5, y2: 0.5 },
count: "38",
},
},
},
categoryLabel: {
lasso: {
"coordinates-as-percent": { x1: 0.05, y1: 0.3, x2: 0.5, y2: 0.5 },
},
newCount: {
bySubsetConfig: {
false: "668",
true: "659",
},
},
},
annotationsFromFile: {
count: {
bySubsetConfig: {
false: "1161",
true: "852",
},
},
},
clip: {
min: "30",
max: "70",
metadata: "n_genes",
gene: "S100A8",
"coordinates-as-percent": { x1: 0.25, y1: 0.5, x2: 0.55, y2: 0.5 },
count: "386",
"gene-cell-count": "416",
},
},
};
+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",
];
+351
View File
@@ -0,0 +1,351 @@
/**
* Smoke test suite that will be run in Travis CI
* Tests included in this file are expected to be relatively stable and test core features
*/
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
import { appUrlBase, DATASET } from "./config";
import { datasets } from "./data";
import {
clickOn,
getAllByClass,
getElementCoordinates,
getOneElementInnerHTML,
getTestId,
goToPage,
waitByID,
} from "./puppeteerUtils";
import {
calcDragCoordinates,
clip,
drag,
getAllCategoriesAndCounts,
getCellSetCount,
selectCategory,
} from "./cellxgeneActions";
const data = datasets[DATASET];
describe("did launch", () => {
test("page launched", async () => {
await goToPage(appUrlBase);
const element = await getOneElementInnerHTML(getTestId("header"));
expect(element).toMatchSnapshot();
});
});
describe("metadata loads", () => {
test("categories and values from dataset appear", async () => {
await goToPage(appUrlBase);
for (const label of Object.keys(data.categorical)) {
const element = await getOneElementInnerHTML(
getTestId(`category-${label}`)
);
expect(element).toMatchSnapshot();
await clickOn(`${label}:category-expand`);
const categories = await getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.categorical[label])
);
}
});
test("continuous data appears", async () => {
await goToPage(appUrlBase);
for (const label of Object.keys(data.continuous)) {
await waitByID(`histogram-${label}`);
}
});
});
describe("cell selection", () => {
test("selects all cells cellset 1", async () => {
await goToPage(appUrlBase);
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(data.dataframe.nObs);
});
test("selects all cells cellset 2", async () => {
await goToPage(appUrlBase);
const cellCount = await getCellSetCount(2);
expect(cellCount).toBe(data.dataframe.nObs);
});
test("selects cells via lasso", async () => {
await goToPage(appUrlBase);
for (const cellset of data.cellsets.lasso) {
const cellset1 = await calcDragCoordinates(
"layout-graph",
cellset["coordinates-as-percent"]
);
await drag("layout-graph", cellset1.start, cellset1.end, true);
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(cellset.count);
}
});
test("selects cells via categorical", async () => {
await goToPage(appUrlBase);
for (const cellset of data.cellsets.categorical) {
await clickOn(`${cellset.metadata}:category-expand`);
await clickOn(`${cellset.metadata}:category-select`);
for (const value of cellset.values) {
await clickOn(`categorical-value-select-${cellset.metadata}-${value}`);
}
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(cellset.count);
}
});
test("selects cells via continuous", async () => {
await goToPage(appUrlBase);
for (const cellset of data.cellsets.continuous) {
const histBrushableAreaId = `histogram-${cellset.metadata}-plot-brushable-area`;
const coords = await calcDragCoordinates(
histBrushableAreaId,
cellset["coordinates-as-percent"]
);
await drag(histBrushableAreaId, coords.start, coords.end);
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(cellset.count);
}
});
});
describe("subset", () => {
test("subset - cell count matches", async () => {
await goToPage(appUrlBase);
for (const select of data.subset.cellset1) {
if (select.kind === "categorical") {
await selectCategory(select.metadata, select.values, true);
}
}
await clickOn("subset-button");
for (const label of Object.keys(data.subset.categorical)) {
const categories = await getAllCategoriesAndCounts(label);
expect(Object.keys(categories)).toMatchObject(
Object.keys(data.subset.categorical[label])
);
expect(Object.values(categories)).toMatchObject(
Object.values(data.subset.categorical[label])
);
}
});
test("lasso after subset", async () => {
await goToPage(appUrlBase);
for (const select of data.subset.cellset1) {
if (select.kind === "categorical") {
await selectCategory(select.metadata, select.values, true);
}
}
await clickOn("subset-button");
const lassoSelection = await calcDragCoordinates(
"layout-graph",
data.subset.lasso["coordinates-as-percent"]
);
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
const cellCount = await getCellSetCount(1);
expect(cellCount).toBe(data.subset.lasso.count);
});
});
describe("clipping", () => {
test("clip continuous", async () => {
await goToPage(appUrlBase);
await clip(data.clip.min, data.clip.max);
const histBrushableAreaId = `histogram-${data.clip.metadata}-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.count);
});
});
// interact with UI elements just that they do not break
describe("ui elements don't error", () => {
test("color by", async () => {
await goToPage(appUrlBase);
const allLabels = [
...Object.keys(data.categorical),
...Object.keys(data.continuous),
];
for (const label of allLabels) {
await clickOn(`colorby-${label}`);
}
});
test("pan and zoom", async () => {
await goToPage(appUrlBase);
await clickOn("mode-pan-zoom");
const panCoords = await calcDragCoordinates(
"layout-graph",
data.pan["coordinates-as-percent"]
);
await drag("layout-graph", panCoords.start, panCoords.end, false);
await page.evaluate("window.scrollBy(0, 1000);");
});
});
describe("centroid labels", () => {
test("labels are created", async () => {
await goToPage(appUrlBase);
const labels = Object.keys(data.categorical);
await clickOn(`colorby-${labels[0]}`);
await clickOn("centroid-label-toggle");
// Toggle colorby for each category and check to see if labels are generated
for (let i = 0, { length } = labels; i < length; i += 1) {
const label = labels[i];
// first label is already enabled
if (i !== 0) await clickOn(`colorby-${label}`);
const generatedLabels = await getAllByClass("centroid-label");
// Number of labels generated should be equal to size of the object
expect(generatedLabels).toHaveLength(
Object.keys(data.categorical[label]).length
);
}
});
});
describe("graph overlay", () => {
test("transform centroids correctly", async () => {
await goToPage(appUrlBase);
const category = Object.keys(data.categorical)[0];
await clickOn(`colorby-${category}`);
await clickOn("centroid-label-toggle");
await clickOn("mode-pan-zoom");
const panCoords = await calcDragCoordinates(
"layout-graph",
data.pan["coordinates-as-percent"]
);
const categoryValue = Object.keys(data.categorical[category])[0];
const initialCoordinates = await getElementCoordinates(
`${categoryValue}-centroid-label`
);
await drag("layout-graph", panCoords.start, panCoords.end, false);
const terminalCoordinates = await getElementCoordinates(
`${categoryValue}-centroid-label`
);
expect(terminalCoordinates[0] - initialCoordinates[0]).toBeCloseTo(
panCoords.end.x - panCoords.start.x
);
expect(terminalCoordinates[1] - initialCoordinates[1]).toBeCloseTo(
panCoords.end.y - panCoords.start.y
);
});
});
test("pan zoom mode resets lasso selection", async () => {
await goToPage(appUrlBase);
const panzoomLasso = data.features.panzoom.lasso;
const lassoSelection = await calcDragCoordinates(
"layout-graph",
panzoomLasso["coordinates-as-percent"]
);
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
await waitByID("lasso-element", { visible: true });
const initialCount = await getCellSetCount(1);
expect(initialCount).toBe(panzoomLasso.count);
await clickOn("mode-pan-zoom");
await clickOn("mode-lasso");
const modeSwitchCount = await getCellSetCount(1);
expect(modeSwitchCount).toBe(initialCount);
});
test("lasso moves after pan", async () => {
await goToPage(appUrlBase);
const panzoomLasso = data.features.panzoom.lasso;
const coordinatesAsPercent = panzoomLasso["coordinates-as-percent"];
const lassoSelection = await calcDragCoordinates(
"layout-graph",
coordinatesAsPercent
);
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
await waitByID("lasso-element", { visible: true });
const initialCount = await getCellSetCount(1);
expect(initialCount).toBe(panzoomLasso.count);
await clickOn("mode-pan-zoom");
const panCoords = await calcDragCoordinates(
"layout-graph",
coordinatesAsPercent
);
await drag("layout-graph", panCoords.start, panCoords.end, false);
await clickOn("mode-lasso");
const panCount = await getCellSetCount(2);
expect(panCount).toBe(initialCount);
});
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
+595
View File
@@ -0,0 +1,595 @@
/*
Tests included in this file are specific to annotation features
*/
import { appUrlBase, DATASET } from "./config";
import { datasets } from "./data";
import {
clickOn,
goToPage,
waitByClass,
waitByID,
getTestId,
getTestClass,
getAllByClass,
clickOnUntil,
getOneElementInnerHTML,
} from "./puppeteerUtils";
import {
assertCategoryDoesNotExist,
calcDragCoordinates,
createCategory,
createLabel,
deleteCategory,
deleteLabel,
drag,
expandCategory,
renameCategory,
renameLabel,
subset,
duplicateCategory,
createGeneset,
deleteGeneset,
assertGenesetExists,
assertGenesetDoesNotExist,
getCellSetCount,
expandGeneset,
editGenesetName,
addGeneToSet,
assertGeneExistsInGeneset,
removeGene,
assertGeneDoesNotExist,
expandGene,
colorByGeneset,
assertColorLegendLabel,
colorByGene,
} from "./cellxgeneActions";
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);
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 });
}
await waitByClass("autosave-complete");
}
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);
const categoryName = `category-created-${config.tag}`;
await assertCategoryDoesNotExist(categoryName);
await createCategory(categoryName);
await assertCategoryExists(categoryName);
});
test("delete a category", async () => {
await setup(config);
await deleteCategory(perTestCategoryName);
await assertCategoryDoesNotExist(perTestCategoryName);
});
test("rename a category", async () => {
await setup(config);
const newCategoryName = `NEW-${config.tag}`;
await renameCategory(perTestCategoryName, newCategoryName);
await assertCategoryDoesNotExist(perTestCategoryName);
await assertCategoryExists(newCategoryName);
});
test("create a label", async () => {
await setup(config);
const labelName = `new-label-${config.tag}`;
await assertLabelDoesNotExist(perTestCategoryName, labelName);
await createLabel(perTestCategoryName, labelName);
await assertLabelExists(perTestCategoryName, labelName);
});
test("delete a label", async () => {
await setup(config);
await deleteLabel(perTestCategoryName, perTestLabelName);
await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName);
});
test("rename a label", async () => {
await setup(config);
const newLabelName = "my-cool-new-label";
await assertLabelDoesNotExist(perTestCategoryName, newLabelName);
await renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName);
await assertLabelExists(perTestCategoryName, newLabelName);
});
test("check cell count for a label loaded from file", async () => {
await setup(config);
const duplicateCategoryName = "duplicate";
await duplicateCategory(duplicateCategoryName);
await page.reload({ waitUntil: ["networkidle0", "domcontentloaded"] });
const firstCategoryExpandIcon = await expect(page).toMatchElement(
getTestClass("category-expand")
);
await firstCategoryExpandIcon.click();
const expectedCategoryRow = await expect(page).toMatchElement(
getTestClass("categorical-row")
);
const expectedLabelName = await getInnerText(
expectedCategoryRow,
"categorical-value"
);
const expectedLabelCount = await getInnerText(
expectedCategoryRow,
"categorical-value-count"
);
await expandCategory(duplicateCategoryName);
const expectedCategory = await expect(page).toMatchElement(
getTestClass("category")
);
const actualCategoryRow = await expect(expectedCategory).toMatchElement(
getTestClass("categorical-row")
);
const actualLabelName = await getInnerText(
actualCategoryRow,
"categorical-value"
);
const actualLabelCount = await getInnerText(
actualCategoryRow,
"categorical-value-count"
);
expect(actualLabelName).toBe(expectedLabelName);
expect(actualLabelCount).toBe(expectedLabelCount);
async function getInnerText(element, className) {
return element.$eval(getTestClass(className), (node) => node?.innerText);
}
});
test("assign cells to a label", async () => {
await setup(config);
await expandCategory(perTestCategoryName);
const lassoSelection = await calcDragCoordinates(
"layout-graph",
data.categoryLabel.lasso["coordinates-as-percent"]
);
await drag("layout-graph", lassoSelection.start, lassoSelection.end, true);
await waitByID("lasso-element", { visible: true });
await clickOn(`${perTestCategoryName}:${perTestLabelName}:see-actions`);
await clickOn(
`${perTestCategoryName}:${perTestLabelName}:add-current-selection-to-this-label`
);
const result = await waitByID(
`categorical-value-count-${perTestCategoryName}-${perTestLabelName}`
);
expect(await result.evaluate((node) => node.innerText)).toBe(
data.categoryLabel.newCount.bySubsetConfig[config.withSubset]
);
});
test("undo/redo category creation", async () => {
await setup(config);
const categoryName = `category-created-undo-${config.tag}`;
await assertCategoryDoesNotExist(categoryName);
await createCategory(categoryName);
await assertCategoryExists(categoryName);
await clickOn("undo");
await assertCategoryDoesNotExist(categoryName);
await clickOn("redo");
await assertCategoryExists(categoryName);
});
test("undo/redo category deletion", async () => {
await setup(config);
const categoryName = `category-deleted-undo-${config.tag}`;
await createCategory(categoryName);
await assertCategoryExists(categoryName);
await deleteCategory(categoryName);
await assertCategoryDoesNotExist(categoryName);
await clickOn("undo");
await assertCategoryExists(categoryName);
await clickOn("redo");
await assertCategoryDoesNotExist(categoryName);
});
test("undo/redo category rename", async () => {
await setup(config);
const newCategoryName = `category-renamed-undo-${config.tag}`;
await assertCategoryDoesNotExist(newCategoryName);
await renameCategory(perTestCategoryName, newCategoryName);
await assertCategoryExists(newCategoryName);
await assertCategoryDoesNotExist(perTestCategoryName);
await clickOn("undo");
await assertCategoryExists(perTestCategoryName);
await assertCategoryDoesNotExist(newCategoryName);
await clickOn("redo");
await assertCategoryExists(newCategoryName);
await assertCategoryDoesNotExist(perTestCategoryName);
});
test("undo/redo label creation", async () => {
await setup(config);
const labelName = `label-created-undo-${config.tag}`;
await assertLabelDoesNotExist(perTestCategoryName, labelName);
await createLabel(perTestCategoryName, labelName);
await assertLabelExists(perTestCategoryName, labelName);
await clickOn("undo");
await assertLabelDoesNotExist(perTestCategoryName);
await clickOn("redo");
await assertLabelExists(perTestCategoryName, labelName);
});
test("undo/redo label deletion", async () => {
await setup(config);
await deleteLabel(perTestCategoryName, perTestLabelName);
await assertLabelDoesNotExist(perTestCategoryName);
await clickOn("undo");
await assertLabelExists(perTestCategoryName, perTestLabelName);
await clickOn("redo");
await assertLabelDoesNotExist(perTestCategoryName);
});
test("undo/redo label rename", async () => {
await setup(config);
const newLabelName = `label-renamed-undo-${config.tag}`;
await assertLabelDoesNotExist(perTestCategoryName, newLabelName);
await renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
await assertLabelExists(perTestCategoryName, newLabelName);
await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName);
await clickOn("undo");
await assertLabelExists(perTestCategoryName, perTestLabelName);
await assertLabelDoesNotExist(perTestCategoryName, newLabelName);
await clickOn("redo");
await assertLabelExists(perTestCategoryName, newLabelName);
await assertLabelDoesNotExist(perTestCategoryName, perTestLabelName);
});
test("stacked bar graph renders", async () => {
await setup(config);
await expandCategory(perTestCategoryName);
await clickOn(`colorby-louvain`);
const labels = await getAllByClass("categorical-row");
const result = await Promise.all(
labels.map((label) => page.evaluate((element) => element.outerHTML, label))
);
expect(result).toMatchSnapshot();
});
test("truncate midpoint whitespace", async () => {
await setup(config);
const newLabelName = "123 456";
await renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
const value = await waitByID(
`categorical-value-${perTestCategoryName}-${newLabelName}`
);
const result = await page.evaluate((elem) => elem.outerHTML, value);
expect(result).toMatchSnapshot();
});
test("truncate single character", async () => {
await setup(config);
const newLabelName = "T";
await renameLabel(perTestCategoryName, perTestLabelName, newLabelName);
const value = await waitByID(
`categorical-value-${perTestCategoryName}-${newLabelName}`
);
const result = await page.evaluate((elem) => elem.outerHTML, value);
expect(result).toMatchSnapshot();
});
async function assertCategoryExists(categoryName) {
const handle = await waitByID(`${categoryName}:category-label`);
const result = await handle.evaluate((node) =>
node.getAttribute("aria-label")
);
return expect(result).toBe(categoryName);
}
async function assertLabelExists(categoryName, labelName) {
await expect(page).toMatchElement(
getTestId(`${categoryName}:category-expand`)
);
await expandCategory(categoryName);
const previous = await waitByID(
`categorical-value-${categoryName}-${labelName}`
);
expect(
await previous.evaluate((node) => node.getAttribute("aria-label"))
).toBe(labelName);
}
async function assertLabelDoesNotExist(categoryName, labelName) {
await expandCategory(categoryName);
const result = await page.$(
`[data-testid='categorical-value-${categoryName}-${labelName}']`
);
expect(result).toBeNull();
}
});
+10
View File
@@ -0,0 +1,10 @@
{
"testRunner": "jest-circus/runner",
"preset": "jest-puppeteer",
"testMatch": ["**/__tests__/**/?(*.)(spec|test).js?(x)"],
"setupFiles": ["../setupMissingGlobals.js"],
"setupFilesAfterEnv": ["expect-puppeteer", "./puppeteer.setup.js"],
"globalSetup": "../globalSetup.js",
"globalTeardown": "jest-environment-puppeteer/teardown",
"testEnvironment": "./screenshot_env.js"
}
+58
View File
@@ -0,0 +1,58 @@
/**
* `client/jest-puppeteer.config.js` is for configuring Puppeteer's launch config options
* `client/__tests__/e2e/puppeteer.setup.js` is for configuring `jest`, `browser`,
* and `page` objects
*/
import { setDefaultOptions } from "expect-puppeteer";
import { isDebug, isDev } from "./config";
import * as ENV_DEFAULT from "../../../environment.default.json";
// (thuang): This is the max time a test can take to run.
// Since when debugging, we run slowMo and !headless, this means
// 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: 60 * 1000 });
jest.retryTimes(ENV_DEFAULT.RETRY_ATTEMPTS);
beforeEach(async () => {
await jestPuppeteer.resetBrowser();
const userAgent = await browser.userAgent();
await page.setUserAgent(`${userAgent}bot`);
await page._client.send("Animation.setPlaybackRate", { playbackRate: 12 });
page.on("pageerror", (err) => {
throw new Error(`Console error: ${err}`);
});
page.on("error", (err) => {
throw new Error(`Console error: ${err}`);
});
page.on("console", async (msg) => {
if (isDev || isDebug) {
// If there is a console.error but an error is not thrown, this will ensure the test fails
console.log(`PAGE LOG: ${msg.text()}`);
if (msg.type() === "error") {
// TODO: chromium does not currently support the CSP directive on the
// line below, so we swallow this error. Remove this when the test
// suite uses a browser version that supports this directive.
if (
msg.text() ===
"Unrecognized Content-Security-Policy directive 'require-trusted-types-for'.\n"
) {
return;
}
const errorMsgText = await Promise.all(
// TODO can we do this without internal properties?
msg.args().map((arg) => arg._remoteObject.description)
);
throw new Error(`Console error: ${errorMsgText}`);
}
}
});
});
+124
View File
@@ -0,0 +1,124 @@
/* eslint-disable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
export function getTestId(id) {
return `[data-testid='${id}']`;
}
export function getTestClass(className) {
return `[data-testclass='${className}']`;
}
export async function waitByID(testId, props = {}) {
return page.waitForSelector(getTestId(testId), props);
}
export async function waitByClass(testClass, props = {}) {
return page.waitForSelector(`[data-testclass='${testClass}']`, props);
}
export async function waitForAllByIds(testIds) {
await Promise.all(
testIds.map((testId) => page.waitForSelector(getTestId(testId)))
);
}
export async function getAllByClass(testClass) {
return page.$$(`[data-testclass=${testClass}]`);
}
export async function typeInto(testId, text) {
// blueprint's typeahead is treating typing weird, clicking & waiting first solves this
// only works for text without special characters
await waitByID(testId);
const selector = getTestId(testId);
// type ahead can be annoying if you don't pause before you type
await page.click(selector);
await page.waitForTimeout(200);
await page.type(selector, text);
}
export async function clearInputAndTypeInto(testId, text) {
await waitByID(testId);
const selector = getTestId(testId);
// 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.waitForTimeout(200);
// select all
await page.click(selector, { clickCount: 3 });
await page.keyboard.press("Backspace");
await page.type(selector, text);
}
export async function clickOn(testId, options = {}) {
await expect(page).toClick(getTestId(testId), options);
}
/**
* (thuang): There are times when Puppeteer clicks on a button and the page doesn't respond.
* So I added clickOnUntil() to retry clicking until a given condition is met.
*/
export async function clickOnUntil(testId, assert) {
const MAX_RETRY = 10;
const WAIT_FOR_MS = 200;
let retry = 0;
while (retry < MAX_RETRY) {
try {
await clickOn(testId);
await assert();
break;
} catch (error) {
retry += 1;
await page.waitForTimeout(WAIT_FOR_MS);
}
}
if (retry === MAX_RETRY) {
throw Error("clickOnUntil() assertion failed!");
}
}
export async function getOneElementInnerHTML(selector, options = {}) {
await page.waitForSelector(selector, options);
return page.$eval(selector, (el) => el.innerHTML);
}
export async function getOneElementInnerText(selector) {
expect(page).toMatchElement(selector);
return page.$eval(selector, (el) => el.innerText);
}
export async function getElementCoordinates(testId) {
return page.$eval(getTestId(testId), (elem) => {
const { left, top } = elem.getBoundingClientRect();
return [left, top];
});
}
async function nameNewAnnotation() {
if (await isElementPresent(getTestId("annotation-dialog"))) {
await typeInto("new-annotation-name", "ignoreE2E");
await clickOn("submit-annotation");
// wait for the page to load
await waitByClass("autosave-complete");
}
}
export async function goToPage(url) {
await page.goto(url, {
waitUntil: "networkidle0",
});
await nameNewAnnotation();
}
export async function isElementPresent(selector, options) {
return Boolean(await page.$(selector, options));
}
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
+36
View File
@@ -0,0 +1,36 @@
const PuppeteerEnvironment = require("jest-environment-puppeteer");
require("jest-circus");
const ENV_DEFAULT = require("../../../environment.default.json");
const takeScreenshot = require("./takeScreenshot");
class ScreenshotEnvironment extends PuppeteerEnvironment {
async handleTestEvent(event, state) {
if (["test_start", "test_done"].includes(event.name)) {
console.log("------------------event name:\n", event.name);
console.log("~~~~ Current test errors\n", new Date(), event.test.errors);
console.log("~~~~ Current test\n", new Date(), event.test);
}
if (event.name === "error") {
console.log("error event:", JSON.stringify(event));
}
if (event.name === "test_fn_failure" || event.name === "hook_failure") {
console.log("------------------event name:\n", event.name);
console.log(">>>> Current state\n", new Date(), state);
console.log("===> Failure event\n", new Date(), event);
// (thuang): We only want to take screenshot on the last try
if (
state.currentlyRunningTest.invocations <= ENV_DEFAULT.RETRY_ATTEMPTS
) {
return;
}
await takeScreenshot(state.currentlyRunningTest.name, this.global.page);
}
}
}
module.exports = ScreenshotEnvironment;
+17
View File
@@ -0,0 +1,17 @@
function toFilename(name) {
return name.replace(/[^a-z0-9.-]+/gi, "-");
}
async function takeScreenshot(currentTestName, page) {
const testName = toFilename(currentTestName);
// Take a screenshot at the point of failure
const date = new Date().toISOString();
const screenshotName = `${date}-${testName}.png`;
await page.screenshot({
path: `./__tests__/screenshots/ignoreE2E-screenshot-${screenshotName}`,
});
}
module.exports = takeScreenshot;
+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();
};
+48
View File
@@ -0,0 +1,48 @@
import cascadeReducers from "../../src/reducers/cascade";
describe("create", () => {
test("from Array", () => {
expect(cascadeReducers([["foo", () => 0]])).toBeInstanceOf(Function);
});
test("from Map", () => {
expect(cascadeReducers(new Map([["foo", () => 0]]))).toBeInstanceOf(
Function
);
});
});
describe("cascade", () => {
test("expected arguments provided & cascade ordering", () => {
const topLevelState = {};
const topLevelAction = { type: "test" };
const reducer = cascadeReducers([
[
"foo",
(currentState, action, nextSharedState, prevSharedState) => {
expect(currentState).toBeUndefined();
expect(action).toEqual(topLevelAction);
expect(nextSharedState).toStrictEqual({});
expect(prevSharedState).toBe(topLevelState);
return 0;
},
],
[
"bar",
(currentState, action, nextSharedState, prevSharedState) => {
expect(currentState).toBeUndefined();
expect(action).toEqual(topLevelAction);
expect(nextSharedState).toStrictEqual({ foo: 0 });
expect(prevSharedState).toBe(topLevelState);
return 99;
},
],
]);
const nextState = reducer(topLevelState, topLevelAction);
expect(nextState).toStrictEqual({ foo: 0, bar: 99 });
expect(topLevelState).toStrictEqual({});
expect(topLevelAction).toStrictEqual({ type: "test" });
});
});
+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);
});
});
@@ -0,0 +1,83 @@
import undoable from "../../src/reducers/undoable";
describe("create", () => {
test("no keys", () => {
expect(() => undoable(() => {})).toThrow();
expect(() => undoable(() => {}, null)).toThrow();
expect(() => undoable(() => {}, [])).toThrow();
expect(() => undoable(() => {}, [], {})).toThrow();
});
test("simple", () => {
expect(undoable(() => {}, ["foo"])).toBeInstanceOf(Function);
expect(undoable(() => {}, ["foo"], {})).toBeInstanceOf(Function);
});
test("handles undefined initial state", () => {
expect(
undoable(() => {}, ["a"])(undefined, { type: "test" })
).toMatchObject({});
});
});
describe("undo", () => {
test("expected state modifications", () => {
const initialState = { a: 0, b: 1000 };
const reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
const undoableReducer = undoable(reducer, ["a"]);
const s1 = undoableReducer(initialState, { type: "test" });
expect(s1).toMatchObject({ a: 1, b: 1001 });
// test that only specified keys are undone
const s2 = undoableReducer(s1, { type: "@@undoable/undo" });
expect(s2).toMatchObject({ a: 0, b: 1001 });
// test backstop when no more history
const s3 = undoableReducer(s2, { type: "@@undoable/undo" });
expect(s3).toMatchObject({ a: 0, b: 1001 });
});
});
describe("redo", () => {
const initialState = { a: 0, b: 1000 };
const reducer = (state) => ({ a: state.a + 1, b: state.b + 1 });
let UR;
beforeEach(() => {
UR = undoable(reducer, ["a"]);
});
test("expected state modifications", () => {
const s1 = UR(initialState, { type: "test" });
expect(s1).toMatchObject({ a: 1, b: 1001 });
// verify undo->redo reverts state.
const s2 = UR(UR(s1, { type: "@@undoable/undo" }), {
type: "@@undoable/redo",
});
expect(s2).toMatchObject({ a: 1, b: 1001 });
// verify backstop when no redo future
const s3 = UR(s2, { type: "@@undoable/redo" });
expect(s3).toMatchObject({ a: 1, b: 1001 });
});
test("history cleared", () => {
// verify future cleared upon a normal state transition
const s1 = UR(initialState, { type: "test" });
expect(s1).toMatchObject({ a: 1, b: 1001 });
const s2 = UR(s1, { type: "@@undoable/undo" });
expect(s2).toMatchObject({ a: 0, b: 1001 });
const s3 = UR(s2, { type: "test" });
expect(s3).toMatchObject({ a: 1, b: 1002 });
const s4 = UR(s3, { type: "@@undoable/redo" });
expect(s4).toMatchObject({ a: 1, b: 1002 });
});
});
/*
TODO:
- historyLimit is enforced
- action filters
*/
+4
View File
@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
+9
View File
@@ -0,0 +1,9 @@
/*
Define globals which are present in the client, but not in node (and therefore not in
the jest test environment).
*/
import { TextDecoder, TextEncoder } from "util";
global.TextDecoder = TextDecoder;
global.TextEncoder = TextEncoder;
+6 -7
View File
@@ -11,14 +11,10 @@ describe("rangeEncodeIndices", () => {
test("sorted flag", () => { test("sorted flag", () => {
expect(rangeEncodeIndices([1, 9, 432], 10, true)).toMatchObject([ expect(rangeEncodeIndices([1, 9, 432], 10, true)).toMatchObject([
1, 1, 9, 432,
9,
432
]); ]);
expect(rangeEncodeIndices([1, 9, 432], 10, false)).toMatchObject([ expect(rangeEncodeIndices([1, 9, 432], 10, false)).toMatchObject([
1, 1, 9, 432,
9,
432
]); ]);
expect(rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, true)).toMatchObject( expect(rangeEncodeIndices([0, 1, 2, 3, 9, 10, 432], 2, true)).toMatchObject(
[[0, 3], [9, 10], 432] [[0, 3], [9, 10], 432]
@@ -34,7 +30,10 @@ describe("rangeEncodeIndices", () => {
).toMatchObject([[3, 7], [10, 12], 99]); ).toMatchObject([[3, 7], [10, 12], 99]);
expect( expect(
rangeEncodeIndices([3, 4, 5, 6, 7, 10, 11, 12], 3, false) rangeEncodeIndices([3, 4, 5, 6, 7, 10, 11, 12], 3, false)
).toMatchObject([[3, 7], [10, 12]]); ).toMatchObject([
[3, 7],
[10, 12],
]);
expect( expect(
rangeEncodeIndices([0, 3, 4, 5, 6, 7, 10, 11, 12], 3, false) rangeEncodeIndices([0, 3, 4, 5, 6, 7, 10, 11, 12], 3, false)
).toMatchObject([0, [3, 7], [10, 12]]); ).toMatchObject([0, [3, 7], [10, 12]]);
@@ -0,0 +1,322 @@
// these TWO statements MUST be first in the file, before any other imports
import { enableFetchMocks } from "jest-fetch-mock";
import * as serverMocks from "./serverMocks";
// OK, continue on!
import {
AnnoMatrixLoader,
clip,
isubset,
isubsetMask,
} from "../../../src/annoMatrix";
import { Dataframe } from "../../../src/util/dataframe";
enableFetchMocks();
describe("AnnoMatrix", () => {
let annoMatrix;
beforeEach(async () => {
fetch.resetMocks(); // reset all fetch mocking state
annoMatrix = new AnnoMatrixLoader(
serverMocks.baseDataURL,
serverMocks.schema.schema
);
});
describe("basics", () => {
test("annomatrix static checks", () => {
expect(annoMatrix).toBeDefined();
expect(annoMatrix.schema).toMatchObject(serverMocks.schema.schema);
expect(annoMatrix.nObs).toEqual(serverMocks.schema.schema.dataframe.nObs);
expect(annoMatrix.nVar).toEqual(serverMocks.schema.schema.dataframe.nVar);
expect(annoMatrix.isView).toBeFalsy();
expect(annoMatrix.viewOf).toBeUndefined();
expect(annoMatrix.rowIndex).toBeDefined();
});
test("simple single column fetch", async () => {
fetch.once(serverMocks.annotationsObs(["name_0"]));
const df = await annoMatrix.fetch("obs", "name_0");
expect(df).toBeInstanceOf(Dataframe);
expect(df.colIndex.labels()).toEqual(["name_0"]);
expect(df.dims).toEqual([annoMatrix.nObs, 1]);
});
test("simple multi column fetch", async () => {
fetch
.once(serverMocks.annotationsObs(["name_0"]))
.once(serverMocks.annotationsObs(["n_genes"]));
await expect(
annoMatrix.fetch("obs", ["name_0", "n_genes"])
).resolves.toBeInstanceOf(Dataframe);
});
describe("fetch from field", () => {
const getLastTwo = async (field) => {
const columnNames = annoMatrix.getMatrixColumns(field).slice(-2);
fetch.mockResponses(...columnNames.map(() => serverMocks.responder));
await expect(
annoMatrix.fetch(field, columnNames)
).resolves.toBeInstanceOf(Dataframe);
};
test("obs", async () => getLastTwo("obs"));
test("var", async () => getLastTwo("var"));
test("emb", async () => getLastTwo("emb"));
});
test("fetch - test all query forms", async () => {
// single string is a column name
fetch.once(serverMocks.annotationsObs(["n_genes"]));
await expect(annoMatrix.fetch("obs", "n_genes")).resolves.toBeInstanceOf(
Dataframe
);
// array of column names, expecting n_genes to be cached.
fetch.once(serverMocks.annotationsObs(["percent_mito"]));
await expect(
annoMatrix.fetch("obs", ["n_genes", "percent_mito"])
).resolves.toBeInstanceOf(Dataframe);
// more complex value filter query, enumerated
fetch.once(serverMocks.responder);
await expect(
annoMatrix.fetch("X", {
where: {
field: "var",
column: annoMatrix.schema.annotations.var.index,
value: "TYMP",
},
})
).resolves.toBeInstanceOf(Dataframe);
// more complex value filter query, range
const varIndex = annoMatrix.schema.annotations.var.index;
fetch
.once(
serverMocks.withExpected("/data/var", [[`var:${varIndex}`, "SUMO3"]])
)
.once(
serverMocks.withExpected("/data/var", [[`var:${varIndex}`, "TYMP"]])
);
await expect(
annoMatrix.fetch("X", [
{
where: {
field: "var",
column: varIndex,
value: "SUMO3",
},
},
{
where: {
field: "var",
column: varIndex,
value: "TYMP",
},
},
])
).resolves.toBeInstanceOf(Dataframe);
// XXX inspect the wherecache?
});
test("push and pop views", async () => {
const am1 = clip(annoMatrix, 0.1, 0.9);
expect(am1.viewOf).toBe(annoMatrix);
expect(am1.nObs).toEqual(annoMatrix.nObs);
expect(am1.nVar).toEqual(annoMatrix.nVar);
expect(am1.rowIndex).toBe(annoMatrix.rowIndex);
const am2 = clip(annoMatrix, 0.1, 0.9);
expect(am2.viewOf).toBe(annoMatrix);
expect(am2).not.toBe(am1);
expect(am2.rowIndex).toBe(annoMatrix.rowIndex);
});
test("schema accessors", () => {
expect(annoMatrix.getMatrixFields()).toEqual(
expect.arrayContaining(["X", "obs", "emb", "var"])
);
expect(annoMatrix.getMatrixColumns("obs")).toEqual(
expect.arrayContaining(["name_0", "n_genes", "louvain"])
);
expect(annoMatrix.getColumnSchema("emb", "umap")).toEqual({
name: "umap",
dims: ["umap_0", "umap_1"],
type: "float32",
});
expect(annoMatrix.getColumnDimensions("emb", "umap")).toEqual([
"umap_0",
"umap_1",
]);
});
/*
test the mask & label access to subset via isubset and isubsetMask
*/
test("isubset", async () => {
const rowList = [0, 10];
const rowMask = new Uint8Array(annoMatrix.nObs);
for (let i = 0; i < rowList.length; i += 1) {
rowMask[rowList[i]] = 1;
}
const am1 = isubset(annoMatrix, rowList);
const am2 = isubsetMask(annoMatrix, rowMask);
expect(am1).not.toBe(am2);
expect(am1.nObs).toEqual(2);
expect(am1.nObs).toEqual(am2.nObs);
expect(am1.nVar).toEqual(am2.nVar);
fetch
.once(serverMocks.annotationsObs(["n_genes"]))
.once(serverMocks.annotationsObs(["n_genes"]));
const ng1 = await am1.fetch("obs", "n_genes");
const ng2 = await am2.fetch("obs", "n_genes");
expect(ng1).toHaveLength(ng2.length);
expect(ng1.colIndex.labels()).toEqual(ng2.colIndex.labels());
expect(ng1.col("n_genes").asArray()).toEqual(
ng2.col("n_genes").asArray()
);
});
});
describe("add/drop column", () => {
async function addDrop(base) {
expect(base.getMatrixColumns("obs")).not.toContain("foo");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(base.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
/* add */
const am1 = base.addObsColumn(
{ name: "foo", type: "float32", writable: true },
Float32Array,
0
);
expect(base.getMatrixColumns("obs")).not.toContain("foo");
expect(am1.getMatrixColumns("obs")).toContain("foo");
const foo = await am1.fetch("obs", "foo");
expect(foo).toBeDefined();
expect(foo).toBeInstanceOf(Dataframe);
expect(foo).toHaveLength(am1.nObs);
expect(foo.col("foo").asArray()).toEqual(
new Float32Array(am1.nObs).fill(0)
);
/* drop */
const am2 = am1.dropObsColumn("foo");
expect(base.getMatrixColumns("obs")).not.toContain("foo");
expect(am2.getMatrixColumns("obs")).not.toContain("foo");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(am2.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
}
test("add/drop column, without view", async () => {
await addDrop(annoMatrix);
});
test("add/drop column, with view", async () => {
const am1 = clip(annoMatrix, 0.1, 0.9);
await addDrop(am1);
const am2 = isubset(am1, [0, 1, 2, 20, 30, 400]);
await addDrop(am2);
const am3 = isubset(annoMatrix, [10, 0, 7, 3]);
await addDrop(am3);
const am4 = clip(am3, 0, 1);
await addDrop(am4);
fetch.mockResponse(serverMocks.responder);
await am1.fetch("obs", am1.getMatrixColumns("obs"));
await am2.fetch("obs", am2.getMatrixColumns("obs"));
await am3.fetch("obs", am3.getMatrixColumns("obs"));
await am4.fetch("obs", am4.getMatrixColumns("obs"));
fetch.resetMocks();
await addDrop(am1);
await addDrop(am2);
await addDrop(am3);
await addDrop(am4);
});
});
describe("setObsColumnValues", () => {
async function addSetDrop(base) {
/* add column */
let am = base.addObsColumn(
{
name: "test",
type: "categorical",
categories: ["unassigned", "red", "green"],
writable: true,
},
Array,
"unassigned"
);
const testVal = await am.fetch("obs", "test");
expect(testVal.col("test").asArray()).toEqual(
new Array(am.nObs).fill("unassigned")
);
/* set values in column */
const whichRows = [1, 2, 10];
const am1 = await am.setObsColumnValues("test", whichRows, "yo");
const testVal1 = await am1.fetch("obs", "test");
const expt = new Array(am1.nObs).fill("unassigned");
for (let i = 0; i < whichRows.length; i += 1) {
const offset = am1.rowIndex.getOffset(whichRows[i]);
expt[offset] = "yo";
}
expect(testVal1).not.toBe(testVal);
expect(testVal1.col("test").asArray()).toEqual(expt);
expect(am1.getColumnSchema("obs", "test").type).toBe("categorical");
expect(am1.getColumnSchema("obs", "test").categories).toEqual(
expect.arrayContaining(["unassigned", "red", "green", "yo"])
);
/* drop column */
fetch.mockRejectOnce(new Error("unknown column name"));
am = am1.dropObsColumn("test");
await expect(am.fetch("obs", "test")).rejects.toThrow(
"unknown column name"
);
}
test("set, without a view", async () => {
await addSetDrop(annoMatrix);
});
test("set, with a view", async () => {
const am1 = clip(annoMatrix, 0.1, 0.9);
await addSetDrop(am1);
const am2 = isubset(am1, [0, 1, 2, 10, 20, 30, 400]);
await addSetDrop(am2);
const am3 = isubset(annoMatrix, [10, 1, 0, 30, 2]);
await addSetDrop(am3);
fetch.mockResponse(serverMocks.responder);
await am1.fetch("obs", am1.getMatrixColumns("obs"));
await am2.fetch("obs", am2.getMatrixColumns("obs"));
await am3.fetch("obs", am3.getMatrixColumns("obs"));
await addSetDrop(am1);
await addSetDrop(am2);
await addSetDrop(am3);
});
});
});
@@ -0,0 +1,692 @@
// these TWO statements MUST be first in the file, before any other imports
import { enableFetchMocks } from "jest-fetch-mock";
import * as serverMocks from "./serverMocks";
// OK, continue on!
import obsLouvain from "./louvain.json";
import obsNGenes from "./n_genes.json";
import embUmap from "./umap.json";
import {
AnnoMatrixLoader,
AnnoMatrixObsCrossfilter,
isubsetMask,
} from "../../../src/annoMatrix";
import { rangeFill } from "../../../src/util/range";
enableFetchMocks();
describe("AnnoMatrixCrossfilter", () => {
let annoMatrix;
let crossfilter;
beforeEach(async () => {
fetch.resetMocks(); // reset all fetch mocking state
annoMatrix = new AnnoMatrixLoader(
serverMocks.baseDataURL,
serverMocks.schema.schema
);
crossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
});
test("initial state of crossfilter", () => {
const { nObs } = annoMatrix;
expect(crossfilter).toBeDefined();
expect(crossfilter.size()).toEqual(nObs);
expect(crossfilter.annoMatrix).toBe(annoMatrix);
/* by default, everything should be selected, even if no data in cache */
expect(crossfilter.countSelected()).toEqual(nObs);
expect(crossfilter.allSelectedLabels()).toEqual(
rangeFill(new Int32Array(nObs))
);
expect(crossfilter.allSelectedMask()).toEqual(new Uint8Array(nObs).fill(1));
expect(crossfilter.fillByIsSelected(new Uint8Array(nObs), 2, 1)).toEqual(
new Uint8Array(nObs).fill(2)
);
});
describe("select", () => {
/*
test the selection state via crossfilter proxy
*/
test("select loads index", async () => {
/*
Select should transparently load/create dimension index.
Internal dimension names are field/col:col:col..., eg,
obs:louvain
emb:umap_0:umap_1
*/
expect(crossfilter.obsCrossfilter.dimensionNames()).toEqual([]);
expect(
crossfilter.obsCrossfilter.hasDimension("obs/louvain")
).toBeFalsy();
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
let newCrossfilter = await crossfilter.select("obs", "louvain", {
mode: "none",
});
expect(newCrossfilter.countSelected()).toEqual(0);
expect(
newCrossfilter.obsCrossfilter.hasDimension("obs/louvain")
).toBeTruthy();
expect(fetch.mock.calls).toHaveLength(1);
newCrossfilter = await crossfilter.select("obs", "louvain", {
mode: "all",
});
expect(newCrossfilter.countSelected()).toEqual(annoMatrix.nObs);
});
test("simple column select", async () => {
let xfltr;
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
xfltr = await crossfilter.select("obs", "louvain", {
mode: "exact",
values: ["NK cells", "B cells"],
});
expect(xfltr).toBeDefined();
expect(xfltr.countSelected()).toEqual(496);
expect(xfltr.allSelectedMask()).toEqual(
Uint8Array.from(
obsLouvain.map((val) =>
val === "NK cells" || val === "B cells" ? 1 : 0
)
)
);
expect(xfltr.allSelectedLabels()).toEqual(
Int32Array.from(
obsLouvain.reduce((acc, val, idx) => {
if (val === "NK cells" || val === "B cells") acc.push(idx);
return acc;
}, [])
)
);
expect(
xfltr.fillByIsSelected(new Uint8Array(annoMatrix.nObs), 3, 1)
).toEqual(
Uint8Array.from(
obsLouvain.map((val) =>
val === "NK cells" || val === "B cells" ? 3 : 1
)
)
);
const df = await annoMatrix.fetch("obs", "louvain");
const values = df.col("louvain").asArray();
const selected = xfltr.allSelectedMask();
values.every(
(val, idx) => !["NK cells", "B cells"].includes(val) !== !selected[idx]
);
fetch.once(
serverMocks.dataframeResponse(["n_genes"], [new Int32Array(obsNGenes)])
);
xfltr = await xfltr.select("obs", "n_genes", {
mode: "range",
lo: 0,
hi: 500,
inclusive: false,
});
expect(xfltr.countSelected()).toEqual(33);
expect(xfltr.allSelectedLabels()).toEqual(
Int32Array.from(
obsNGenes.reduce((acc, val, idx) => {
const louvain = obsLouvain[idx];
if (
val >= 0 &&
val < 500 &&
(louvain === "NK cells" || louvain === "B cells")
)
acc.push(idx);
return acc;
}, [])
)
);
xfltr = await xfltr.selectAll();
expect(xfltr.countSelected()).toEqual(annoMatrix.nObs);
});
test("join column select", async () => {
const varIndex = annoMatrix.schema.annotations.var.index;
const { nObs } = annoMatrix.schema.dataframe;
fetch.once(
serverMocks.dataframeResponse(
["TEST"],
[rangeFill(new Float32Array(nObs), 0, 0.1)]
)
);
const xfltr = await crossfilter.select(
"X",
{
where: {
field: "var",
column: varIndex,
value: "TYMP",
},
},
{
mode: "range",
lo: 0,
hi: 50,
inclusive: true,
}
);
expect(xfltr).toBeDefined();
expect(xfltr.countSelected()).toEqual(501);
const df = await annoMatrix.fetch("X", {
where: {
field: "var",
column: varIndex,
value: "TYMP",
},
});
const values = df.icol(0).asArray();
const selected = xfltr.allSelectedMask();
values.every((val, idx) => !(val >= 0 && val <= 50) !== !selected[idx]);
expect(selected.reduce((acc, val) => (val ? acc + 1 : acc), 0)).toEqual(
xfltr.countSelected()
);
});
test("spatial column select", async () => {
fetch.once(
serverMocks.dataframeResponse(
["umap_0", "umap_1"],
[Float32Array.from(embUmap[0]), Float32Array.from(embUmap[1])]
)
);
const xfltr = await crossfilter.select("emb", "umap", {
mode: "within-rect",
minX: 0,
minY: 0,
maxX: 0.5,
maxY: 0.5,
});
expect(xfltr.countSelected()).toEqual(16);
});
test("select on subset", async () => {
const mask = new Uint8Array(annoMatrix.nObs).fill(0);
for (let i = 0; i < mask.length; i += 2) {
mask[i] = true;
}
const annoMatrixSubset = isubsetMask(annoMatrix, mask);
expect(annoMatrixSubset.nObs).toEqual(Math.floor(annoMatrix.nObs / 2));
let xfltr = new AnnoMatrixObsCrossfilter(annoMatrixSubset);
expect(xfltr.countSelected()).toEqual(annoMatrixSubset.nObs);
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
xfltr = await xfltr.select("obs", "louvain", {
mode: "exact",
values: ["NK cells", "B cells"],
});
expect(xfltr).toBeDefined();
expect(xfltr.countSelected()).toEqual(240);
const df = await annoMatrixSubset.fetch("obs", "louvain");
const values = df.col("louvain").asArray();
const selected = xfltr.allSelectedMask();
values.every(
(val, idx) => !["NK cells", "B cells"].includes(val) !== !selected[idx]
);
});
test("select catches errors", async () => {
await expect(crossfilter.select("NADA", "foo")).rejects.toThrow(
"Unknown field name"
);
await expect(crossfilter.select("var", "foo")).rejects.toThrow(
"unable to obsSelect upon the var dimension"
);
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(crossfilter.select("obs", "foo")).rejects.toThrow(
"unknown column name"
);
});
});
describe("mutate matrix", () => {
/*
test the matrix mutators via crossfilter proxy
*/
async function helperAddTestCol(cf, colName, colSchema = null) {
expect(
cf.annoMatrix.getMatrixColumns("obs").includes(colName)
).toBeFalsy();
if (colSchema === null) {
colSchema = {
name: colName,
type: "categorical",
categories: ["toasty"],
};
}
colSchema.name = colName;
const initValue = colSchema.categories[0];
const xfltr = cf.addObsColumn(colSchema, Array, initValue);
expect(
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
(v) => v.name === colName
)
).toHaveLength(1);
const df = await xfltr.annoMatrix.fetch("obs", colName);
expect(df.hasCol(colName)).toBeTruthy();
return xfltr;
}
test("addObsColumn", async () => {
expect(crossfilter.countSelected()).toBe(annoMatrix.nObs);
expect(
crossfilter.annoMatrix.getMatrixColumns("obs").includes("foo")
).toBeFalsy();
const xfltr = crossfilter.addObsColumn(
{ name: "foo", type: "categorical", categories: ["A"] },
Array,
"A"
);
// check schema updates correctly.
expect(xfltr.countSelected()).toBe(annoMatrix.nObs);
expect(
xfltr.annoMatrix.getMatrixColumns("obs").includes("foo")
).toBeTruthy();
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toMatchObject({
name: "foo",
type: "categorical",
});
expect(
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
(v) => v.name === "foo"
)
).toHaveLength(1);
// check data update.
const df = await xfltr.annoMatrix.fetch("obs", "foo");
expect(
df
.col("foo")
.asArray()
.every((v) => v === "A")
).toBeTruthy();
// check that we catch dups
expect(() =>
xfltr.addObsColumn(
{ name: "foo", type: "categorical" },
Array,
"toasty"
)
).toThrow("column already exists");
expect(() =>
xfltr.addObsColumn(
{ name: "louvain", type: "categorical" },
Array,
"toasty"
)
).toThrow("column already exists");
});
test("dropObsColumn", async () => {
let xfltr;
/* check that we catch attempt to drop readonly dimension */
expect(() => crossfilter.dropObsColumn("louvain")).toThrow(
"Unknown or readonly obs column"
);
/* non-existent column */
expect(() => crossfilter.dropObsColumn("does-not-exist")).toThrow(
"Unknown or readonly obs column"
);
// add a column, then drop it.
xfltr = await helperAddTestCol(crossfilter, "foo");
xfltr = xfltr.dropObsColumn("foo");
expect(
xfltr.annoMatrix.schema.annotations.obs.columns.filter(
(v) => v.name === "foo"
)
).toHaveLength(0);
expect(xfltr.annoMatrix.schema.annotations.obsByName.foo).toBeUndefined();
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.annoMatrix.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
// now same, but ensure we have built an index before doing the drop
xfltr = await helperAddTestCol(crossfilter, "bar");
xfltr = await xfltr.select("obs", "bar", {
mode: "exact",
values: "whatever",
});
xfltr = xfltr.dropObsColumn("bar");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.select("obs", "bar", { mode: "all" })).rejects.toThrow(
"unknown column name"
);
});
test("renameObsColumn", async () => {
let xfltr;
/* catch attempts to rename non-existent or readonly columns */
expect(() =>
crossfilter.renameObsColumn("does-not-exist", "foo")
).toThrow("Unknown or readonly obs column");
expect(() => crossfilter.renameObsColumn("louvain", "foo")).toThrow(
"Unknown or readonly obs column"
);
// add a column, then rename it.
xfltr = await helperAddTestCol(crossfilter, "foo");
xfltr = xfltr.renameObsColumn("foo", "bar");
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toBeUndefined();
expect(xfltr.annoMatrix.getColumnSchema("obs", "bar")).toMatchObject({
name: "bar",
type: "categorical",
});
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.annoMatrix.fetch("obs", "foo")).rejects.toThrow(
"unknown column name"
);
const df = await xfltr.annoMatrix.fetch("obs", "bar");
expect(df.hasCol("bar")).toBeTruthy();
// now same, but ensure we have built an index before doing the rename
xfltr = await helperAddTestCol(crossfilter, "bar");
xfltr = await xfltr.select("obs", "bar", {
mode: "exact",
values: "whatever",
});
xfltr = xfltr.renameObsColumn("bar", "xyz");
fetch.mockRejectOnce(new Error("unknown column name"));
await expect(xfltr.select("obs", "bar", { mode: "all" })).rejects.toThrow(
"unknown column name"
);
await expect(
xfltr.select("obs", "xyz", { mode: "none" })
).resolves.toBeInstanceOf(AnnoMatrixObsCrossfilter);
});
test("addObsAnnoCategory", async () => {
let xfltr;
// catch unknown or readonly columns
expect(() => crossfilter.addObsAnnoCategory("louvain", "mumble")).toThrow(
"Unknown or readonly obs column"
);
expect(() =>
crossfilter.addObsAnnoCategory("undefined-name", "mumble")
).toThrow("Unknown or readonly obs column");
// add a column and then add category to it
xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned"],
});
xfltr = xfltr.addObsAnnoCategory("foo", "a-new-label");
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
});
// do it again, dup; should throw
expect(() => xfltr.addObsAnnoCategory("foo", "a-new-label")).toThrow(
"category already exists"
);
// now same, but ensure we have built an index before doing the operation
xfltr = await helperAddTestCol(crossfilter, "bar", {
name: "bar",
type: "categorical",
categories: ["unassigned"],
});
xfltr = await xfltr.select("obs", "bar", {
mode: "exact",
values: "something",
});
xfltr = xfltr.addObsAnnoCategory("bar", "a-new-label");
expect(xfltr.annoMatrix.getColumnSchema("obs", "bar")).toMatchObject({
name: "bar",
type: "categorical",
categories: expect.arrayContaining(["a-new-label", "unassigned"]),
});
});
test("removeObsAnnoCategory", async () => {
let xfltr;
// catch unknown or readonly categories
await expect(() =>
crossfilter.removeObsAnnoCategory("louvain", "mumble", "unassigned")
).rejects.toThrow("Unknown or readonly obs column");
await expect(() =>
crossfilter.removeObsAnnoCategory("undefined-name", "mumble")
).rejects.toThrow("Unknown or readonly obs column");
xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned", "red", "green", "blue"],
});
xfltr = await xfltr.select("obs", "foo", { mode: "all" });
expect(
(await xfltr.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "unassigned")
).toBeTruthy();
expect(xfltr.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"red",
"green",
"blue",
]),
});
// remove an unused category
const xfltr1 = await xfltr.removeObsAnnoCategory("foo", "red", "mumble");
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "unassigned")
).toBeTruthy();
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"green",
"blue",
"mumble",
]),
});
// remove a used category
const xfltr2 = await xfltr.removeObsAnnoCategory(
"foo",
"unassigned",
"red"
);
expect(
(await xfltr2.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "red")
).toBeTruthy();
expect(xfltr2.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining(["green", "blue", "red"]),
});
});
test("setObsColumnValues", async () => {
// catch unknown or readonly categories
await expect(() =>
crossfilter.setObsColumnValues("louvain", [0, 1], "unassigned")
).rejects.toThrow("Unknown or readonly obs column");
await expect(() =>
crossfilter.setObsColumnValues("undefined-name", [0], "mumble")
).rejects.toThrow("Unknown or readonly obs column");
let xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned", "red", "green", "blue"],
});
xfltr = await xfltr.select("obs", "foo", { mode: "all" });
// catch unknown row label
await expect(() =>
xfltr.setObsColumnValues("foo", [-1], "red")
).rejects.toThrow("Unknown row label");
// set a few rows
expect(
(await xfltr.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every((v) => v === "unassigned")
).toBeTruthy();
const xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.every(
(v, i) =>
v === "unassigned" || (v === "purple" && (i === 0 || i === 10))
)
).toBeTruthy();
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"red",
"green",
"blue",
"purple",
]),
});
expect(xfltr1.countSelected()).toEqual(xfltr1.annoMatrix.nObs);
const xfltr2 = await xfltr1.select("obs", "foo", {
mode: "exact",
values: ["purple"],
});
expect(xfltr2.countSelected()).toEqual(2);
expect(xfltr2.allSelectedLabels()).toEqual(Int32Array.from([0, 10]));
});
test("resetObsColumnValues", async () => {
// catch unknown or readonly categories
await expect(() =>
crossfilter.resetObsColumnValues("louvain", "red", "blue")
).rejects.toThrow("Unknown or readonly obs column");
await expect(() =>
crossfilter.resetObsColumnValues("undefined-name", "red", "blue")
).rejects.toThrow("Unknown or readonly obs column");
let xfltr = await helperAddTestCol(crossfilter, "foo", {
name: "foo",
type: "categorical",
categories: ["unassigned", "red", "green", "blue"],
});
xfltr = await xfltr.select("obs", "foo", {
mode: "exact",
values: "red",
});
// catch unknown category name label
await expect(() =>
xfltr.resetObsColumnValues("foo", "unknown-label", "red")
).rejects.toThrow("unknown category");
let xfltr1 = await xfltr.setObsColumnValues("foo", [0, 10], "purple");
xfltr1 = await xfltr1.select("obs", "foo", {
mode: "exact",
values: "purple",
});
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.filter((v) => v === "purple")
).toHaveLength(2);
xfltr1 = await xfltr1.resetObsColumnValues("foo", "purple", "magenta");
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.filter((v) => v === "magenta")
).toHaveLength(2);
expect(
(await xfltr1.annoMatrix.fetch("obs", "foo"))
.col("foo")
.asArray()
.filter((v) => v === "purple")
).toHaveLength(0);
expect(xfltr1.annoMatrix.getColumnSchema("obs", "foo")).toMatchObject({
name: "foo",
type: "categorical",
categories: expect.arrayContaining([
"unassigned",
"red",
"green",
"blue",
"purple",
"magenta",
]),
});
});
});
describe("edge cases", () => {
test("transition from empty annoMatrix", async () => {
// select before fetch needs to work
fetch.once(serverMocks.dataframeResponse(["louvain"], [obsLouvain]));
const xfltr = await crossfilter.select("obs", "louvain", {
mode: "exact",
values: "B cells",
});
expect(fetch.mock.calls).toHaveLength(1);
expect(xfltr.obsCrossfilter.hasDimension("obs/louvain")).toBeTruthy();
expect(xfltr.obsCrossfilter.all()).toBe(xfltr.annoMatrix._cache.obs);
expect(xfltr.countSelected()).toEqual(
obsLouvain.reduce(
(count, v) => (v === "B cells" ? count + 1 : count),
0
)
);
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,180 @@
[
781, 1352, 1131, 960, 522, 782, 783, 790, 533, 550, 1116, 751, 866, 1059, 458,
335, 1424, 1014, 1446, 446, 1020, 417, 878, 789, 510, 824, 1545, 996, 937,
1368, 428, 406, 1020, 786, 1019, 750, 822, 982, 876, 930, 838, 1014, 732, 877,
782, 787, 791, 880, 801, 1215, 343, 1460, 1250, 756, 836, 824, 827, 1238,
1243, 1652, 843, 825, 656, 776, 766, 1465, 790, 871, 803, 965, 800, 876, 690,
988, 906, 741, 620, 867, 916, 969, 803, 732, 555, 790, 862, 900, 674, 397,
663, 563, 786, 859, 568, 412, 1043, 1206, 702, 1263, 929, 1079, 938, 316, 900,
919, 862, 903, 390, 1717, 819, 1877, 660, 791, 478, 769, 481, 819, 866, 600,
1185, 650, 775, 699, 642, 857, 832, 388, 710, 341, 894, 935, 604, 1008, 985,
679, 603, 864, 1031, 887, 603, 610, 1119, 669, 794, 963, 756, 637, 1032, 776,
860, 825, 852, 745, 858, 925, 1228, 806, 715, 668, 779, 1197, 888, 1273, 873,
847, 781, 959, 805, 554, 604, 785, 978, 910, 936, 997, 961, 1314, 799, 1112,
677, 775, 1298, 657, 626, 1313, 467, 936, 977, 780, 1311, 432, 579, 850, 736,
800, 892, 860, 720, 822, 681, 954, 889, 1265, 919, 799, 833, 496, 1476, 848,
869, 1059, 490, 897, 832, 864, 419, 856, 907, 791, 756, 771, 957, 1190, 680,
524, 908, 506, 851, 775, 793, 748, 951, 643, 1277, 828, 480, 969, 1112, 648,
805, 1223, 1023, 669, 489, 390, 350, 1113, 837, 1547, 840, 581, 748, 1861,
735, 488, 1016, 585, 797, 769, 490, 1307, 895, 686, 602, 772, 704, 892, 1169,
1375, 1189, 892, 2455, 355, 1856, 1317, 703, 825, 736, 1997, 892, 1034, 545,
1188, 659, 1056, 819, 979, 632, 598, 690, 310, 803, 743, 560, 1073, 844, 882,
841, 815, 771, 976, 986, 820, 957, 640, 1012, 927, 794, 753, 791, 366, 539,
752, 769, 650, 947, 771, 824, 744, 837, 723, 640, 923, 1174, 1597, 699, 618,
1418, 820, 1047, 981, 866, 527, 762, 717, 860, 603, 828, 476, 1071, 775, 614,
913, 836, 669, 942, 792, 871, 1046, 859, 793, 822, 751, 435, 1142, 781, 718,
471, 1750, 892, 841, 1156, 1031, 912, 873, 824, 1233, 1312, 575, 605, 717,
1019, 1215, 928, 1780, 657, 718, 646, 808, 1120, 750, 390, 1100, 456, 755,
1118, 571, 867, 728, 916, 491, 960, 625, 1090, 772, 968, 480, 810, 725, 1016,
1011, 1075, 808, 672, 950, 862, 766, 963, 507, 570, 678, 768, 1037, 885, 1426,
1496, 587, 879, 924, 783, 696, 1057, 783, 867, 919, 1251, 1023, 727, 645,
1217, 929, 792, 994, 1025, 946, 600, 881, 975, 1609, 758, 772, 682, 998, 979,
1045, 706, 808, 855, 819, 1147, 742, 914, 969, 704, 1398, 581, 809, 921, 805,
542, 888, 519, 1092, 762, 698, 752, 771, 899, 1101, 760, 881, 1124, 809, 445,
1703, 789, 641, 819, 890, 767, 806, 1323, 942, 807, 981, 888, 726, 1190, 826,
661, 713, 816, 822, 806, 864, 464, 664, 931, 860, 674, 803, 464, 788, 1068,
781, 843, 779, 873, 707, 492, 669, 982, 749, 789, 780, 561, 655, 432, 801,
945, 770, 503, 766, 776, 970, 989, 654, 762, 882, 1236, 1499, 626, 1380, 1170,
491, 833, 924, 581, 842, 596, 811, 542, 1572, 758, 854, 274, 1413, 872, 559,
907, 951, 1175, 946, 905, 737, 604, 843, 606, 1079, 668, 785, 726, 978, 941,
994, 655, 1013, 987, 591, 1041, 625, 582, 814, 570, 775, 822, 715, 738, 956,
1178, 743, 1861, 841, 944, 783, 643, 924, 936, 431, 490, 532, 524, 620, 749,
1334, 834, 790, 702, 892, 693, 784, 944, 471, 839, 529, 729, 1084, 802, 886,
815, 856, 746, 1318, 545, 696, 872, 1154, 467, 725, 1027, 479, 728, 926, 1282,
907, 833, 1024, 838, 900, 737, 367, 459, 1030, 1279, 756, 662, 1323, 1003,
359, 770, 813, 634, 924, 1184, 901, 816, 1421, 771, 706, 953, 348, 716, 870,
715, 550, 689, 947, 1157, 690, 383, 374, 882, 697, 246, 833, 1006, 1181, 974,
856, 978, 1551, 965, 907, 565, 417, 907, 927, 966, 658, 727, 743, 381, 385,
905, 645, 1167, 936, 724, 618, 1038, 853, 808, 1403, 762, 741, 687, 932, 1096,
601, 652, 895, 682, 1553, 604, 823, 803, 646, 917, 942, 850, 795, 771, 949,
516, 664, 1001, 637, 619, 907, 961, 812, 793, 1043, 1343, 1326, 981, 675, 937,
631, 1026, 1135, 499, 948, 801, 848, 741, 604, 864, 1076, 1106, 1111, 624,
1008, 908, 815, 346, 1062, 803, 749, 779, 1027, 1032, 1040, 654, 631, 755,
854, 850, 798, 864, 1078, 690, 864, 1523, 838, 966, 389, 1654, 808, 885, 1665,
920, 855, 807, 859, 1276, 987, 1079, 678, 626, 831, 829, 1009, 547, 893, 722,
656, 415, 773, 1262, 1218, 365, 661, 805, 1409, 1094, 779, 898, 830, 1242,
864, 576, 901, 1274, 852, 1006, 719, 763, 683, 957, 831, 724, 354, 763, 910,
749, 626, 870, 795, 1078, 736, 835, 402, 1203, 699, 755, 697, 1229, 637, 666,
846, 1036, 1027, 831, 999, 942, 891, 1007, 712, 990, 725, 745, 921, 333, 1042,
895, 873, 1612, 724, 929, 601, 862, 908, 658, 775, 724, 753, 741, 690, 379,
608, 927, 777, 969, 827, 709, 385, 690, 769, 554, 892, 761, 367, 731, 1103,
944, 832, 675, 652, 418, 727, 745, 872, 1336, 863, 934, 844, 721, 432, 782,
1006, 834, 840, 840, 819, 699, 489, 665, 576, 1291, 1102, 826, 880, 738, 904,
686, 874, 887, 873, 560, 766, 710, 1135, 1054, 805, 724, 973, 1201, 575, 838,
865, 546, 811, 884, 886, 791, 1026, 2000, 644, 763, 969, 800, 359, 624, 993,
800, 1167, 833, 1871, 616, 822, 647, 1000, 618, 734, 618, 1938, 861, 945,
1032, 723, 984, 994, 771, 738, 1583, 1113, 614, 1146, 615, 848, 983, 677, 972,
791, 827, 804, 395, 843, 493, 741, 941, 1659, 742, 1517, 559, 937, 740, 781,
819, 813, 578, 1022, 1191, 824, 1146, 757, 638, 830, 713, 609, 1271, 680, 769,
1119, 731, 804, 781, 916, 735, 835, 1257, 472, 879, 851, 1023, 661, 1008, 748,
845, 393, 675, 843, 876, 939, 932, 760, 735, 1561, 752, 940, 705, 405, 690,
1071, 544, 927, 817, 388, 560, 1322, 640, 886, 1075, 689, 524, 606, 802, 868,
939, 753, 770, 1105, 841, 786, 445, 703, 593, 875, 901, 927, 798, 1221, 415,
1381, 949, 1322, 1169, 745, 727, 799, 490, 767, 943, 808, 926, 664, 569, 843,
727, 1222, 457, 1515, 1138, 1174, 525, 878, 525, 999, 778, 772, 819, 1015,
939, 856, 715, 793, 837, 1193, 764, 834, 677, 625, 420, 837, 874, 679, 811,
546, 787, 587, 821, 669, 813, 780, 649, 924, 1322, 701, 792, 817, 667, 804,
593, 740, 1243, 859, 685, 596, 1193, 859, 775, 947, 689, 907, 734, 621, 336,
922, 802, 812, 941, 943, 868, 947, 767, 701, 692, 423, 775, 500, 1100, 812,
728, 616, 928, 835, 454, 812, 769, 1058, 914, 628, 649, 452, 754, 1327, 776,
670, 1017, 599, 431, 967, 1077, 2033, 731, 533, 926, 725, 559, 870, 964, 1341,
1981, 1103, 326, 428, 808, 821, 554, 596, 680, 1034, 849, 566, 879, 1091, 823,
447, 1688, 868, 1254, 942, 462, 1055, 852, 738, 804, 775, 726, 993, 1462,
1007, 798, 1036, 786, 948, 743, 761, 838, 1040, 859, 867, 1188, 846, 687, 672,
629, 725, 660, 809, 469, 600, 812, 856, 397, 786, 895, 882, 449, 890, 823,
1051, 823, 1055, 741, 999, 1241, 790, 878, 778, 1066, 815, 465, 1079, 743,
1098, 807, 1120, 1025, 805, 676, 828, 763, 997, 852, 866, 1118, 508, 928, 958,
932, 892, 905, 494, 710, 1068, 795, 787, 951, 720, 842, 890, 1355, 1005, 872,
1185, 912, 869, 894, 997, 770, 554, 806, 1426, 1012, 452, 896, 426, 239, 829,
895, 787, 1139, 925, 1015, 1360, 1097, 650, 853, 549, 1052, 307, 1152, 907,
1628, 731, 897, 1749, 762, 712, 1195, 851, 864, 968, 845, 331, 840, 734, 948,
842, 1543, 661, 981, 912, 912, 1063, 683, 823, 996, 695, 1483, 927, 574, 1052,
571, 1028, 1263, 671, 958, 747, 866, 896, 489, 643, 923, 820, 1466, 550, 1112,
1006, 1448, 727, 899, 998, 563, 870, 903, 516, 754, 879, 588, 740, 798, 798,
653, 902, 990, 724, 953, 891, 1437, 653, 714, 956, 877, 1012, 824, 1077, 740,
692, 1063, 771, 808, 1389, 1264, 952, 816, 795, 795, 760, 886, 349, 868, 842,
819, 626, 418, 903, 838, 723, 436, 1112, 724, 1299, 719, 843, 1090, 696, 885,
627, 809, 423, 729, 853, 855, 608, 627, 823, 1063, 575, 743, 1528, 681, 544,
422, 731, 920, 761, 884, 982, 784, 496, 573, 521, 663, 794, 975, 856, 978,
590, 905, 695, 816, 976, 816, 753, 791, 858, 813, 841, 1085, 1692, 716, 955,
1467, 741, 296, 738, 1573, 1119, 918, 283, 703, 842, 1253, 676, 1636, 1273,
380, 799, 1491, 878, 939, 725, 1365, 818, 719, 1343, 905, 837, 803, 990, 1084,
976, 1630, 795, 1408, 771, 650, 779, 648, 817, 1127, 882, 954, 830, 732, 783,
756, 708, 976, 718, 887, 809, 795, 662, 912, 1550, 1509, 1021, 1751, 776, 910,
714, 530, 846, 631, 1152, 1118, 755, 573, 1176, 267, 918, 1132, 849, 938,
1140, 909, 840, 806, 904, 788, 778, 715, 869, 714, 883, 767, 858, 788, 553,
634, 1230, 1131, 849, 811, 827, 1753, 713, 484, 783, 722, 596, 514, 372, 925,
747, 840, 673, 955, 796, 719, 718, 857, 578, 1063, 594, 997, 1268, 341, 388,
753, 834, 593, 1189, 911, 738, 476, 816, 641, 746, 635, 953, 801, 326, 849,
499, 865, 1420, 487, 876, 797, 981, 756, 850, 1097, 998, 829, 1159, 955, 1061,
696, 786, 743, 1211, 893, 491, 744, 1447, 767, 1050, 853, 1118, 1428, 555,
612, 854, 789, 889, 784, 712, 1323, 921, 682, 794, 846, 1012, 839, 807, 587,
881, 781, 1010, 1182, 1149, 812, 607, 1155, 714, 642, 1088, 1291, 1196, 325,
804, 370, 359, 855, 1165, 836, 923, 863, 1284, 1011, 889, 984, 512, 939, 883,
697, 1211, 362, 969, 1135, 1239, 580, 1103, 975, 825, 1170, 921, 640, 1180,
378, 982, 916, 1122, 792, 619, 750, 913, 775, 661, 766, 768, 675, 944, 940,
761, 727, 767, 873, 1043, 850, 995, 680, 595, 700, 753, 736, 891, 685, 780,
986, 989, 830, 810, 685, 784, 642, 742, 961, 906, 829, 621, 822, 717, 1210,
800, 1963, 749, 757, 570, 831, 721, 336, 802, 1001, 886, 631, 759, 631, 550,
984, 767, 835, 777, 639, 860, 1413, 747, 779, 540, 367, 1629, 1380, 689, 1001,
809, 337, 1103, 796, 966, 782, 1018, 642, 967, 436, 826, 779, 1000, 601, 796,
945, 1679, 1123, 596, 995, 720, 588, 759, 452, 780, 836, 515, 846, 392, 283,
710, 1158, 796, 895, 585, 559, 859, 879, 858, 842, 643, 1308, 595, 1181, 909,
710, 821, 817, 841, 1197, 640, 1425, 947, 900, 852, 460, 1100, 824, 780, 932,
542, 1137, 1225, 997, 572, 780, 765, 906, 793, 753, 772, 854, 936, 1048, 819,
645, 619, 314, 726, 737, 1162, 1081, 868, 1032, 913, 476, 490, 799, 1201, 997,
898, 212, 1586, 427, 947, 937, 724, 380, 715, 739, 931, 973, 773, 1497, 906,
798, 953, 471, 821, 806, 714, 828, 727, 773, 976, 856, 727, 761, 1084, 1557,
693, 559, 627, 795, 750, 838, 803, 453, 734, 607, 1029, 805, 669, 505, 858,
832, 1019, 585, 1225, 1287, 903, 752, 2020, 774, 666, 843, 857, 887, 1082,
656, 674, 911, 734, 910, 672, 802, 539, 699, 941, 828, 800, 642, 733, 607,
992, 379, 562, 847, 787, 1461, 732, 941, 785, 696, 795, 809, 828, 651, 882,
972, 1323, 679, 774, 784, 766, 520, 671, 796, 644, 1549, 756, 723, 788, 643,
856, 825, 730, 831, 653, 429, 641, 637, 812, 1527, 859, 972, 744, 869, 508,
624, 923, 976, 801, 1014, 1429, 586, 692, 704, 1176, 806, 883, 1249, 765, 743,
907, 666, 669, 364, 794, 959, 766, 937, 1398, 942, 1469, 905, 812, 572, 1378,
1058, 1215, 697, 531, 676, 1819, 503, 801, 943, 874, 813, 694, 494, 660, 1467,
976, 833, 689, 921, 625, 1428, 817, 909, 956, 765, 1207, 829, 1648, 554, 1500,
953, 647, 537, 786, 814, 761, 862, 838, 1102, 1392, 1042, 372, 971, 1364,
1137, 847, 935, 710, 1070, 914, 855, 759, 654, 981, 1193, 397, 1123, 616, 747,
876, 949, 965, 789, 752, 717, 1198, 952, 625, 784, 914, 659, 913, 863, 959,
1637, 796, 1508, 906, 714, 1195, 867, 819, 375, 656, 1047, 745, 866, 1186,
1669, 539, 942, 839, 927, 796, 734, 831, 967, 620, 814, 605, 1391, 655, 1512,
625, 719, 547, 864, 902, 853, 1143, 990, 858, 604, 1291, 701, 859, 768, 1621,
715, 594, 783, 1608, 927, 740, 805, 705, 491, 514, 768, 880, 993, 356, 748,
993, 801, 843, 1194, 794, 606, 810, 882, 682, 1126, 792, 829, 1657, 786, 610,
664, 790, 852, 678, 589, 930, 690, 840, 647, 622, 427, 856, 522, 1262, 722,
641, 997, 983, 1099, 607, 704, 947, 886, 1062, 659, 995, 492, 754, 676, 837,
532, 598, 1274, 921, 700, 828, 828, 543, 815, 709, 842, 1200, 1126, 1327, 849,
1160, 707, 675, 709, 990, 270, 445, 892, 1040, 832, 815, 437, 1401, 1341, 480,
1517, 704, 622, 823, 731, 781, 849, 1209, 472, 838, 643, 1101, 879, 895, 408,
832, 845, 928, 920, 1269, 652, 1463, 840, 706, 956, 1219, 612, 970, 789, 382,
533, 627, 388, 655, 1103, 782, 1231, 617, 589, 758, 882, 909, 661, 558, 947,
491, 1386, 1481, 823, 1064, 681, 652, 363, 824, 933, 611, 886, 806, 531, 864,
648, 1142, 736, 955, 826, 725, 1174, 841, 1006, 1418, 955, 829, 1038, 632,
750, 846, 1033, 859, 890, 893, 1030, 913, 1385, 750, 416, 703, 522, 856, 914,
742, 381, 1079, 671, 1101, 858, 663, 581, 845, 881, 823, 835, 1290, 598, 1130,
793, 838, 765, 768, 815, 810, 912, 818, 790, 665, 703, 875, 657, 1567, 688,
749, 1076, 782, 543, 1100, 841, 809, 789, 1263, 758, 368, 1022, 730, 1101,
478, 488, 618, 940, 771, 784, 847, 1303, 821, 1111, 752, 1128, 958, 742, 782,
824, 751, 872, 939, 606, 698, 705, 963, 607, 621, 1650, 1093, 545, 670, 617,
723, 1194, 1019, 1291, 758, 855, 1549, 743, 1372, 802, 337, 1121, 1028, 1524,
645, 847, 866, 941, 751, 583, 796, 793, 975, 936, 524, 659, 607, 1433, 562,
696, 927, 517, 719, 599, 639, 977, 1019, 816, 672, 1903, 1162, 964, 936, 947,
989, 790, 902, 982, 673, 856, 629, 692, 843, 940, 795, 780, 821, 471, 702,
631, 1557, 868, 901, 798, 1020, 885, 881, 719, 1043, 1238, 565, 776, 696, 725,
365, 811, 1212, 1178, 1132, 661, 831, 786, 471, 835, 564, 929, 958, 706, 388,
842, 965, 1088, 511, 794, 900, 865, 789, 504, 701, 817, 796, 972, 906, 871,
922, 724, 628, 1479, 533, 1101, 1913, 855, 1266, 884, 817, 619, 591, 685, 887,
1336, 656, 1227, 980, 817, 582, 1370, 460, 638, 471, 650, 414, 907, 1147, 732,
992, 801, 822, 529, 737, 806, 816, 889, 1305, 588, 657, 1154, 713, 326, 1129,
1603, 879, 1156, 642, 285, 825, 823, 719, 1253, 971, 853, 916, 1053, 515,
1017, 953, 832, 645, 667, 1326, 547, 636, 1783, 1211, 788, 807, 1104, 884,
848, 788, 1013, 1003, 916, 818, 828, 882, 959, 395, 368, 787, 929, 1379, 711,
733, 752, 464, 626, 735, 946, 876, 647, 536, 954, 486, 712, 786, 438, 807,
1016, 551, 841, 929, 757, 971, 708, 567, 881, 801, 873, 805, 1359, 866, 945,
1068, 819, 815, 1058, 845, 881, 1051, 1179, 718, 657, 882, 709, 754, 735, 603,
944, 1794, 712, 721, 1097, 813, 788, 917, 656, 1104, 1268, 1239, 862, 739,
858, 1066, 752, 615, 721, 571, 861, 933, 807, 1082, 820, 887, 850, 1567, 803,
1156, 721, 692, 700, 458, 637, 873, 1544, 1155, 1227, 622, 454, 724
]
@@ -0,0 +1,4 @@
export const baseDataURL = "https://a.fake.url/api/v0.2";
export { schema } from "./schema";
export * from "./routes";
@@ -0,0 +1,211 @@
import { schema } from "./schema";
import { Dataframe, KeyIndex } from "../../../../src/util/dataframe";
import { encodeMatrixFBS } from "../../../../src/util/stateManager/matrix";
const indexedSchema = {
obsByName: Object.fromEntries(
schema.schema.annotations.obs.columns.map((v) => [v.name, v]) ?? []
),
varByName: Object.fromEntries(
schema.schema.annotations.var.columns.map((v) => [v.name, v]) ?? []
),
embByName: Object.fromEntries(
schema.schema.layout.obs.map((v) => [v.name, v]) ?? []
),
};
function makeMockColumn(s, length) {
const { type } = s;
switch (type) {
case "int32":
return new Int32Array(length).fill(Math.floor(99 * Math.random()));
case "string":
return new Array(length).fill("test");
case "float32":
return new Float32Array(length).fill(99 * Math.random());
case "boolean":
return new Array(length).fill(false);
case "categorical":
return new Array(length).fill(s.categories[0]);
default:
throw new Error("unkonwn type");
}
}
function getEncodedDataframe(colNames, length, colSchemas) {
const colIndex = new KeyIndex(colNames);
const columns = colSchemas.map((s) => makeMockColumn(s, length));
const df = new Dataframe([length, colNames.length], columns, null, colIndex);
const body = encodeMatrixFBS(df);
return body;
}
export function dataframeResponse(colNames, columns) {
const colIndex = new KeyIndex(colNames);
const df = new Dataframe(
[columns[0].length, colNames.length],
columns,
null,
colIndex
);
const body = encodeMatrixFBS(df);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return () => Promise.resolve({ body, init: { status: 200, headers } });
}
function annotationObsResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const names = params
.filter(([k]) => k === "annotation-name")
.map(([, v]) => v);
if (!names.every((n) => indexedSchema.obsByName[n])) {
return Promise.reject(new Error("bad obs annotation name in URL"));
}
const colSchemas = names.map((n) => indexedSchema.obsByName[n]);
const body = getEncodedDataframe(
names,
schema.schema.dataframe.nObs,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
function annotationVarResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const names = params
.filter(([k]) => k === "annotation-name")
.map(([, v]) => v);
if (!names.every((n) => indexedSchema.varByName[n])) {
return Promise.reject(new Error("bad var annotation name in URL"));
}
const colSchemas = names.map((n) => indexedSchema.varByName[n]);
const body = getEncodedDataframe(
names,
schema.schema.dataframe.nVar,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
function layoutObsResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const names = params.filter(([k]) => k === "layout-name").map(([, v]) => v);
if (!names.every((n) => indexedSchema.embByName[n])) {
return Promise.reject(new Error("bad layout name in URL"));
}
const dims = names.map((n) => indexedSchema.embByName[n].dims).flat();
const colSchemas = names
.map((n) => [indexedSchema.embByName[n], indexedSchema.embByName[n]])
.flat();
const body = getEncodedDataframe(
dims,
schema.schema.dataframe.nObs,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
function dataVarResponse(request) {
const url = new URL(request.url);
const params = Array.from(url.searchParams.entries());
const colNames = params.map((v) => `${v[0]}/${v[1]}`);
const colSchemas = colNames.map(() => schema.schema.dataframe);
const body = getEncodedDataframe(
colNames,
schema.schema.dataframe.nObs,
colSchemas
);
const headers = new Headers({
"Content-Type": "application/octet-stream",
});
return Promise.resolve({
body,
init: { status: 200, headers },
});
}
export function responder(request) {
const url = new URL(request.url);
const { pathname } = url;
if (pathname.endsWith("/annotations/obs")) {
return annotationObsResponse(request);
}
if (pathname.endsWith("/annotations/var")) {
return annotationVarResponse(request);
}
if (pathname.endsWith("/layout/obs")) {
return layoutObsResponse(request);
}
if (pathname.endsWith("/data/var")) {
return dataVarResponse(request);
}
return Promise.reject(new Error("bad URL"));
}
export function withExpected(expectedURL, expectedParams) {
/*
Do some additional error checking
*/
return (request) => {
// if URL is bogus, reject the promise
const url = new URL(request.url);
if (!url.pathname.endsWith(expectedURL)) {
return Promise.reject(new Error("Unexpected URL!"));
}
const params = Array.from(url.searchParams.entries()).sort(
(a, b) => a[0] < b[0]
);
expectedParams = expectedParams.slice().sort((a, b) => a[0] < b[0]);
if (
params.length !== expectedParams.length ||
!params.every(
(p, i) => p[0] === expectedParams[i][0] && p[1] === expectedParams[i][1]
)
) {
return Promise.reject(new Error("unexpected name requested in URL"));
}
return responder(request);
};
}
export function annotationsObs(names) {
return withExpected(
"/annotations/obs",
names.map((name) => ["annotation-name", name])
);
}
@@ -0,0 +1,80 @@
export const schema = {
schema: {
annotations: {
obs: {
columns: [
{
name: "name_0",
type: "string",
writable: false,
},
{
name: "n_genes",
type: "int32",
writable: false,
},
{
name: "percent_mito",
type: "float32",
writable: false,
},
{
name: "n_counts",
type: "float32",
writable: false,
},
{
name: "louvain",
type: "string",
writable: false,
},
],
index: "name_0",
},
var: {
columns: [
{
name: "name_0",
type: "string",
writable: false,
},
{
name: "n_cells",
type: "int32",
writable: false,
},
],
index: "name_0",
},
},
dataframe: {
nObs: 2638,
nVar: 1838,
type: "float32",
},
layout: {
obs: [
{
dims: ["draw_graph_fr_0", "draw_graph_fr_1"],
name: "draw_graph_fr",
type: "float32",
},
{
dims: ["pca_0", "pca_1"],
name: "pca",
type: "float32",
},
{
dims: ["tsne_0", "tsne_1"],
name: "tsne",
type: "float32",
},
{
dims: ["umap_0", "umap_1"],
name: "umap",
type: "float32",
},
],
},
},
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,382 @@
import sha1 from "sha1";
import {
_whereCacheGet,
_whereCacheCreate,
_whereCacheMerge,
} from "../../../src/annoMatrix/whereCache";
const schema = {};
describe("whereCache", () => {
test("whereCacheGet - where query, missing cache values", () => {
expect(
_whereCacheGet({}, schema, "X", {
where: {
field: "var",
column: "foo",
value: "bar",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet({}, schema, "X", {
summarize: {
field: "var",
column: "foo",
values: ["bar"],
},
})
).toEqual([undefined]);
expect(
_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(
{ 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",
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]);
});
test("whereCacheGet - varied lookups", () => {
const whereCache = {
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", {
where: {
field: "var",
column: "foo",
value: "bar",
},
})
).toEqual([0]);
expect(
_whereCacheGet(whereCache, schema, "X", {
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", {
where: {
field: "whoknows",
column: "whatever",
value: "snork",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
where: {
field: "var",
column: "whatever",
value: "snork",
},
})
).toEqual([undefined]);
expect(
_whereCacheGet(whereCache, schema, "X", {
where: {
field: "var",
column: "foo",
value: "snork",
},
})
).toEqual([undefined]);
});
test("whereCacheCreate, where query", () => {
const query = {
where: {
field: "queryField",
column: "queryColumn",
value: "queryValue",
},
};
const wc = _whereCacheCreate(
"field",
{
where: {
field: "queryField",
column: "queryColumn",
value: "queryValue",
},
},
[0, 1, 2]
);
expect(wc).toBeDefined();
expect(wc).toEqual(
expect.objectContaining({
where: {
field: {
queryField: expect.any(Map),
},
},
})
);
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("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",
{ where: { field: "queryField", column: "queryColumn", value: "foo" } },
["foo"]
);
const dst1 = _whereCacheCreate(
"field",
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
["dst1"]
);
wc = _whereCacheMerge(dst1, src);
expect(
_whereCacheGet(wc, schema, "field", {
where: {
field: "queryField",
column: "queryColumn",
value: "foo",
},
})
).toEqual(["foo"]);
expect(
_whereCacheGet(wc, schema, "field", {
where: {
field: "queryField",
column: "queryColumn",
value: "bar",
},
})
).toEqual(["dst1"]);
const dst2 = _whereCacheCreate(
"field",
{ where: { field: "queryField", column: "queryColumn", value: "bar" } },
["dst2"]
);
wc = _whereCacheMerge(dst2, dst1, src);
expect(
_whereCacheGet(wc, schema, "field", {
where: {
field: "queryField",
column: "queryColumn",
value: "foo",
},
})
).toEqual(["foo"]);
expect(
_whereCacheGet(wc, schema, "field", {
where: {
field: "queryField",
column: "queryColumn",
value: "bar",
},
})
).toEqual(["dst1"]);
wc = _whereCacheMerge({}, src);
expect(wc).toEqual(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]);
});
});
+75
View File
@@ -0,0 +1,75 @@
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 { normalizeWritableCategoricalSchema } from "../../src/annoMatrix/normalize";
describe("centroid", () => {
let schema;
let obsAnnotations;
let obsLayout;
beforeAll(() => {
schema = indexEntireSchema(cloneDeep(REST.schema.schema));
obsAnnotations = matrixFBSToDataframe(REST.annotationsObs);
obsLayout = matrixFBSToDataframe(REST.layoutObs);
normalizeWritableCategoricalSchema(
schema.annotations.obsByName.field3,
obsAnnotations.col("field3")
);
});
test("field4 (categorical obsAnnotation)", () => {
const centroidResult = calcCentroid(
schema,
"field4",
obsAnnotations,
{ current: "umap", currentDimNames: ["umap_0", "umap_1"] },
obsLayout
);
// Check to see that a centroid has been calculated for every categorical value
const keysAsArray = Array.from(centroidResult.keys());
expect(keysAsArray).toEqual(
expect.arrayContaining([83, true, "foo", 2.222222])
);
// This expected result assumes that all cells belong in all categorical values inside of sample response
const expectedResult = [
quantile([0.5], obsLayout.col("umap_0").asArray())[0],
quantile([0.5], obsLayout.col("umap_1").asArray())[0],
];
centroidResult.forEach((coordinate) => {
expect(coordinate).toEqual(expectedResult);
});
});
test("field3 (boolean obsAnnotation)", () => {
const centroidResult = calcCentroid(
schema,
"field3",
obsAnnotations,
{ current: "umap", currentDimNames: ["umap_0", "umap_1"] },
obsLayout
);
// Check to see that a centroid has been calculated for every categorical value
const keysAsArray = Array.from(centroidResult.keys());
expect(keysAsArray).toEqual(expect.arrayContaining([false, true]));
// This expected result assumes that all cells belong in all categorical values inside of sample response
const expectedResult = [
quantile([0.5], obsLayout.col("umap_0").asArray())[0],
quantile([0.5], obsLayout.col("umap_1").asArray())[0],
];
centroidResult.forEach((coordinate) => {
expect(coordinate).toEqual(expectedResult);
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
import * as Dataframe from "../../../src/util/dataframe";
describe("Dataframe column histogram", () => {
test("categorical by categorical", () => {
const df = new Dataframe.Dataframe(
[3, 3],
[["n1", "n2", "n3"], ["c1", "c2", "c3"], new Int32Array([0, 1, 2])],
null,
new Dataframe.KeyIndex(["name", "cat", "value"])
);
const h1 = df.col("cat").histogram(df.col("name"));
expect(h1).toMatchObject(
new Map([
["n1", new Map([["c1", 1]])],
["n2", new Map([["c2", 1]])],
["n3", new Map([["c3", 1]])],
])
);
// memoized?
expect(df.col("cat").histogram(df.col("name"))).toMatchObject(h1);
});
test("continuous by categorical", () => {
const df = new Dataframe.Dataframe(
[3, 3],
[["n1", "n2", "n3"], ["c1", "c2", "c3"], new Int32Array([0, 1, 2])],
null,
new Dataframe.KeyIndex(["name", "cat", "value"])
);
const h1 = df.col("value").histogram(3, [0, 2], df.col("name"));
expect(h1).toMatchObject(
new Map([
["n1", [1, 0, 0]],
["n2", [0, 1, 0]],
["n3", [0, 0, 1]],
])
);
// memoized?
expect(df.col("value").histogram(3, [0, 2], df.col("name"))).toMatchObject(
h1
);
});
test("categorical", () => {
const df = new Dataframe.Dataframe(
[3, 3],
[["n1", "n2", "n3"], ["c1", "c2", "c3"], new Int32Array([0, 1, 2])],
null,
new Dataframe.KeyIndex(["name", "cat", "value"])
);
const h1 = df.col("cat").histogram();
expect(h1).toMatchObject(
new Map([
["c1", 1],
["c2", 1],
["c3", 1],
])
);
// memoized?
expect(df.col("value").histogram(3, [0, 2])).toMatchObject(h1);
});
test("continuous", () => {
const df = new Dataframe.Dataframe(
[3, 3],
[["n1", "n2", "n3"], ["c1", "c2", "c3"], new Int32Array([0, 1, 2])],
null,
new Dataframe.KeyIndex(["name", "cat", "value"])
);
const h1 = df.col("value").histogram(3, [0, 2]);
expect(h1).toMatchObject([1, 1, 1]);
// memoized?
expect(df.col("value").histogram(3, [0, 2])).toMatchObject(h1);
});
test("continuous thesholds correct", () => {
const vals = [0, 1, 9, 10, 11, 20, 99, 100];
const df = new Dataframe.Dataframe(
[8, 2],
[new Int32Array(vals), new Float32Array(vals)]
);
expect(df.col(0).histogram(5, [0, 100])).toEqual([5, 1, 0, 0, 2]);
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,
]);
});
});
@@ -0,0 +1,281 @@
import * as Dataframe from "../../../src/util/dataframe";
function float32Conversion(f) {
return new Float32Array([f])[0];
}
describe("Dataframe column summary", () => {
test("empty column test", () => {
const df = Dataframe.Dataframe.create([0, 1], [[]]);
const summary = df.icol(0).summarize();
expect(summary).toEqual(
expect.objectContaining({
categorical: true,
categories: [],
categoryCounts: new Map(),
numCategories: 0,
})
);
});
test("simple test", () => {
const df = new Dataframe.Dataframe(
[1, 6],
[
["n1"],
["hi"],
[true],
new Float32Array([39.3]),
new Int32Array([99]),
[1],
],
null,
new Dataframe.KeyIndex([
"name",
"nameString",
"nameBoolean",
"nameFloat32",
"nameInt32",
"nameCategorical",
])
);
expect(df.icol(0).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: ["n1"],
categoryCounts: new Map([["n1", 1]]),
numCategories: 1,
})
);
expect(df.icol(1).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: ["hi"],
categoryCounts: new Map([["hi", 1]]),
numCategories: 1,
})
);
expect(df.icol(2).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: [true],
categoryCounts: new Map([[true, 1]]),
numCategories: 1,
})
);
expect(df.icol(3).summarize()).toEqual(
expect.objectContaining({
categorical: false,
min: float32Conversion(39.3),
max: float32Conversion(39.3),
nan: 0,
ninf: 0,
pinf: 0,
})
);
expect(df.icol(4).summarize()).toEqual(
expect.objectContaining({
categorical: false,
min: 99,
max: 99,
nan: 0,
ninf: 0,
pinf: 0,
})
);
expect(df.icol(5).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: [1],
categoryCounts: new Map([[1, 1]]),
numCategories: 1,
})
);
});
test("multi test", () => {
const df = new Dataframe.Dataframe(
[3, 6],
[
["n0", "n1", "n2"],
["hi", "hi", "bye"],
[false, true, true],
new Float32Array([39.3, 39.3, 0]),
new Int32Array([99, 99, 99]),
[1, false, "0"],
],
null,
new Dataframe.KeyIndex([
"name",
"nameString",
"nameBoolean",
"nameFloat32",
"nameInt32",
"nameCategorical",
])
);
expect(df.icol(0).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining(["n0", "n1", "n2"]),
categoryCounts: new Map([
["n0", 1],
["n1", 1],
["n2", 1],
]),
numCategories: 3,
})
);
expect(df.icol(1).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining(["hi", "bye"]),
categoryCounts: new Map([
["hi", 2],
["bye", 1],
]),
numCategories: 2,
})
);
expect(df.icol(2).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining([true, false]),
categoryCounts: new Map([
[true, 2],
[false, 1],
]),
numCategories: 2,
})
);
expect(df.icol(3).summarize()).toEqual(
expect.objectContaining({
categorical: false,
min: 0,
max: float32Conversion(39.3),
nan: 0,
ninf: 0,
pinf: 0,
})
);
expect(df.icol(4).summarize()).toEqual(
expect.objectContaining({
categorical: false,
min: 99,
max: 99,
nan: 0,
ninf: 0,
pinf: 0,
})
);
expect(df.icol(5).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining([1, false, "0"]),
categoryCounts: new Map([
[1, 1],
[false, 1],
["0", 1],
]),
numCategories: 3,
})
);
});
test("non-finite numbers", () => {
const df = new Dataframe.Dataframe(
[4, 6],
[
["n0", "n1", "n2", "n2"],
["hi", "hi", "bye", "bye"],
[false, true, true, true],
new Float32Array([
39.3,
Number.NEGATIVE_INFINITY,
Number.NaN,
Number.POSITIVE_INFINITY,
]),
new Int32Array([99, 99, 99, 99]),
[1, false, "0", "0"],
],
null,
new Dataframe.KeyIndex([
"name",
"nameString",
"nameBoolean",
"nameFloat32",
"nameInt32",
"nameCategorical",
])
);
expect(df.icol(0).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining(["n0", "n1", "n2"]),
categoryCounts: new Map([
["n0", 1],
["n1", 1],
["n2", 2],
]),
numCategories: 3,
})
);
expect(df.icol(1).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining(["hi", "bye"]),
categoryCounts: new Map([
["hi", 2],
["bye", 1],
]),
numCategories: 2,
})
);
expect(df.icol(2).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining([true, false]),
categoryCounts: new Map([
[true, 2],
[false, 1],
]),
numCategories: 2,
})
);
expect(df.icol(3).summarize()).toEqual(
expect.objectContaining({
categorical: false,
min: float32Conversion(39.3),
max: float32Conversion(39.3),
nan: 1,
ninf: 1,
pinf: 1,
})
);
expect(df.icol(4).summarize()).toEqual(
expect.objectContaining({
categorical: false,
min: 99,
max: 99,
nan: 0,
ninf: 0,
pinf: 0,
})
);
expect(df.icol(5).summarize()).toEqual(
expect.objectContaining({
categorical: true,
categories: expect.arrayContaining([1, false, "0"]),
categoryCounts: new Map([
[1, 1],
[false, 1],
["0", 1],
]),
numCategories: 3,
})
);
});
});
+8 -8
View File
@@ -6,7 +6,7 @@ import {
obsAnnoDimensionName, obsAnnoDimensionName,
diffexpDimensionName, diffexpDimensionName,
userDefinedDimensionName, userDefinedDimensionName,
makeContinuousDimensionName makeContinuousDimensionName,
} from "../../src/util/nameCreators"; } from "../../src/util/nameCreators";
describe("nameCreators", () => { describe("nameCreators", () => {
@@ -14,25 +14,25 @@ describe("nameCreators", () => {
layoutDimensionName, layoutDimensionName,
obsAnnoDimensionName, obsAnnoDimensionName,
diffexpDimensionName, diffexpDimensionName,
userDefinedDimensionName userDefinedDimensionName,
]; ];
test("check for namespace isolation", () => { test("check for namespace isolation", () => {
const foo = "foo"; const foo = "foo";
nameCreators.forEach(fn => expect(fn(foo)).not.toBe(foo)); nameCreators.forEach((fn) => expect(fn(foo)).not.toBe(foo));
const bar = "bar"; const bar = "bar";
nameCreators.forEach(fn => expect(fn(foo)).not.toBe(fn(bar))); nameCreators.forEach((fn) => expect(fn(foo)).not.toBe(fn(bar)));
nameCreators.forEach(fn => { nameCreators.forEach((fn) => {
const allOtherFn = nameCreators.filter(elmnt => elmnt !== fn); const allOtherFn = nameCreators.filter((elmnt) => elmnt !== fn);
allOtherFn.forEach(otherFn => expect(fn(foo)).not.toBe(otherFn(foo))); allOtherFn.forEach((otherFn) => expect(fn(foo)).not.toBe(otherFn(foo)));
}); });
}); });
test("check for legal keys", () => { test("check for legal keys", () => {
/* need namespace creators to return strings only */ /* need namespace creators to return strings only */
nameCreators.forEach(fn => expect(fn("X")).toMatch(/X/)); nameCreators.forEach((fn) => expect(fn("X")).toMatch(/X/));
}); });
}); });
@@ -0,0 +1,96 @@
import PromiseLimit from "../../src/util/promiseLimit";
import { range } from "../../src/util/range";
const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
describe("PromiseLimit", () => {
test("simple evaluation, concurrency 1", async () => {
const plimit = new PromiseLimit(1);
const result = await Promise.all([
plimit.add(() => Promise.resolve(1)),
plimit.add(() => Promise.resolve(2)),
plimit.add(() => Promise.resolve(3)),
plimit.add(() => Promise.resolve(4)),
]);
expect(result).toEqual([1, 2, 3, 4]);
});
test("simple evaluation, concurrency > 1", async () => {
const plimit = new PromiseLimit(100);
const result = await Promise.all([
plimit.add(() => Promise.resolve(1)),
plimit.add(() => Promise.resolve(2)),
plimit.add(() => Promise.resolve(3)),
plimit.add(() => Promise.resolve(4)),
]);
expect(result).toEqual([1, 2, 3, 4]);
});
test("eval in order of insertion", async () => {
const plimit = new PromiseLimit(100);
const result = await Promise.all([
plimit.add(() => Promise.resolve(1)),
plimit.add(() => Promise.resolve(2)),
plimit.add(() => Promise.resolve(3)),
plimit.add(() => Promise.resolve(4)),
]);
expect(result).toEqual([1, 2, 3, 4]);
});
test("obeys concurrency limit", async () => {
const plimit = new PromiseLimit(2);
let running = 0;
let maxRunning = 0;
const callback = async () => {
running += 1;
maxRunning = running > maxRunning ? running : maxRunning;
await delay(100);
running -= 1;
};
await Promise.all(range(10).map((i) => plimit.add(() => callback(i))));
expect(maxRunning).toEqual(2);
});
test("rejection", async () => {
const plimit = new PromiseLimit(2);
const result = await Promise.all([
plimit.add(() => Promise.resolve("OK")),
// eslint-disable-next-line prefer-promise-reject-errors -- unit test
plimit.add(() => Promise.reject("not OK")).catch((e) => e),
plimit.add(() => Promise.resolve("OK")),
plimit
.add(() => {
throw new Error("not OK");
})
.catch((e) => e.message),
]);
expect(result).toEqual(["OK", "not OK", "OK", "not OK"]);
});
test("priority queue", async () => {
const plimit = new PromiseLimit(1);
let finishOrder = 0;
const callback = () => async () => {
await delay(100);
const result = finishOrder;
finishOrder += 1;
return result;
};
const result = await Promise.all([
plimit.add(callback()),
plimit.priorityAdd(4, callback()),
plimit.priorityAdd(0, callback()),
plimit.priorityAdd(1, callback()),
plimit.priorityAdd(-1, callback()),
]);
expect(result).toEqual([0, 4, 2, 3, 1]);
});
});
+25
View File
@@ -0,0 +1,25 @@
import quantile from "../../src/util/quantile";
describe("quantile", () => {
test("single q", () => {
const arr = new Float32Array([9, 3, 5, 6, 0]);
expect(quantile([1.0], arr)).toMatchObject([9]);
expect(quantile([0.9], arr)).toMatchObject([9]);
expect(quantile([0.8], arr)).toMatchObject([9]);
expect(quantile([0.7], arr)).toMatchObject([6]);
expect(quantile([0.6], arr)).toMatchObject([6]);
expect(quantile([0.5], arr)).toMatchObject([5]);
expect(quantile([0.4], arr)).toMatchObject([5]);
expect(quantile([0.3], arr)).toMatchObject([3]);
expect(quantile([0.2], arr)).toMatchObject([3]);
expect(quantile([0.1], arr)).toMatchObject([0]);
expect(quantile([0], arr)).toMatchObject([0]);
});
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,
]);
});
});
+48
View File
@@ -0,0 +1,48 @@
import { range, rangeFill, linspace } from "../../src/util/range";
describe("range", () => {
test("no defaults", () => {
expect(range(0, 3, 1)).toMatchObject([0, 1, 2]);
});
test("range(stop)", () => {
expect(range(3)).toMatchObject([0, 1, 2]);
expect(range(0)).toMatchObject([]);
expect(range(1)).toMatchObject([0]);
});
test("range(start,stop)", () => {
expect(range(0, 0)).toMatchObject([]);
expect(range(0, 2)).toMatchObject([0, 1]);
expect(range(4, 8)).toMatchObject([4, 5, 6, 7]);
});
test("range(start, stop, step", () => {
expect(range(4, 0, -1)).toMatchObject([4, 3, 2, 1]);
expect(range(0, 4, 2)).toMatchObject([0, 2]);
});
});
describe("rangefill", () => {
test("rangeFill(arr)", () => {
expect(rangeFill(new Int32Array(3))).toMatchObject(
new Int32Array([0, 1, 2])
);
});
test("rangeFill(arr, start)", () => {
expect(rangeFill(new Int32Array(2), 1)).toMatchObject(
new Int32Array([1, 2])
);
});
test("rangeFill(arr, start, step)", () => {
expect(rangeFill(new Int32Array(3), 2, -1)).toMatchObject(
new Int32Array([2, 1, 0])
);
});
});
describe("linspace", () => {
test("linspace(arr, start, step)", () => {
expect(linspace(0.0, 2.0, 5)).toMatchObject([0.0, 0.5, 1.0, 1.5, 2.0]);
});
});
@@ -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 */
@@ -0,0 +1,9 @@
/*
test controls helpers
*/
// TODO #2227 test: improve test coverage on control helper functions
// (`isSelectableCategoryName()`, `selectableCategoryNames()`, `createCategorySummaryFromDfCol()`, `createCategoricalSelection()`, )
describe("controls helpers", () => {
test("placeholder", () => {});
});
@@ -0,0 +1,34 @@
/*
test FBS encode/decode API
*/
import { Dataframe, KeyIndex } from "../../../src/util/dataframe";
import {
decodeMatrixFBS,
encodeMatrixFBS,
} from "../../../src/util/stateManager/matrix";
describe("encode/decode", () => {
test("round trip", () => {
const columns = [
["red", "green", "blue"],
new Int32Array(3).fill(0),
new Uint32Array(3).fill(1),
new Float32Array(3).fill(2),
];
const dfNoColIdx = new Dataframe([3, 4], columns);
const dfA = decodeMatrixFBS(encodeMatrixFBS(dfNoColIdx));
expect([dfA.nRows, dfA.nCols]).toEqual(dfNoColIdx.dims);
expect(dfA.colIdx).toBeNull();
expect(dfA.rowIdx).toBeNull();
expect(dfA.columns).toEqual(columns);
const colIndex = new KeyIndex(["a", "b", "c", "d"]);
const dfWithColIdx = new Dataframe([3, 4], columns, null, colIndex);
const dfB = decodeMatrixFBS(encodeMatrixFBS(dfWithColIdx));
expect([dfB.nRows, dfB.nCols]).toEqual(dfWithColIdx.dims);
expect(dfB.colIdx).toEqual(colIndex.labels());
expect(dfB.rowIdx).toBeNull();
expect(dfB.columns).toEqual(columns);
});
});
@@ -1,249 +0,0 @@
import _ from "lodash";
import * as kvCache from "../../../src/util/stateManager/keyvalcache";
/*
This is PRIVATE to keyvalcache and must be kept in sync with
any changs ot that module. Need to Know - to enable error handling test
*/
const cachePrivateKey = "__kvcachekey__";
/*
helper function - promisify setTimeout()
*/
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
describe("kvcache API", () => {
/*
test the happy path create/set/get API
*/
test("simple create", () => {
/* with defaults */
const kvc = kvCache.create();
expect(kvc).toBeDefined();
expect(kvc).toEqual(expect.objectContaining({}));
expect(kvCache.get(kvc, "test")).toBeUndefined();
/* with params */
const kvc1 = kvCache.create(/* lowWatermark */ 99, /* minTTL */ 0);
expect(kvc1).toBeDefined();
expect(kvc1).toEqual(expect.objectContaining({}));
});
test("set/get", () => {
/*
- check basic get/set functionality
- check set does not mutate source cache
*/
const keyName = "foo";
const kvc1 = kvCache.create();
expect(kvc1).toBeDefined();
expect(kvCache.get(kvc1, keyName)).toBeUndefined();
const val2 = [2];
const kvc2 = kvCache.set(kvc1, keyName, val2);
expect(kvc2).toBeDefined();
expect(kvc2).not.toBe(kvc1);
expect(kvCache.get(kvc1, keyName)).toBeUndefined();
expect(kvCache.get(kvc2, keyName)).toBe(val2);
const val3 = [3];
const kvc3 = kvCache.set(kvc2, keyName, val3);
expect(kvc3).toBeDefined();
expect(kvc3).not.toBe(kvc1);
expect(kvc3).not.toBe(kvc2);
expect(kvCache.get(kvc1, keyName)).toBeUndefined();
expect(kvCache.get(kvc2, keyName)).toBe(val2);
expect(kvCache.get(kvc3, keyName)).toBe(val3);
});
});
describe("common error handling", () => {
/*
Test common error handlers
*/
test("set() protection from namespace pollution", () => {
/*
Test that set() will not allow use of the private cache key
*/
const kvc = kvCache.create();
expect(() => {
kvCache.set(kvc, cachePrivateKey, {});
}).toThrow();
});
test("create() does not accept bogus config", () => {
expect(() => {
kvCache.create([], {});
}).toThrow();
expect(() => {
kvCache.create(-99, 0);
}).toThrow();
expect(() => {
kvCache.create(100, -1);
}).toThrow();
expect(() => {
kvCache.create(1000, "foobar");
}).toThrow();
expect(() => {
kvCache.create(null, 8);
}).toThrow();
});
});
describe("map", () => {
/*
Test kvCache.map() - create new cache that is a transformation of an
existing cache
*/
test("map of empty cache", () => {
const kvc = kvCache.create();
const callback = jest.fn();
const kvcMapped = kvCache.map(kvc, callback);
expect(callback).not.toHaveBeenCalled();
expect(kvcMapped).toBeDefined();
expect(kvcMapped).not.toBe(kvc); // immutable operation
expect(kvcMapped).toEqual(kvc);
});
test("map of non-empty cache", () => {
const key = "aKey";
const val = [0, 1, 2];
let kvc = kvCache.create();
kvc = kvCache.set(kvc, key, val);
const mockCB = jest.fn().mockImplementation(v => [...v]);
const kvcMapped = kvCache.map(kvc, mockCB);
expect(kvcMapped).toBeDefined();
expect(kvcMapped).not.toBe(kvc); // immutable operation
expect(_.isEqual(kvc, kvcMapped)).toBe(true);
expect(mockCB).toHaveBeenCalledTimes(1);
expect(mockCB).toHaveBeenLastCalledWith(val, key);
});
});
describe("flush", () => {
/*
test various cache flush behavior
*/
test("flush - lowWatermark, disable minTTL", () => {
/*
verify lowWatermark functions correctly
*/
// set lowWatermark to 2, set three times - only the final two
// should remain.
let kvc = kvCache.create(2, 0);
["a", "b", "c"].forEach(k => {
kvc = kvCache.set(kvc, k, []);
});
expect(kvc).toEqual(
expect.objectContaining({
b: expect.arrayContaining([]),
c: expect.arrayContaining([])
})
);
expect(kvc).toEqual(
expect.not.objectContaining({
a: expect.arrayContaining([])
})
);
});
test("flush - minTTL, disable lowWatermark", async () => {
/*
verify minTTL functions correctly
*/
// set minTTL to 1 ms
let kvc = kvCache.create(0, 10);
kvc = kvCache.set(kvc, "a", []);
await timeout(20);
["b", "c"].forEach(k => {
kvc = kvCache.set(kvc, k, []);
});
expect(kvc).toEqual(
expect.objectContaining({
b: expect.arrayContaining([]),
c: expect.arrayContaining([])
})
);
expect(kvc).toEqual(
expect.not.objectContaining({
a: expect.arrayContaining([])
})
);
});
test("flush - minTTL and lowWatermark", async () => {
/*
verify minTTL functions correctly
*/
// set lowwatermark to 3, minTTL to 1 ms
let kvc = kvCache.create(3, 10);
kvc = kvCache.set(kvc, "a", []);
// delay
await timeout(20);
["b", "c"].forEach(k => {
kvc = kvCache.set(kvc, k, []);
});
expect(kvc).toEqual(
expect.objectContaining({
a: expect.arrayContaining([]),
b: expect.arrayContaining([]),
c: expect.arrayContaining([])
})
);
kvc = kvCache.set(kvc, "d", []);
expect(kvc).toEqual(
expect.objectContaining({
b: expect.arrayContaining([]),
c: expect.arrayContaining([]),
d: expect.arrayContaining([])
})
);
expect(kvc).toEqual(
expect.not.objectContaining({
a: expect.arrayContaining([])
})
);
});
test("manual flush", async () => {
let kvc = kvCache.create(1, 10);
["a", "b", "c", "d"].forEach(k => {
kvc = kvCache.set(kvc, k, []);
});
// Before TTL has expired, should have all values in cache.
expect(kvc).toEqual(
expect.objectContaining({
a: expect.arrayContaining([]),
b: expect.arrayContaining([]),
c: expect.arrayContaining([])
})
);
// let TTL expire
await timeout(10);
// manually flush
const postFlushKvc = kvCache.flush(kvc);
expect(postFlushKvc).toBeDefined();
expect(postFlushKvc).not.toBe(kvc);
expect(postFlushKvc).toEqual(
expect.objectContaining({
d: expect.arrayContaining([])
})
);
});
});
@@ -1,5 +1,10 @@
/* eslint no-bitwise: "off" */ import every from "lodash.every";
import map from "lodash.map";
import isNumber from "lodash.isnumber";
import zip from "lodash.zip";
import _ from "lodash"; import _ from "lodash";
import { flatbuffers } from "flatbuffers";
import { NetEncoding } from "../../../src/util/stateManager/matrix_generated";
/* /*
test data mocking REST 0.2 API responses. Used in several tests. test data mocking REST 0.2 API responses. Used in several tests.
@@ -15,13 +20,13 @@ const aConfigResponse = {
{ method: "POST", path: "/cluster/", available: false }, { method: "POST", path: "/cluster/", available: false },
{ method: "POST", path: "/layout/", available: false }, { method: "POST", path: "/layout/", available: false },
{ method: "POST", path: "/diffexp/", available: false }, { method: "POST", path: "/diffexp/", available: false },
{ method: "POST", path: "/saveLocal/", available: false } { method: "POST", path: "/saveLocal/", available: false },
], ],
displayNames: { displayNames: {
engine: "the little engine that could", engine: "the little engine that could",
dataset: "all your zeros are mine" dataset: "all your zeros are mine",
} },
} },
}; };
const aSchemaResponse = { const aSchemaResponse = {
@@ -29,88 +34,173 @@ const aSchemaResponse = {
dataframe: { dataframe: {
nObs, nObs,
nVar, nVar,
type: "float32" type: "float32",
}, },
annotations: { annotations: {
obs: [ obs: {
{ name: "name", type: "string" }, index: "name",
{ name: "field1", type: "int32" }, columns: [
{ name: "field2", type: "float32" }, { name: "name", type: "string" },
{ name: "field3", type: "boolean" }, { name: "field1", type: "int32" },
{ { name: "field2", type: "float32" },
name: "field4", { name: "field3", type: "boolean" },
type: "categorical", {
categories: field4Categories name: "field4",
} type: "categorical",
], categories: field4Categories,
var: [ },
{ name: "name", type: "string" }, ],
{ name: "fieldA", type: "int32" }, },
{ name: "fieldB", type: "float32" }, var: {
{ name: "fieldC", type: "boolean" }, index: "name",
{ columns: [
name: "fieldD", { name: "name", type: "string" },
type: "categorical", { name: "fieldA", type: "int32" },
categories: fieldDCategories { name: "fieldB", type: "float32" },
} { name: "fieldC", type: "boolean" },
] {
} name: "fieldD",
} type: "categorical",
categories: fieldDCategories,
},
],
},
},
layout: {
obs: [{ name: "umap", type: "float32", dims: ["umap_0", "umap_1"] }],
var: [],
},
},
}; };
const anAnnotationsObsResponse = { const anAnnotationsObsJSONResponse = {
names: ["name", "field1", "field2", "field3", "field4"], names: ["name", "field1", "field2", "field3", "field4"],
data: _() data: _()
.range(nObs) .range(nObs)
.map(idx => [ .map((idx) => [
idx, idx,
`obs${idx}`, `obs${idx}`,
2 * idx, 2 * idx,
idx + 0.0133, idx + 0.0133,
// eslint-disable-next-line no-bitwise -- idx & 1 to check for odd numbers
!!(idx & 1), !!(idx & 1),
field4Categories[idx % field4Categories.length] field4Categories[idx % field4Categories.length],
]) ])
.value() .value(),
}; };
const anAnnotationsVarResponse = { const anAnnotationsVarJSONResponse = {
names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"], names: ["fieldA", "fieldB", "fieldC", "fieldD", "name"],
data: _() data: _()
.range(nVar) .range(nVar)
.map(idx => [ .map((idx) => [
idx, idx,
10 * idx, 10 * idx,
idx + 2.90143, idx + 2.90143,
// eslint-disable-next-line no-bitwise -- idx & 1 to check for odd numbers
!!(idx & 1), !!(idx & 1),
fieldDCategories[idx % fieldDCategories.length], fieldDCategories[idx % fieldDCategories.length],
`var${idx}` `var${idx}`,
]) ])
.value() .value(),
}; };
const aLayoutResponse = { function encodeTypedArray(builder, uType, uData) {
layout: { const uTypeName = NetEncoding.TypedArray[uType];
ndims: 2, const ArrayType = NetEncoding[uTypeName];
coordinates: _() const dv = ArrayType.createDataVector(builder, uData);
.range(nObs) builder.startObject(1);
.map(idx => [idx, Math.random(), Math.random()]) builder.addFieldOffset(0, dv, 0);
.value() return builder.endObject();
}
function encodeMatrix(columns, colIndex = undefined) {
/*
IMPORTANT: this is not a general purpose encoder. in particular,
it doesn't correctly handle all column index types, nor does it
handle all column typedarray types.
encodeMatrixFBS in matrix.py is more general. This is used only
as a testing santity check (alt implementation).
*/
const utf8Encoder = new TextEncoder("utf-8");
const builder = new flatbuffers.Builder(1024);
const cols = map(columns, (carr) => {
let uType;
let tarr;
if (every(carr, isNumber)) {
uType = NetEncoding.TypedArray.Float32Array;
tarr = encodeTypedArray(builder, uType, new Float32Array(carr));
} else {
uType = NetEncoding.TypedArray.JSONEncodedArray;
const json = JSON.stringify(carr);
const jsonUTF8 = utf8Encoder.encode(json);
tarr = encodeTypedArray(builder, uType, jsonUTF8);
}
NetEncoding.Column.startColumn(builder);
NetEncoding.Column.addUType(builder, uType);
NetEncoding.Column.addU(builder, tarr);
return NetEncoding.Column.endColumn(builder);
});
const encColumns = NetEncoding.Matrix.createColumnsVector(builder, cols);
let encColIndex;
if (colIndex) {
encColIndex = encodeTypedArray(
builder,
NetEncoding.TypedArray.JSONEncodedArray,
utf8Encoder.encode(JSON.stringify(colIndex))
);
} }
};
NetEncoding.Matrix.startMatrix(builder);
NetEncoding.Matrix.addNRows(builder, columns[0].length);
NetEncoding.Matrix.addNCols(builder, columns.length);
NetEncoding.Matrix.addColumns(builder, encColumns);
if (colIndex) {
NetEncoding.Matrix.addColIndexType(
builder,
NetEncoding.TypedArray.JSONEncodedArray
);
NetEncoding.Matrix.addColIndex(builder, encColIndex);
}
const root = NetEncoding.Matrix.endMatrix(builder);
builder.finish(root);
return builder.asUint8Array();
}
const anAnnotationsObsFBSResponse = (() => {
const columns = zip(...anAnnotationsObsJSONResponse.data).slice(1);
return encodeMatrix(columns, anAnnotationsObsJSONResponse.names);
})();
const anAnnotationsVarFBSResponse = (() => {
const columns = zip(...anAnnotationsVarJSONResponse.data).slice(1);
return encodeMatrix(columns, anAnnotationsVarJSONResponse.names);
})();
const aLayoutFBSResponse = (() => {
const coords = [
new Float32Array(nObs).fill(Math.random()),
new Float32Array(nObs).fill(Math.random()),
];
return encodeMatrix(coords, ["umap_0", "umap_1"]);
})();
const aDataObsResponse = { const aDataObsResponse = {
var: [2, 4, 29], var: [2, 4, 29],
obs: _() obs: _()
.range(nObs) .range(nObs)
.map(idx => [idx, Math.random(), Math.random(), Math.random()]) .map((idx) => [idx, Math.random(), Math.random(), Math.random()])
.value() .value(),
}; };
export { export {
aLayoutResponse as layoutObs, aLayoutFBSResponse as layoutObs,
aDataObsResponse as dataObs, aDataObsResponse as dataObs,
anAnnotationsVarResponse as annotationsVar, anAnnotationsVarFBSResponse as annotationsVar,
anAnnotationsObsResponse as annotationsObs, anAnnotationsObsFBSResponse as annotationsObs,
aSchemaResponse as schema, aSchemaResponse as schema,
aConfigResponse as config aConfigResponse as config,
}; };
@@ -1,121 +0,0 @@
import _ from "lodash";
import * as Universe from "../../../src/util/stateManager/universe";
import * as REST from "./sampleResponses";
describe("createUniverseFromRestV02Response", () => {
/*
test createUniverseFromRestV02Response - this function converts
a set of REST 0.2 responses into a "new" Universe.
createUniverseFromRestV02Response(
configResponse,
schemaResponse,
annotationsObsResponse,
annotationsVarResponse,
layoutObsResponse
) --> Universe
where:
configResponse: GET /.../config
schemaResponse: GET /.../schema
annotationsObsResponse: GET /.../annotations/obs
annotationsVarResponse: GET /.../annotations/var
layoutObsResponse: GET /.../layout/obs
See spec in docs/REST_API.md.
*/
test("create from test data", () => {
/*
create a universe from sample data nad validate its shape & contents
*/
const { nObs, nVar } = REST.schema.schema.dataframe;
const universe = Universe.createUniverseFromRestV02Response(
REST.config,
REST.schema,
REST.annotationsObs,
REST.annotationsVar,
REST.layoutObs
);
expect(universe).toBeDefined();
expect(universe).toMatchObject(
expect.objectContaining({
api: "0.2",
nObs,
nVar,
schema: REST.schema.schema,
obsAnnotations: expect.any(Array),
varAnnotations: expect.any(Array),
obsNameToIndexMap: expect.any(Object),
varNameToIndexMap: expect.any(Object),
obsLayout: expect.objectContaining({
X: expect.any(Float32Array),
Y: expect.any(Float32Array)
}),
varDataCache: expect.any(Object)
})
);
expect(universe.obsAnnotations).toHaveLength(nObs);
expect(_.keys(universe.obsNameToIndexMap)).toHaveLength(nObs);
expect(universe.obsLayout.X).toHaveLength(nObs);
expect(universe.obsLayout.Y).toHaveLength(nObs);
expect(universe.varAnnotations).toHaveLength(nVar);
expect(_.keys(universe.varNameToIndexMap)).toHaveLength(nVar);
});
});
describe("convertExpressionRESTv02ToObject", () => {
/*
test convertExpressionRESTv02ToObject
convertExpressionRESTv02ToObject(
universe,
response) --> { geneName: Float32Array, geneName: Float32Array, ... }
reponse is a /data/obs response:
{
var: [ varIndices fetched ],
obs: [
[ obsIndex, evalue, ... ],
...
]
}
*/
test("create from response data", () => {
const universe = Universe.createUniverseFromRestV02Response(
REST.config,
REST.schema,
REST.annotationsObs,
REST.annotationsVar,
REST.layoutObs
);
const expression = Universe.convertExpressionRESTv02ToObject(
universe,
REST.dataObs
);
/* Check that the expected keys are present */
const expectedGeneNames = _.map(
REST.dataObs.var,
v => REST.annotationsVar.data[v][5]
);
expect(Object.keys(expression)).toEqual(
expect.arrayContaining(expectedGeneNames)
);
const expectedExpressionValues = _.map(
_.unzip(REST.dataObs.obs),
a => new Float32Array(a)
);
_.forEach(REST.dataObs.var, (varIdx, idx) => {
const varName = universe.varAnnotations[varIdx].name;
expect(varName).toBeDefined();
expect(varIdx).toBe(universe.varNameToIndexMap[varName]);
expect(expression[varName]).toEqual(expectedExpressionValues[idx + 1]);
});
});
});
@@ -1,255 +0,0 @@
import _ from "lodash";
import * as Universe from "../../../src/util/stateManager/universe";
import * as World from "../../../src/util/stateManager/world";
import Crossfilter from "../../../src/util/typedCrossfilter";
import * as REST from "./sampleResponses";
import {
obsAnnoDimensionName,
layoutDimensionName
} from "../../../src/util/nameCreators";
import * as kvCache from "../../../src/util/stateManager/keyvalcache";
/*
Helper - creates universe, world, corssfilter and dimensionMap from
the default REST test response.
*/
const defaultBigBang = () => {
/* create unverse, world, crossfilter and dimensionMap */
/* create universe */
const universe = Universe.createUniverseFromRestV02Response(
REST.config,
REST.schema,
REST.annotationsObs,
REST.annotationsVar,
REST.layoutObs
);
/* create world */
const world = World.createWorldFromEntireUniverse(universe);
/* create crossfilter */
const crossfilter = Crossfilter(world.obsAnnotations);
/* create dimension map */
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
return {
universe,
world,
crossfilter,
dimensionMap
};
};
describe("createWorldFromEntireUniverse", () => {
test("create from REST sample", () => {
const universe = Universe.createUniverseFromRestV02Response(
REST.config,
REST.schema,
REST.annotationsObs,
REST.annotationsVar,
REST.layoutObs
);
expect(universe).toBeDefined();
const world = World.createWorldFromEntireUniverse(universe);
expect(world).toBeDefined();
expect(world).toMatchObject(
expect.objectContaining({
api: "0.2",
nObs: universe.nObs,
nVar: universe.nVar,
schema: universe.schema,
obsAnnotations: universe.obsAnnotations,
varAnnotations: universe.varAnnotations,
obsLayout: universe.obsLayout,
summary: expect.objectContaining({
obs: _(REST.schema.schema.annotations.obs)
.filter(v => v.name !== "name")
.keyBy("name")
.mapValues(() => expect.any(Object))
.value(),
var: _(REST.schema.schema.annotations.var)
.filter(v => v.name !== "name")
.keyBy("name")
.mapValues(() => expect.any(Object))
.value()
}),
varDataCache: expect.any(Object),
obsIndex: null, // null indicating full universe
obsBackIndex: null
})
);
});
});
describe("createWorldFromCurrentSelection", () => {
test("create from REST sample", () => {
const {
universe,
world: originalWorld,
crossfilter,
dimensionMap
} = defaultBigBang();
/* mock a selection */
dimensionMap[obsAnnoDimensionName("field1")].filterRange([0, 5]);
dimensionMap[obsAnnoDimensionName("field3")].filterExact(false);
/* create the world from the selection */
const world = World.createWorldFromCurrentSelection(
universe,
originalWorld,
crossfilter
);
expect(world).toBeDefined();
expect(world.nObs).toEqual(crossfilter.countFiltered());
/*
calculate expected values and match against result
*/
/* matchFilter must match the dimension filters above */
const matchFilter = val => val.field1 >= 0 && val.field1 < 5 && !val.field3;
const universeIndices = _()
.range(universe.nObs)
.filter(idx => matchFilter(universe.obsAnnotations[idx]))
.value();
const expected = {
nObs: universeIndices.length,
obsAnnotations: _.map(universeIndices, i => universe.obsAnnotations[i]),
obsLayout: {
X: new Float32Array(
_.map(universeIndices, i => universe.obsLayout.X[i])
),
Y: new Float32Array(
_.map(universeIndices, i => universe.obsLayout.Y[i])
)
},
obsBackIndex: _.transform(
universeIndices,
(result, univIdx, worldIdx) => {
result[univIdx] = worldIdx;
},
new Uint32Array(universe.nObs).fill(-1)
),
obsIndex: new Uint32Array(universeIndices)
};
expect(world).toMatchObject(
expect.objectContaining({
api: "0.2",
nObs: expected.nObs,
nVar: universe.nVar,
schema: universe.schema,
obsAnnotations: expected.obsAnnotations,
varAnnotations: universe.varAnnotations,
obsLayout: expected.obsLayout,
summary: {
obs: expect.any(Object) /* we could do better! */,
var: expect.any(Object) /* we could do better! */
},
varDataCache: expect.any(Object),
obsIndex: expected.obsIndex,
obsBackIndex: expected.obsBackIndex
})
);
});
});
describe("createObsDimensionMap", () => {
test("when universe eq world", () => {
/*
check for:
- creates a dimension for all obsAnnotations, PLUS X/Y layout
- check that dimension typing is sane
*/
const { dimensionMap } = defaultBigBang();
const schemaByObsName = _.keyBy(REST.schema.schema.annotations.obs, "name");
expect(dimensionMap).toBeDefined();
REST.annotationsObs.names.forEach(name => {
const dim = dimensionMap[obsAnnoDimensionName(name)];
if (name === "name") {
expect(dim).toBeUndefined();
} else {
const { type } = schemaByObsName[name];
if (type === "string" || type === "boolean" || type === "categorical") {
expect(dim).toBeInstanceOf(Crossfilter.EnumDimension);
} else {
expect(dim).toBeInstanceOf(Crossfilter.ScalarDimension);
}
}
});
expect(dimensionMap[layoutDimensionName("X")]).toBeInstanceOf(
Crossfilter.ScalarDimension
);
expect(dimensionMap[layoutDimensionName("Y")]).toBeInstanceOf(
Crossfilter.ScalarDimension
);
});
});
describe("subsetVarData", () => {
test("when world eq universe", () => {
const { universe, world } = defaultBigBang();
/* create a mock varData array for subsetting */
const sourceVarData = new Float32Array(universe.nObs);
/* expect literally the same object back */
const result = World.subsetVarData(world, universe, sourceVarData);
expect(result).toBe(sourceVarData);
});
test("when world neq universe", () => {
const { universe, world, crossfilter, dimensionMap } = defaultBigBang();
/* create a mock varData array for subsetting */
const sourceVarData = Float32Array.from(_.range(universe.nObs));
/* mock a selection */
dimensionMap[obsAnnoDimensionName("field1")].filterRange([0, 5]);
dimensionMap[obsAnnoDimensionName("field3")].filterExact(false);
/* create the world from the selection */
const newWorld = World.createWorldFromCurrentSelection(
universe,
world,
crossfilter
);
expect(newWorld.obsIndex).toMatchObject(new Uint32Array([0, 2]));
/* expect a subset */
const result = World.subsetVarData(newWorld, universe, sourceVarData);
expect(result).not.toBe(sourceVarData);
expect(result).toHaveLength(newWorld.nObs);
/* check that we have expected source var content */
expect(result).toMatchObject(new Float32Array([0, 2]));
});
});
describe("createVarDimension", () => {
/* create default universe */
const { world, crossfilter } = defaultBigBang();
/* create a mock var data cache */
const varDataCache = kvCache.set(
kvCache.create(),
"GENE",
Float32Array.from(_.range(world.nObs))
);
const result = World.createVarDimension(
world,
varDataCache,
crossfilter,
"GENE"
);
expect(result).toBeInstanceOf(Crossfilter.ScalarDimension);
});
describe("worldEqUniverse", () => {
const { universe, world } = defaultBigBang();
const result = World.worldEqUniverse(world, universe);
expect(result).toBe(true);
});
@@ -1,5 +1,3 @@
// jshint esversion: 6
import BitArray from "../../../src/util/typedCrossfilter/bitArray"; import BitArray from "../../../src/util/typedCrossfilter/bitArray";
const defaultTestLength = 8; const defaultTestLength = 8;
@@ -118,16 +116,16 @@ describe("selectionCount", () => {
const dim2 = ba.allocDimension(); const dim2 = ba.allocDimension();
expect(dim2).toBeDefined(); expect(dim2).toBeDefined();
expect(ba.selectionCount).toEqual(0); expect(ba.selectionCount()).toEqual(0);
ba.selectAll(dim1); ba.selectAll(dim1);
expect(ba.selectionCount).toEqual(0); expect(ba.selectionCount()).toEqual(0);
ba.selectAll(dim2); ba.selectAll(dim2);
expect(ba.selectionCount).toEqual(defaultTestLength); expect(ba.selectionCount()).toEqual(defaultTestLength);
for (let i = 0; i < defaultTestLength; i += 1) { for (let i = 0; i < defaultTestLength; i += 1) {
ba.deselectOne(dim1, i); ba.deselectOne(dim1, i);
expect(ba.selectionCount).toEqual(defaultTestLength - i - 1); expect(ba.selectionCount()).toEqual(defaultTestLength - i - 1);
expect(ba.selectionCount).toEqual(ba.countAllOnes()); expect(ba.selectionCount()).toEqual(ba.countAllOnes());
} }
ba.freeDimension(dim1); ba.freeDimension(dim1);
@@ -183,3 +181,24 @@ describe("fillBySelection", () => {
expect(arr).toEqual(truth); expect(arr).toEqual(truth);
}); });
}); });
describe("wide bitarray", () => {
test.each([9, 30, 31, 32, 33, 54, 63, 64, 65, 127, 128, 129])(
"more than %d dimensions",
(ndim) => {
/* ensure we move across the uint boundary correctly */
const ba = new BitArray(defaultTestLength);
expect(ba).toBeDefined();
for (let i = 0; i < ndim; i += 1) {
expect(ba.allocDimension()).toEqual(i);
}
ba.freeDimension(0);
expect(ba.allocDimension()).toEqual(0);
ba.freeDimension(ndim - 1);
expect(ba.allocDimension()).toEqual(ndim - 1);
}
);
});
@@ -0,0 +1,499 @@
import filter from "lodash.filter";
import zip from "lodash.zip";
import Crossfilter from "../../../src/util/typedCrossfilter";
const someData = [
{
date: "2011-11-14T16:17:54Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001"],
coords: [0, 0],
nonFinite: 0.0,
},
{
date: "2011-11-14T16:20:19Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001", "005"],
coords: [0.4, 0.4],
nonFinite: Number.NaN,
},
{
date: "2011-11-14T16:28:54Z",
quantity: 1,
total: 300,
tip: 200,
type: "visa",
productIDs: ["004", "005"],
coords: [0.3, 0.1],
nonFinite: Number.POSITIVE_INFINITY,
},
{
date: "2011-11-14T16:30:43Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002"],
coords: [0.392, 0.1],
nonFinite: Number.NEGATIVE_INFINITY,
},
{
date: "2011-11-14T16:48:46Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["005"],
coords: [0.7, 0.0482],
nonFinite: 1.0,
},
{
date: "2011-11-14T16:53:41Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "004", "005"],
coords: [0.9999, 1.0],
nonFinite: Number.NaN,
},
{
date: "2011-11-14T16:54:06Z",
quantity: 1,
total: 100,
tip: 0,
type: "cash",
productIDs: ["001", "002", "003", "004", "005"],
coords: [0.384, 0.6938],
nonFinite: 99.0,
},
{
date: "2011-11-14T16:58:03Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001"],
coords: [0.4822, 0.482],
nonFinite: Number.NaN,
},
{
date: "2011-11-14T17:07:21Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["004", "005"],
coords: [0.2234, 0],
nonFinite: Number.NaN,
},
{
date: "2011-11-14T17:22:59Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002", "004", "005"],
coords: [0.382, 0.38485],
nonFinite: -1,
},
{
date: "2011-11-14T17:25:45Z",
quantity: 2,
total: 200,
tip: 0,
type: "cash",
productIDs: ["002"],
coords: [0.998, 0.8472],
nonFinite: 0.0,
},
{
date: "2011-11-14T17:29:52Z",
quantity: 1,
total: 200,
tip: 100,
type: "visa",
productIDs: ["004"],
coords: [0.8273, 0.3384],
nonFinite: 0.0,
},
];
let payments = null;
beforeEach(() => {
payments = new Crossfilter(someData);
});
describe("ImmutableTypedCrossfilter", () => {
test("create crossfilter", () => {
expect(payments).toBeDefined();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
const p = payments
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
.select("quantity", { mode: "all" });
expect(p).toBeDefined();
expect(p.all()).toEqual(someData);
expect(p.size()).toEqual(someData.length);
expect(p.isElementSelected(0)).toBeTruthy();
expect(p.countSelected()).toEqual(someData.length);
expect(p.allSelected()).toEqual(someData);
});
test("immutability", () => {
/*
the following should return a new crossfilter:
- addDimension()
- delDimension()
- select
*/
const p2 = payments.addDimension(
"quantity",
"scalar",
(i, data) => data[i].quantity,
Int32Array
);
expect(payments).not.toBe(p2);
const p3 = p2.select("quantity", { mode: "all" });
expect(p3).not.toBe(p2);
const p4 = p3.delDimension("quantity");
expect(p4).not.toBe(p3);
const p5 = p2.renameDimension("quantity", "Quantity");
expect(p5).not.toBe(p2);
});
test("select all and none", () => {
let p = payments
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
.addDimension("total", "scalar", (i, d) => d[i].total, Float32Array)
.addDimension("type", "enum", (i, d) => d[i].type);
expect(p).toBeDefined();
/* expect all records to be selected - default init state */
expect(p.allSelected()).toEqual(someData);
expect(p.countSelected()).toEqual(someData.length);
expect(p.allSelectedMask()).toEqual(
new Uint8Array(someData.length).fill(1)
);
expect(p.fillByIsSelected(new Uint8Array(someData.length), 99, 0)).toEqual(
new Uint8Array(someData.length).fill(99)
);
for (let i = 0; i < someData.length; i += 1) {
expect(p.isElementSelected(i)).toBeTruthy();
}
/* expect a selectAll on one dimension to change nothing */
p = p.select("tip", { mode: "all" });
expect(p.allSelected()).toEqual(someData);
/* ditto */
p = p.select("quantity", { mode: "all" });
expect(p.allSelected()).toEqual(someData);
/* select none on one dimension */
p = p.select("type", { mode: "none" });
expect(p.allSelected()).toEqual([]);
expect(p.countSelected()).toEqual(0);
expect(p.allSelectedMask()).toEqual(
new Uint8Array(someData.length).fill(0)
);
expect(p.fillByIsSelected(new Uint8Array(someData.length), 99, 0)).toEqual(
new Uint8Array(someData.length).fill(0)
);
for (let i = 0; i < someData.length; i += 1) {
expect(p.isElementSelected(i)).toBeFalsy();
}
p = p.select("quantity", { mode: "none" });
expect(p.allSelected()).toEqual([]);
// invert the first none; should have no effect because type is
// still not filtered.
p = p.select("quantity", { mode: "all" });
expect(p.allSelected()).toEqual([]);
/* select all of type; should select all records */
p = p.select("type", { mode: "all" });
expect(p.allSelected()).toEqual(someData);
});
describe("scalar dimension", () => {
let p;
beforeEach(() => {
p = payments
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
.addDimension("tip", "scalar", (i, d) => d[i].tip, Float32Array)
.select("tip", { mode: "all" });
});
/*
select modes: all, none, exact, range
*/
test("all", () => {
expect(p.select("quantity", { mode: "all" }).countSelected()).toEqual(
someData.length
);
});
test("none", () => {
expect(p.select("quantity", { mode: "none" }).countSelected()).toEqual(0);
});
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)
);
test("single value exact", () => {
expect(
p.select("quantity", { mode: "exact", values: 2 }).countSelected()
).toEqual(filter(someData, (d) => d.quantity === 2).length);
});
test.each([
[0, 1],
[1, 2],
[0, 99],
[99, 100000],
])("range %p", (lo, hi) =>
expect(
p.select("quantity", { mode: "range", lo, hi }).countSelected()
).toEqual(
filter(someData, (d) => d.quantity >= lo && d.quantity < hi).length
)
);
test("bad mode", () => {
expect(() => p.select("type", { mode: "bad mode" })).toThrow(Error);
});
});
describe("enum dimension", () => {
let p;
beforeEach(() => {
p = payments.addDimension("type", "enum", (i, d) => d[i].type);
});
test("all", () => {
expect(p.select("type", { mode: "all" }).countSelected()).toEqual(
someData.length
);
});
test("none", () => {
expect(p.select("type", { mode: "none" }).countSelected()).toEqual(0);
});
test.each([
[[]],
[["tab"]],
[["visa"]],
[["visa", "tab"]],
[["cash", "tab", "visa"]],
])("exact: %p", (v) =>
expect(
p.select("type", { mode: "exact", values: v }).countSelected()
).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);
});
test("range", () => {
expect(() => p.select("type", { mode: "range", lo: 0, hi: 9 })).toThrow(
Error
);
});
test("bad mode", () => {
expect(() => p.select("type", { mode: "bad mode" })).toThrow(Error);
});
});
describe("spatial dimension", () => {
let p;
beforeEach(() => {
const X = someData.map((r) => r.coords[0]);
const Y = someData.map((r) => r.coords[1]);
p = payments.addDimension("coords", "spatial", X, Y);
});
test("all", () => {
expect(p.select("coords", { mode: "all" }).countSelected()).toEqual(
someData.length
);
});
test("none", () => {
expect(p.select("coords", { mode: "none" }).countSelected()).toEqual(0);
});
test.each([
[0, 0, 1, 1],
[0, 0, 0.5, 0.5],
[0.5, 0.5, 1, 1],
])("within-rect %d %d %d %d", (minX, minY, maxX, maxY) => {
expect(
p
.select("coords", { mode: "within-rect", minX, minY, maxX, maxY })
.allSelected()
).toEqual(
filter(someData, (d) => {
const [x, y] = d.coords;
return minX <= x && x < maxX && minY <= y && y < maxY;
})
);
});
test.each([
[
[
[0, 0],
[0, 1],
[1, 1],
[1, 0],
],
[
true,
true,
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
],
],
[
[
[0, 0],
[0, 0.5],
[0.5, 0.5],
[0.5, 0],
],
[
true,
true,
true,
true,
false,
false,
false,
true,
true,
true,
false,
false,
],
],
])("within-polygon %p", (polygon, expected) => {
expect(
p.select("coords", { mode: "within-polygon", polygon }).allSelected()
).toEqual(
zip(someData, expected)
.filter((x) => x[1])
.map((x) => x[0])
);
});
});
describe("non-finite scalars", () => {
let p;
beforeEach(() => {
p = payments
.addDimension("quantity", "scalar", (i, d) => d[i].quantity, Int32Array)
.addDimension(
"nonFinite",
"scalar",
(i, d) => d[i].nonFinite,
Float32Array
)
.select("quantity", { mode: "all" });
});
test("all or none", () => {
expect(p.select("nonFinite", { mode: "all" }).countSelected()).toEqual(
someData.length
);
expect(p.select("nonFinite", { mode: "none" }).countSelected()).toEqual(
0
);
});
test("exact", () => {
expect(
p.select("nonFinite", { mode: "exact", values: [0] }).countSelected()
).toEqual(3);
expect(
p.select("nonFinite", { mode: "exact", values: [1] }).countSelected()
).toEqual(1);
expect(
p
.select("nonFinite", {
mode: "exact",
values: [Number.POSITIVE_INFINITY],
})
.countSelected()
).toEqual(1);
expect(
p
.select("nonFinite", {
mode: "exact",
values: [Number.NEGATIVE_INFINITY],
})
.countSelected()
).toEqual(1);
expect(
p
.select("nonFinite", { mode: "exact", values: [Number.NaN] })
.countSelected()
).toEqual(4);
expect(
p
.select("nonFinite", {
mode: "exact",
values: [Number.POSITIVE_INFINITY, 0, 1, 99],
})
.countSelected()
).toEqual(6);
});
test("range", () => {
expect(
p
.select("nonFinite", {
mode: "range",
lo: 0,
hi: Number.POSITIVE_INFINITY,
})
.countSelected()
).toEqual(5);
expect(
p
.select("nonFinite", {
mode: "range",
lo: 0,
hi: Number.NaN,
})
.countSelected()
).toEqual(6);
expect(
p
.select("nonFinite", {
mode: "range",
lo: Number.NEGATIVE_INFINITY,
hi: Number.POSITIVE_INFINITY,
})
.countSelected()
).toEqual(7);
});
});
});
@@ -1,5 +1,3 @@
// jshint esversion: 6
// const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals"); // const PositiveIntervals = require("../../src/util/typedCrossfilter/positiveIntervals");
import PositiveIntervals from "../../../src/util/typedCrossfilter/positiveIntervals"; import PositiveIntervals from "../../../src/util/typedCrossfilter/positiveIntervals";
@@ -10,15 +8,30 @@ describe("canonicalize", () => {
test("simple, already correct", () => { test("simple, already correct", () => {
expect(PositiveIntervals.canonicalize([[0, 1]])).toEqual([[0, 1]]); expect(PositiveIntervals.canonicalize([[0, 1]])).toEqual([[0, 1]]);
expect(PositiveIntervals.canonicalize([[0, 1], [2, 3]])).toEqual([ expect(
PositiveIntervals.canonicalize([
[0, 1],
[2, 3],
])
).toEqual([
[0, 1], [0, 1],
[2, 3] [2, 3],
]); ]);
}); });
test("non-canonical, need to be canonicalized", () => { test("non-canonical, need to be canonicalized", () => {
expect(PositiveIntervals.canonicalize([[0, 1], [1, 2]])).toEqual([[0, 2]]); expect(
expect(PositiveIntervals.canonicalize([[1, 2], [2, 3]])).toEqual([[1, 3]]); PositiveIntervals.canonicalize([
[0, 1],
[1, 2],
])
).toEqual([[0, 2]]);
expect(
PositiveIntervals.canonicalize([
[1, 2],
[2, 3],
])
).toEqual([[1, 3]]);
}); });
}); });
@@ -26,14 +39,30 @@ describe("union", () => {
test("empty range", () => { test("empty range", () => {
expect(PositiveIntervals.union([], [])).toEqual([]); expect(PositiveIntervals.union([], [])).toEqual([]);
expect(PositiveIntervals.union([], [[1, 2]])).toEqual([[1, 2]]); expect(PositiveIntervals.union([], [[1, 2]])).toEqual([[1, 2]]);
expect(PositiveIntervals.union([], [[1, 2], [3, 4]])).toEqual([ expect(
PositiveIntervals.union(
[],
[
[1, 2],
[3, 4],
]
)
).toEqual([
[1, 2], [1, 2],
[3, 4] [3, 4],
]); ]);
expect(PositiveIntervals.union([[3, 4]], [])).toEqual([[3, 4]]); expect(PositiveIntervals.union([[3, 4]], [])).toEqual([[3, 4]]);
expect(PositiveIntervals.union([[1, 2], [3, 4]], [])).toEqual([ expect(
PositiveIntervals.union(
[
[1, 2],
[3, 4],
],
[]
)
).toEqual([
[1, 2], [1, 2],
[3, 4] [3, 4],
]); ]);
expect(PositiveIntervals.union([[3, 3]], [])).toEqual([[3, 3]]); expect(PositiveIntervals.union([[3, 3]], [])).toEqual([[3, 3]]);
expect(PositiveIntervals.union([], [[3, 3]])).toEqual([[3, 3]]); expect(PositiveIntervals.union([], [[3, 3]])).toEqual([[3, 3]]);
@@ -44,17 +73,37 @@ describe("union", () => {
expect(PositiveIntervals.union([[2, 3]], [[1, 2]])).toEqual([[1, 3]]); expect(PositiveIntervals.union([[2, 3]], [[1, 2]])).toEqual([[1, 3]]);
expect(PositiveIntervals.union([[1, 2]], [[3, 4]])).toEqual([ expect(PositiveIntervals.union([[1, 2]], [[3, 4]])).toEqual([
[1, 2], [1, 2],
[3, 4] [3, 4],
]); ]);
expect( expect(
PositiveIntervals.union([[1, 2], [3, 4]], [[6, 7], [19, 40]]) PositiveIntervals.union(
).toEqual([[1, 2], [3, 4], [6, 7], [19, 40]]); [
expect(PositiveIntervals.union([[1, 4]], [[1, 1], [3, 4]])).toEqual([ [1, 2],
[1, 4] [3, 4],
],
[
[6, 7],
[19, 40],
]
)
).toEqual([
[1, 2],
[3, 4],
[6, 7],
[19, 40],
]); ]);
expect(
PositiveIntervals.union(
[[1, 4]],
[
[1, 1],
[3, 4],
]
)
).toEqual([[1, 4]]);
expect(PositiveIntervals.union([[3, 3]], [[4, 4]])).toEqual([ expect(PositiveIntervals.union([[3, 3]], [[4, 4]])).toEqual([
[3, 3], [3, 3],
[4, 4] [4, 4],
]); ]);
}); });
}); });
@@ -70,34 +119,43 @@ describe("intersection", () => {
expect(PositiveIntervals.intersection([[1, 2]], [[2, 3]])).toEqual([]); expect(PositiveIntervals.intersection([[1, 2]], [[2, 3]])).toEqual([]);
expect(PositiveIntervals.intersection([[2, 3]], [[1, 2]])).toEqual([]); expect(PositiveIntervals.intersection([[2, 3]], [[1, 2]])).toEqual([]);
expect(PositiveIntervals.intersection([[1, 10]], [[1, 10]])).toEqual([ expect(PositiveIntervals.intersection([[1, 10]], [[1, 10]])).toEqual([
[1, 10] [1, 10],
]); ]);
expect(PositiveIntervals.intersection([[1, 10]], [[2, 8]])).toEqual([ expect(PositiveIntervals.intersection([[1, 10]], [[2, 8]])).toEqual([
[2, 8] [2, 8],
]); ]);
expect(PositiveIntervals.intersection([[2, 8]], [[1, 10]])).toEqual([ expect(PositiveIntervals.intersection([[2, 8]], [[1, 10]])).toEqual([
[2, 8] [2, 8],
]); ]);
expect(PositiveIntervals.intersection([[1, 10]], [[2, 12]])).toEqual([ expect(PositiveIntervals.intersection([[1, 10]], [[2, 12]])).toEqual([
[2, 10] [2, 10],
]); ]);
expect(PositiveIntervals.intersection([[2, 12]], [[1, 10]])).toEqual([ expect(PositiveIntervals.intersection([[2, 12]], [[1, 10]])).toEqual([
[2, 10] [2, 10],
]); ]);
expect(PositiveIntervals.intersection([[1, 10]], [[1, 8]])).toEqual([ expect(PositiveIntervals.intersection([[1, 10]], [[1, 8]])).toEqual([
[1, 8] [1, 8],
]); ]);
expect(PositiveIntervals.intersection([[1, 8]], [[1, 10]])).toEqual([ expect(PositiveIntervals.intersection([[1, 8]], [[1, 10]])).toEqual([
[1, 8] [1, 8],
]);
expect(
PositiveIntervals.intersection(
[[1, 10]],
[
[1, 2],
[6, 9],
]
)
).toEqual([
[1, 2],
[6, 9],
]); ]);
expect(PositiveIntervals.intersection([[1, 10]], [[1, 2], [6, 9]])).toEqual(
[[1, 2], [6, 9]]
);
expect(PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])).toEqual( expect(PositiveIntervals.intersection([[0, 2638]], [[1363, 2638]])).toEqual(
[[1363, 2638]] [[1363, 2638]]
); );
expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([ expect(PositiveIntervals.intersection([[1, 2]], [[1, 2]])).toEqual([
[1, 2] [1, 2],
]); ]);
}); });
}); });
@@ -110,32 +168,66 @@ describe("difference", () => {
}); });
test("simple", () => { test("simple", () => {
expect(PositiveIntervals.difference([[1, 2], [3, 4]], [])).toEqual([ expect(
PositiveIntervals.difference(
[
[1, 2],
[3, 4],
],
[]
)
).toEqual([
[1, 2], [1, 2],
[3, 4] [3, 4],
]);
expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[5, 10]])).toEqual([
[1, 2],
[3, 5]
]);
expect(PositiveIntervals.difference([[1, 2], [3, 10]], [[0, 5]])).toEqual([
[5, 10]
]); ]);
expect( expect(
PositiveIntervals.difference([[0, 2638]], [[0, 1363], [2055, 2638]]) PositiveIntervals.difference(
[
[1, 2],
[3, 10],
],
[[5, 10]]
)
).toEqual([
[1, 2],
[3, 5],
]);
expect(
PositiveIntervals.difference(
[
[1, 2],
[3, 10],
],
[[0, 5]]
)
).toEqual([[5, 10]]);
expect(
PositiveIntervals.difference(
[[0, 2638]],
[
[0, 1363],
[2055, 2638],
]
)
).toEqual([[1363, 2055]]); ).toEqual([[1363, 2055]]);
expect( expect(
PositiveIntervals.difference([[0, 1363], [2055, 2638]], [[0, 2638]]) PositiveIntervals.difference(
[
[0, 1363],
[2055, 2638],
],
[[0, 2638]]
)
).toEqual([]); ).toEqual([]);
expect(PositiveIntervals.difference([[0, 10]], [[0, 1]])).toEqual([ expect(PositiveIntervals.difference([[0, 10]], [[0, 1]])).toEqual([
[1, 10] [1, 10],
]); ]);
expect(PositiveIntervals.difference([[0, 10]], [[1, 2]])).toEqual([ expect(PositiveIntervals.difference([[0, 10]], [[1, 2]])).toEqual([
[0, 1], [0, 1],
[2, 10] [2, 10],
]); ]);
expect(PositiveIntervals.difference([[0, 10]], [[9, 10]])).toEqual([ expect(PositiveIntervals.difference([[0, 10]], [[9, 10]])).toEqual([
[0, 9] [0, 9],
]); ]);
}); });
}); });
@@ -1,4 +1,19 @@
import { sort, sortIndex } from "../../../src/util/typedCrossfilter/sort"; import {
sortArray,
sortIndex,
lowerBound,
} from "../../../src/util/typedCrossfilter/sort";
/*
Sort tests should keep in mind that there are separate code
paths for:
- small vs. large arrays (insertionsort only)
- float-only typed arrays vs. other array types (non-finite handling)
- indexed vs. direct sort
*/
const pInf = Number.POSITIVE_INFINITY;
const nInf = Number.NEGATIVE_INFINITY;
function fillRange(arr, start = 0) { function fillRange(arr, start = 0) {
const larr = arr; const larr = arr;
@@ -15,42 +30,225 @@ function fillRand(arr) {
return arr; return arr;
} }
describe("sort", () => { describe("sortArray", () => {
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map(Type => describe("JS vals", () => {
test(Type.name, () => { [
expect(sort(Type.from([6, 5, 4, 3, 2, 1, 0]))).toMatchObject( [true, false],
Type.from([0, 1, 2, 3, 4, 5, 6]) ["a", "b", "0", "1"],
); [0, "a", true, null, undefined, 3.1415],
expect(sort(Type.from([6, 5, 4, 3, 2, 1]))).toMatchObject( fillRand(new Array(1000)),
Type.from([1, 2, 3, 4, 5, 6]) ["a", NaN, null, pInf],
); ].map((val, idx) =>
test(`JS vals ${idx}`, () => {
expect(sortArray(val)).toMatchObject(val.sort());
})
);
});
const source = fillRand(new Type(1000)); describe("finite numbers", () => {
expect(sort(Type.from(source))).toMatchObject(Type.from(source).sort()); [Array, Float32Array, Uint32Array, Int32Array, Float64Array].map((Type) =>
}) test(Type.name, () => {
); expect(sortArray(Type.from([6, 5, 4, 3, 2, 1, 0]))).toMatchObject(
Type.from([0, 1, 2, 3, 4, 5, 6])
);
expect(sortArray(Type.from([6, 5, 4, 3, 2, 1]))).toMatchObject(
Type.from([1, 2, 3, 4, 5, 6])
);
const source = fillRand(new Type(1000));
expect(sortArray(Type.from(source))).toMatchObject(
Type.from(source).sort()
);
})
);
});
describe("non-finite numbers", () => {
test("infinity", () => {
expect(sortArray(new Float32Array([pInf, nInf, 0, 1, 2]))).toMatchObject(
new Float32Array([nInf, 0, 1, 2, pInf])
);
expect(
sortArray(new Float32Array([pInf, nInf, pInf, nInf]))
).toMatchObject(new Float32Array([nInf, nInf, pInf, pInf]));
expect(
sortArray(new Float32Array([pInf, nInf, pInf, nInf, pInf]))
).toMatchObject(new Float32Array([nInf, nInf, pInf, pInf, pInf]));
expect(
sortArray(
new Float32Array(100).fill(Infinity, 0, 50).fill(-Infinity, 50, 100)
)
).toMatchObject(
new Float32Array(100).fill(-Infinity, 0, 50).fill(Infinity, 50, 100)
);
});
test("NaN", () => {
expect(sortArray(new Float64Array([NaN, 2, 1, 0]))).toMatchObject(
new Float64Array([0, 1, 2, NaN])
);
expect(sortArray(new Float32Array([NaN, 2, 1, 0]))).toMatchObject(
new Float32Array([0, 1, 2, NaN])
);
expect(sortArray(new Float32Array([NaN, 2, NaN, 1, 0]))).toMatchObject(
new Float32Array([0, 1, 2, NaN, NaN])
);
expect(sortArray(new Float32Array([NaN, 2, 1, NaN, 0]))).toMatchObject(
new Float32Array([0, 1, 2, NaN, NaN])
);
expect(
sortArray(fillRange(new Float32Array(100)).fill(NaN, 0, 10))
).toMatchObject(fillRange(new Float32Array(100), 10).fill(NaN, 90, 100));
});
test("mixed numbers", () => {
expect(
sortArray(new Float32Array([NaN, pInf, nInf, NaN, NaN]))
).toMatchObject(new Float32Array([nInf, pInf, NaN, NaN, NaN]));
expect(
sortArray(new Float32Array([NaN, pInf, nInf, NaN, 1, NaN, 2]))
).toMatchObject(new Float32Array([nInf, 1, 2, pInf, NaN, NaN, NaN]));
expect(
sortArray(new Float32Array([NaN, pInf, nInf, 0, 1, NaN, 2]))
).toMatchObject(new Float32Array([nInf, 0, 1, 2, pInf, NaN, NaN]));
expect(
sortArray(
fillRange(new Float32Array(100))
.fill(NaN, 0, 10)
.fill(Infinity, 10, 20)
)
).toMatchObject(
fillRange(new Float32Array(100), 20)
.fill(Infinity, 80, 90)
.fill(NaN, 90, 100)
);
});
});
}); });
describe("sortIndex", () => { describe("sortIndex", () => {
[Array, Float32Array, Uint32Array, Int32Array, Float64Array].map(Type => describe("finite numbers", () => {
test(Type.name, () => { [Array, Float32Array, Uint32Array, Int32Array, Float64Array].map((Type) =>
const source1 = Type.from([6, 5, 4, 3, 2, 1, 0]); test(Type.name, () => {
const source1 = Type.from([6, 5, 4, 3, 2, 1, 0]);
const index1 = fillRange(new Uint32Array(source1.length));
expect(sortIndex(index1, source1)).toMatchObject(
index1.sort((a, b) => source1[a] - source1[b])
);
const source2 = Type.from([6, 5, 4, 3, 2, 1]);
const index2 = fillRange(new Uint32Array(source2.length));
expect(sortIndex(index2, source2)).toMatchObject(
index2.sort((a, b) => source1[a] - source1[b])
);
const source3 = fillRand(new Type(1000));
const index3 = fillRange(new Uint32Array(source3.length));
expect(sortIndex(index3, source3)).toMatchObject(
index3.sort((a, b) => source1[a] - source1[b])
);
})
);
});
describe("non-finite numbers", () => {
test("mixed numbers", () => {
const source1 = new Float32Array([NaN, pInf, nInf, NaN, 1, NaN, 2]);
const index1 = fillRange(new Uint32Array(source1.length)); const index1 = fillRange(new Uint32Array(source1.length));
expect(sortIndex(index1, source1)).toMatchObject( expect(sortIndex(index1, source1)).toMatchObject(
index1.sort((a, b) => source1[a] - source1[b]) new Uint32Array([2, 4, 6, 1, 0, 3, 5])
); );
const source2 = Type.from([6, 5, 4, 3, 2, 1]); const source2 = new Float32Array([NaN, pInf, nInf, 0, 1, NaN, 2]);
const index2 = fillRange(new Uint32Array(source2.length)); const index2 = fillRange(new Uint32Array(source2.length));
expect(sortIndex(index2, source2)).toMatchObject( expect(sortIndex(index2, source2)).toMatchObject(
index2.sort((a, b) => source1[a] - source1[b]) new Uint32Array([2, 3, 4, 6, 1, 0, 5])
); );
});
});
});
const source3 = fillRand(new Type(1000)); describe("lowerBound", () => {
const index3 = fillRange(new Uint32Array(source3.length)); test("non-float path", () => {
expect(sortIndex(index3, source3)).toMatchObject( expect(lowerBound([], 0, 0, 0)).toEqual(0);
index3.sort((a, b) => source1[a] - source1[b])
); expect(lowerBound([0, 1, 2, 3], -1, 0, 4)).toEqual(0);
}) expect(lowerBound([0, 1, 2, 3], 0, 0, 4)).toEqual(0);
); expect(lowerBound([0, 1, 2, 3], 1, 0, 4)).toEqual(1);
expect(lowerBound([0, 1, 2, 3], 3, 0, 4)).toEqual(3);
expect(lowerBound([0, 1, 2, 3], 4, 0, 4)).toEqual(4);
expect(lowerBound([0, 1, 2, 3], 4, 0, 3)).toEqual(3);
expect(lowerBound([0, 1, 2, 3, 4], -1, 0, 5)).toEqual(0);
expect(lowerBound([0, 1, 2, 3, 4], 0, 0, 5)).toEqual(0);
expect(lowerBound([0, 1, 2, 3, 4], 2, 0, 5)).toEqual(2);
expect(lowerBound([0, 1, 2, 3, 4], 4, 0, 5)).toEqual(4);
expect(lowerBound([0, 1, 2, 3, 4], 5, 0, 5)).toEqual(5);
expect(lowerBound([0, 2, 4, 6, 8], 5, 0, 5)).toEqual(3);
expect(lowerBound([0, 2, 2, 2, 8], 5, 0, 5)).toEqual(4);
expect(lowerBound([0, 1, 2, 3, 4, 5, 6, 7, 8], 3, 2, 4)).toEqual(3);
expect(lowerBound([0, 1, 2, 3, 4, 5, 6, 7, 8], 99, 2, 4)).toEqual(4);
});
test("float path, finites", () => {
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 1, 0, 4)).toEqual(1);
expect(lowerBound(new Float32Array([]), 0, 0, 0)).toEqual(0);
expect(lowerBound(new Float32Array([0, 1, 2, 3]), -1, 0, 4)).toEqual(0);
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 0, 0, 4)).toEqual(0);
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 1, 0, 4)).toEqual(1);
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 3, 0, 4)).toEqual(3);
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 4, 0, 4)).toEqual(4);
expect(lowerBound(new Float32Array([0, 1, 2, 3]), 4, 0, 3)).toEqual(3);
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), -1, 0, 5)).toEqual(0);
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 0, 0, 5)).toEqual(0);
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 2, 0, 5)).toEqual(2);
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 4, 0, 5)).toEqual(4);
expect(lowerBound(new Float32Array([0, 1, 2, 3, 4]), 5, 0, 5)).toEqual(5);
expect(lowerBound(new Float32Array([0, 2, 4, 6, 8]), 5, 0, 5)).toEqual(3);
expect(lowerBound(new Float32Array([0, 2, 2, 2, 8]), 5, 0, 5)).toEqual(4);
expect(
lowerBound(new Float32Array([0, 1, 2, 3, 4, 5, 6, 7, 8]), 3, 2, 4)
).toEqual(3);
expect(
lowerBound(new Float32Array([0, 1, 2, 3, 4, 5, 6, 7, 8]), 99, 2, 4)
).toEqual(4);
});
test("float path, non-finite", () => {
expect(
lowerBound(
new Float32Array([-Infinity, 0, 1, Infinity, NaN]),
-Infinity,
0,
5
)
).toEqual(0);
expect(
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), 0, 0, 5)
).toEqual(1);
expect(
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), 1, 0, 5)
).toEqual(2);
expect(
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), 2, 0, 5)
).toEqual(3);
expect(
lowerBound(
new Float32Array([-Infinity, 0, 1, Infinity, NaN]),
Infinity,
0,
5
)
).toEqual(3);
expect(
lowerBound(new Float32Array([-Infinity, 0, 1, Infinity, NaN]), NaN, 0, 5)
).toEqual(4);
});
}); });
@@ -1,462 +0,0 @@
// jshint esversion: 6
import _ from "lodash";
import crossfilter from "../../../src/util/typedCrossfilter";
const someData = [
{
date: "2011-11-14T16:17:54Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001"]
},
{
date: "2011-11-14T16:20:19Z",
quantity: 2,
total: 190,
tip: 100,
type: "tab",
productIDs: ["001", "005"]
},
{
date: "2011-11-14T16:28:54Z",
quantity: 1,
total: 300,
tip: 200,
type: "visa",
productIDs: ["004", "005"]
},
{
date: "2011-11-14T16:30:43Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002"]
},
{
date: "2011-11-14T16:48:46Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["005"]
},
{
date: "2011-11-14T16:53:41Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "004", "005"]
},
{
date: "2011-11-14T16:54:06Z",
quantity: 1,
total: 100,
tip: 0,
type: "cash",
productIDs: ["001", "002", "003", "004", "005"]
},
{
date: "2011-11-14T16:58:03Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001"]
},
{
date: "2011-11-14T17:07:21Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["004", "005"]
},
{
date: "2011-11-14T17:22:59Z",
quantity: 2,
total: 90,
tip: 0,
type: "tab",
productIDs: ["001", "002", "004", "005"]
},
{
date: "2011-11-14T17:25:45Z",
quantity: 2,
total: 200,
tip: 0,
type: "cash",
productIDs: ["002"]
},
{
date: "2011-11-14T17:29:52Z",
quantity: 1,
total: 200,
tip: 100,
type: "visa",
productIDs: ["004"]
}
];
function groupReduce(data, valueMap, valueReduce, valueInit) {
return _
.reduce(
data,
(acc, value) => {
const k = valueMap(value);
let r = _.find(acc, o => o.key === k);
if (!r) {
r = { key: k, value: valueInit() };
acc.push(r);
}
r.value = valueReduce(r.value, value);
return acc;
},
[]
)
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
}
function groupCount(data, map) {
return groupReduce(data, map, (p, v) => p + 1, () => 0);
}
function groupSum(data, map) {
return groupReduce(data, map, (p, v) => (p += map(v)), () => 0);
}
var payments = null;
beforeEach(() => {
payments = crossfilter(someData);
});
describe("typedCrossfilter", () => {
test("alloc and free", () => {
expect(payments).toBeDefined();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
const quantity = payments.dimension(r => r.quantity, Int32Array);
expect(quantity).toBeDefined();
expect(quantity.id()).toBeDefined();
quantity.dispose();
expect(payments.size()).toEqual(someData.length);
expect(payments.all()).toEqual(someData);
});
test("filterAll and filterNone", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
expect(quantity).toBeDefined();
expect(tip).toBeDefined();
expect(total).toBeDefined();
expect(type).toBeDefined();
// initially, all should be filtered
expect(payments.allFiltered()).toHaveLength(payments.size());
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// filterAll
tip.filterAll(); // should change nothing
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// ditto
total.filterAll();
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(someData.length);
// filterNone
type.filterNone();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
quantity.filterNone();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
// invert the first none; should have no effect because type is
// still not filtered
quantity.filterAll();
expect(payments.allFiltered()).toEqual([]);
expect(payments.countFiltered()).toEqual(0);
// filter all of type; should select all
type.filterAll();
expect(payments.allFiltered()).toEqual(payments.all());
expect(payments.countFiltered()).toEqual(payments.size());
});
test("filterExact", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
quantity.filterExact(1);
expect(payments.countFiltered()).toEqual(
_.countBy(someData, "quantity")[1]
);
expect(payments.allFiltered()).toEqual(_.filter(someData, { quantity: 1 }));
tip.filterExact(0);
expect(payments.allFiltered()).toEqual(
_.filter(someData, { tip: 0, quantity: 1 })
);
type.filterExact("cash");
expect(payments.allFiltered()).toEqual(
_.filter(someData, { tip: 0, quantity: 1, type: "cash" })
);
});
test("filterRange", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
tip.filterRange([0, 91]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 0 && r.tip < 91)
.value()
);
tip.filterRange([0, 90]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 0 && r.tip < 90)
.value()
);
tip.filterRange([1, 90]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.tip >= 1 && r.tip < 91)
.value()
);
});
test("filterEnum", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
type.filterEnum(["tab", "cash"]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.type === "cash" || r.type === "tab")
.value()
);
tip.filterEnum([0, 100]);
expect(payments.allFiltered()).toEqual(
_(someData)
.filter(r => r.type === "cash" || r.type === "tab")
.filter(r => r.tip === 0 || r.tip === 100)
.value()
);
});
test("more than 32 dimensions", () => {
expect(payments).toBeDefined();
const quantity = payments.dimension(r => r.quantity, Int32Array);
const tip = payments.dimension(r => r.tip, Float32Array);
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
// Create a bunch of fake dimensions to ensure we can handle > 32
let dimMap = {};
for (let i = 0; i < 65; i++) {
dimMap[i] = payments.dimension(r => Math.random(), Float32Array);
expect(dimMap[i]).toBeDefined();
expect(dimMap[i].id()).toBeDefined();
}
// everything should start as selected/filtered
expect(payments.countFiltered()).toEqual(someData.length);
dimMap[0].filterAll();
dimMap[64].filterAll();
expect(payments.countFiltered()).toEqual(someData.length);
dimMap[33].filterNone();
expect(payments.allFiltered()).toEqual([]);
dimMap[33].filterAll();
expect(payments.allFiltered()).toEqual(someData);
});
test("group, default mapping, default reducer, no filter", () => {
expect(payments).toBeDefined();
var quantity = payments.dimension(r => r.quantity, Int32Array);
var tip = payments.dimension(r => r.tip, Int32Array);
var type = payments.dimension(r => r.type, "enum");
var total = payments.dimension(r => r.total, Int32Array);
_.each(
{
tip: tip.group(r => r),
type: type.group(),
total: total.group(),
quantity: quantity.group()
},
(grp, k) => {
const whatWeExpect = groupCount(someData, v => v[k]);
expect(grp.all()).toEqual(whatWeExpect);
expect(grp.size()).toEqual(whatWeExpect.length);
expect(grp.dispose()).toEqual(grp);
}
);
});
test("group, custom map, default reducer, no filters", () => {
expect(payments).toBeDefined();
// custom mapping in groups only works for scalar types. Enums do not
// currently implement it.
const tip = payments.dimension(r => r.tip, Int32Array);
const totalX10 = payments.dimension(r => r.total * 10, Int32Array);
const type = payments.dimension(r => r.type, "enum");
const paymentsByTip_A = tip.group();
const paymentsByTip_B = tip.group(r => 10 * r);
const paymentsByType = type.group(); // identity only
const paymentsByTotalX10_A = totalX10.group();
const paymentsByTotalX10_B = totalX10.group(r => r / 10);
expect(paymentsByTip_A.all()).toEqual(groupCount(someData, v => v.tip));
expect(paymentsByTip_B.all()).toEqual(
groupCount(someData, v => 10 * v.tip)
);
expect(paymentsByType.all()).toEqual(groupCount(someData, v => v.type));
expect(paymentsByTotalX10_A.all()).toEqual(
groupCount(someData, v => 10 * v.total)
);
expect(paymentsByTotalX10_B.all()).toEqual(
groupCount(someData, v => (10 * v.total) / 10)
);
for (let i of [
paymentsByTip_A,
paymentsByTip_B,
paymentsByType,
paymentsByTotalX10_A,
paymentsByTotalX10_B,
tip,
totalX10,
type
]) {
expect(i.dispose()).toEqual(i);
}
});
test("group, default map, custom reducer, no filters", () => {
expect(payments).toBeDefined();
const total = payments.dimension(r => r.total, Float32Array);
const type = payments.dimension(r => r.type, "enum");
const paymentsByTotal = total.group();
const paymentsByType = type.group();
// reduceCount
expect(paymentsByTotal.reduceCount()).toEqual(paymentsByTotal);
expect(paymentsByTotal.all()).toEqual(groupCount(someData, v => v.total));
// reduceSum
expect(paymentsByTotal.reduceSum(v => v.total)).toEqual(paymentsByTotal);
expect(paymentsByTotal.all()).toEqual(groupSum(someData, v => v.total));
// use custom reducers (my reducers) - count by three, init 1
expect(
paymentsByTotal.reduce((p, v) => (p += 3), (p, v) => (p -= 3), () => 1)
).toEqual(paymentsByTotal);
expect(paymentsByTotal.all()).toEqual(
groupReduce(someData, v => v.total, (p, v) => p + 3, () => 1)
);
for (let i of [paymentsByTotal, paymentsByType, type]) {
expect(i.dispose()).toEqual(i);
}
});
test("group, default map, default reducer, filters", () => {
// From the docs:
// Note: a grouping intersects the crossfilter's current filters, except for the
// associated dimension's filter. Thus, group methods consider only records that
// satisfy every filter except this dimension's filter. So, if the crossfilter of
// payments is filtered by type and total, then group by total only observes the
// filter by type.
expect(payments).toBeDefined();
const tip = payments.dimension(r => r.tip, Int32Array);
const total = payments.dimension(r => r.total, Int32Array);
const type = payments.dimension(r => r.type, "enum");
const paymentsByTip = tip.group();
const paymentsByTotal = total.group();
const paymentsByType = type.group();
// 1. confirm that changing the filter on a dimension does NOT change that
// dimensions groups.
{
tip.filterAll(), total.filterAll(), type.filterAll();
let before = _.cloneDeep(paymentsByTip.all());
tip.filterExact(0);
expect(paymentsByTip.all()).toEqual(before);
}
// 2. confirm that changing a filter on a different dimension DOES change
// all other groups.
{
tip.filterAll(), total.filterAll(), type.filterAll();
const before = _.cloneDeep([paymentsByTotal.all(), paymentsByType.all()]);
tip.filterExact(0);
const after = [paymentsByTotal.all(), paymentsByType.all()];
expect(after).not.toEqual(before);
expect(after).toEqual([
groupReduce(
someData,
v => v.total,
(p, v) => (v.tip !== 0 ? p : p + 1),
() => 0
),
groupReduce(
someData,
v => v.type,
(p, v) => (v.tip !== 0 ? p : p + 1),
() => 0
)
]);
}
for (let i of [
paymentsByTip,
paymentsByTotal,
paymentsByType,
tip,
total,
type
]) {
expect(i.dispose()).toEqual(i);
}
});
});
@@ -1,11 +1,8 @@
import { import {
fillRange,
sliceByIndex, sliceByIndex,
makeSortIndex, makeSortIndex,
lowerBound,
lowerBoundIndirect,
upperBoundIndirect
} from "../../../src/util/typedCrossfilter/util"; } from "../../../src/util/typedCrossfilter/util";
import { rangeFill as fillRange } from "../../../src/util/range";
describe("fillRange", () => { describe("fillRange", () => {
test("Array", () => { test("Array", () => {
+16 -5
View File
@@ -2,13 +2,24 @@ module.exports = {
babelrc: false, babelrc: false,
cacheDirectory: true, cacheDirectory: true,
presets: [ presets: [
["modern-browsers", { loose: true, modules: false }], [
"@babel/preset-react" "@babel/preset-env",
{
useBuiltIns: "entry",
corejs: 3,
modules: false,
},
],
"@babel/preset-react",
], ],
plugins: [ plugins: [
"@babel/plugin-proposal-function-bind", "@babel/plugin-proposal-function-bind",
"@babel/plugin-proposal-class-properties",
["@babel/plugin-proposal-decorators", { legacy: true }], ["@babel/plugin-proposal-decorators", { legacy: true }],
"@babel/plugin-proposal-export-namespace-from" ["@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",
],
}; };
+16 -5
View File
@@ -1,15 +1,26 @@
module.exports = { module.exports = {
babelrc: false, babelrc: false,
presets: [ presets: [
["modern-browsers", { loose: true, modules: false }], [
"@babel/preset-react" "@babel/preset-env",
{
useBuiltIns: "entry",
corejs: 3,
modules: false,
},
],
"@babel/preset-react",
], ],
plugins: [ plugins: [
"@babel/plugin-proposal-function-bind", "@babel/plugin-proposal-function-bind",
"@babel/plugin-proposal-class-properties",
["@babel/plugin-proposal-decorators", { legacy: true }], ["@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-export-namespace-from",
"@babel/plugin-transform-react-constant-elements", "@babel/plugin-transform-react-constant-elements",
"@babel/plugin-transform-runtime" "@babel/plugin-transform-runtime",
] "@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
],
}; };
+54 -17
View File
@@ -1,18 +1,48 @@
module.exports = { module.exports = {
root: true, root: true,
parser: "babel-eslint",
extends: ["airbnb", "prettier", "prettier/react"], extends: [
"airbnb",
"plugin:eslint-comments/recommended",
"plugin:@blueprintjs/recommended",
"plugin:compat/recommended",
"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: {
// 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 }, env: { browser: true, commonjs: true, es6: true },
globals: { expect: true }, globals: {
expect: true,
jest: true,
jestPuppeteer: true,
it: true,
page: true,
browser: true,
context: true,
beforeEach: true,
},
parser: "@babel/eslint-parser",
parserOptions: { parserOptions: {
ecmaVersion: 2017, ecmaVersion: 2017,
sourceType: "module", sourceType: "module",
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
generators: true generators: true,
} },
babelOptions: {
configFile: "./configuration/babel/babel.prod.js",
},
}, },
rules: { rules: {
"react/jsx-no-target-blank": "off",
"eslint-comments/require-description": ["error"],
"no-magic-numbers": "off", "no-magic-numbers": "off",
"no-nested-ternary": "off", "no-nested-ternary": "off",
"func-style": "off", "func-style": "off",
@@ -21,13 +51,7 @@ module.exports = {
"react/jsx-filename-extension": "off", "react/jsx-filename-extension": "off",
"comma-dangle": "off", "comma-dangle": "off",
"no-underscore-dangle": "off", "no-underscore-dangle": "off",
quotes: ["error", "double"],
"implicit-arrow-linebreak": "off", "implicit-arrow-linebreak": "off",
"operator-linebreak": [
"error",
"after",
{ overrides: { "?": "before", ":": "before" } }
],
"no-console": "off", "no-console": "off",
"spaced-comment": ["error", "always", { exceptions: ["*"] }], "spaced-comment": ["error", "always", { exceptions: ["*"] }],
"no-param-reassign": "off", "no-param-reassign": "off",
@@ -35,13 +59,26 @@ module.exports = {
"react/prop-types": [0], "react/prop-types": [0],
"space-before-function-paren": "off", "space-before-function-paren": "off",
"function-paren-newline": "off", "function-paren-newline": "off",
"prefer-destructuring": ["error", { object: true, array: false }] "prefer-destructuring": ["error", { object: true, array: false }],
"import/prefer-default-export": "off",
"no-restricted-syntax": [
"error",
"ForInStatement",
"LabeledStatement",
"WithStatement",
],
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: true,
},
],
}, },
overrides: [ overrides: [
{ {
files: ["**/*.test.js"], files: ["**/*.test.js"],
env: { env: {
jest: true // now **/*.test.js files' env has both es6 *and* jest jest: true, // now **/*.test.js files' env has both es6 *and* jest
}, },
// Can't extend in overrides: https://github.com/eslint/eslint/issues/8813 // Can't extend in overrides: https://github.com/eslint/eslint/issues/8813
// "extends": ["plugin:jest/recommended"] // "extends": ["plugin:jest/recommended"]
@@ -51,8 +88,8 @@ module.exports = {
"jest/no-focused-tests": "error", "jest/no-focused-tests": "error",
"jest/no-identical-title": "error", "jest/no-identical-title": "error",
"jest/prefer-to-have-length": "warn", "jest/prefer-to-have-length": "warn",
"jest/valid-expect": "error" "jest/valid-expect": "error",
} },
} },
] ],
}; };
@@ -0,0 +1,4 @@
module.exports = {
"*.js": "eslint --fix",
"**/*": "prettier --write --ignore-unknown",
};
@@ -0,0 +1,53 @@
/* eslint-disable import/no-extraneous-dependencies -- this file is a devDependency*/
const cheerio = require("cheerio");
const crypto = require("crypto");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const digest = (str) => {
const hash = crypto.createHash("sha256").update(str, "utf8").digest("base64");
return `sha256-${hash}`;
};
class CspHashPlugin {
constructor(opts) {
this.opts = { ...opts };
}
apply(compiler) {
compiler.hooks.compilation.tap("CspHashPlugin", (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
"CspHashPlugin",
(data, cb) => {
const { filename } = this.opts;
const $ = cheerio.load(data.html, { decodeEntities: false });
if (filename) {
const results = {};
results["script-hashes"] = $("script:not([src]):not([no-csp-hash])")
.map((i, elmt) => digest($(elmt).html()))
.get();
const json = JSON.stringify(results);
compilation.assets[filename] = {
source: () => json,
size: () => json.length,
};
}
// Remove no-csp-hash attributes. Cheerio does not parse Jinja templates
// correctly, so we brute force this with a regular expression.
data.html = data.html.replace(
/(<script .*)no-csp-hash(.*>)/,
(match, p1, p2) => [p1, p2].join("")
);
// Tell webpack to move on
cb(null, data);
}
);
});
}
}
module.exports = CspHashPlugin;
/* eslint-enable import/no-extraneous-dependencies -- enable*/
@@ -0,0 +1,75 @@
<script>
/** Reflect ANY changes to this script in the script hash in `server/eb/app.py` **/
var root = document.getElementById("root");
root.remove();
var portals = document.getElementsByClassName("bp3-portal");
for (var i = 0; i < portals.length; i += 1) {
portals[i].remove();
}
</script>
<div
style="
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
text-align: center;
justify-content: center;
align-items: center;
background: #8080801a;
font-family: 'Roboto Condensed, sans serif';
"
>
<img
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/cellxgene-logo.png"
style="width: 320px;"
/>
<div
style="
margin-top: 16px;
background: white;
width: 40vw;
border-radius: 4px;
padding: 24px 64px;
-webkit-box-shadow: 0px 0px 3px 2px rgba(0, 0, 0, 0.38);
-moz-box-shadow: 0px 0px 3px 2px rgba(0, 0, 0, 0.38);
box-shadow: 0px 0px 3px 2px rgba(0, 0, 0, 0.38);
max-width: 550px;
"
>
<div style="margin-bottom: 0; font-weight: bolder; font-size: 1.2em;">
Unsupported Browser
</div>
<div style="margin-top: 0;">
cellxgene is currently supported on the following browsers
</div>
<div
style="display: flex; justify-content: space-around; margin-top: 16px;"
>
<a
href="https://www.google.com/chrome/?hl=en%22"
aria-label="Download Google Chrome"
>
<img
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/chrome.png"
style="width: 80px; height: 80px;"
/>
<div>Chrome &gt; 60</div>
</a>
<a href="https://www.mozilla.com/firefox/" aria-label="Download Firefox">
<img
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/firefox.png"
style="width: 80px; height: 80px;"
/>
<div>Firefox ≥ 60</div>
</a>
<a href="//www.microsoft.com/edge" aria-label="Download Edge">
<img
src="https://cellxgene.cziscience.com/s3/cellxgene/static/images/edge.png"
style="width: 80px; height: 80px;"
/>
<div>Edge ≥ 79</div>
</a>
</div>
</div>
</div>
@@ -1,82 +1,66 @@
// jshint esversion: 6
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const src = path.resolve("src"); const { merge } = require("webpack-merge");
const nodeModules = path.resolve("node_modules");
const sharedConfig = require("./webpack.config.shared");
const babelOptions = require("../babel/babel.dev"); const babelOptions = require("../babel/babel.dev");
module.exports = { const fonts = path.resolve("src/fonts");
const images = path.resolve("src/images");
const nodeModules = path.resolve("node_modules");
const devConfig = {
mode: "development", mode: "development",
devtool: "eval", devtool: "eval",
entry: ["./src/index"],
output: { output: {
path: path.resolve("build"),
pathinfo: true, pathinfo: true,
filename: "static/js/bundle.js", filename: "static/js/bundle.js",
publicPath: "/"
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.js$/, test: /\.jsx?$/,
include: src,
loader: "babel-loader", loader: "babel-loader",
options: babelOptions options: babelOptions,
}, },
{ {
test: /\.css$/, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
include: src,
exclude: [path.resolve(src, "index.css")],
loader: [
{
loader: "style-loader"
},
{
loader: "css-loader",
options: {
modules: true,
localIdentName: "[name]__[local]___[hash:base64:5]"
}
}
]
},
{
test: /index\.css$/,
include: [path.resolve(src, "index.css")],
loader: [
{
loader: "style-loader"
},
{
loader: "css-loader"
}
]
},
{ test: /\.json$/, include: [src, nodeModules], loader: "json-loader" },
{
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
include: [src, nodeModules],
loader: "file-loader", loader: "file-loader",
query: { name: "static/media/[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: "..",
},
}, },
{ ],
test: /\.(mp4|webm)(\?.*)?$/,
include: [src, nodeModules],
loader: "url-loader",
query: { limit: 10000, name: "static/media/[name].[ext]" }
}
]
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: true, inject: true,
template: path.resolve("index.html"), template: path.resolve("index.html"),
favicon: path.resolve("favicon.png")
}), }),
new webpack.NoEmitOnErrorsPlugin() new MiniCssExtractPlugin({
] filename: "static/[name].css",
}),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.DefinePlugin({
__REACT_DEVTOOLS_GLOBAL_HOOK__: "({ isDisabled: true })",
}),
new webpack.DefinePlugin({
// 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,119 +1,87 @@
// jshint esversion: 6
const path = require("path"); const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");
const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin");
const src = path.resolve("src"); const { merge } = require("webpack-merge");
const nodeModules = path.resolve("node_modules");
const babelOptions = require("../babel/babel.prod"); const babelOptions = require("../babel/babel.prod");
const publicPath = "/"; const CspHashPlugin = require("./cspHashPlugin");
const sharedConfig = require("./webpack.config.shared");
module.exports = { const fonts = path.resolve("src/fonts");
const images = path.resolve("src/images");
const nodeModules = path.resolve("node_modules");
const prodConfig = {
mode: "production", mode: "production",
bail: true, bail: true,
cache: false, cache: false,
devtool: "cheap-source-map",
entry: ["./src/index.js"],
output: { output: {
path: path.resolve("build"), filename: "static/[name]-[contenthash].js",
filename: "static/js/[name].[chunkhash:8].js",
publicPath
}, },
optimization: {
minimize: true,
minimizer: [
new TerserJSPlugin({}),
new CssMinimizerPlugin({
minify: CssMinimizerPlugin.cleanCssMinify,
}),
],
},
devtool: "source-map",
module: { module: {
rules: [ rules: [
{ {
test: /\.js$/, test: /\.jsx?$/,
include: src,
loader: "babel-loader", loader: "babel-loader",
options: babelOptions options: babelOptions,
}, },
{ {
test: /\.css$/, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2|otf)$/i,
include: src,
exclude: [path.resolve(src, "index.css")],
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
modules: true,
importLoaders: 1,
localIdentName: "[name]__[local]___[hash:base64:5]"
}
}
]
},
{
test: /index\.css$/,
include: [path.resolve(src, "index.css")],
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
importLoaders: 1
}
}
]
},
{
test: /\.json$/,
include: [src, nodeModules],
loader: "json-loader",
exclude: /manifest.json$/
},
{
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
include: [src, nodeModules],
loader: "file-loader", loader: "file-loader",
include: [nodeModules, fonts, images],
options: { options: {
name: "[name].[hash:8].[ext]", name: "static/assets/[name]-[contenthash].[ext]",
outputPath: "static/media/" // (thuang): This is needed to make sure @font url path is '../static/assets/'
} publicPath: "..",
},
}, },
{ ],
test: /\.(mp4|webm)(\?.*)?$/,
include: [src, nodeModules],
loader: "url-loader",
query: { limit: 10000, name: "static/media/[name].[hash:8].[ext]" }
}
]
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: "body",
filename: "index.html", filename: "index.html",
template: path.resolve("index_template.html"), template: path.resolve("index_template.html"),
favicon: path.resolve("favicon.png"), decodeEntities: false,
inlineSource: ".(js|css)$", minify: false,
minify: { }),
removeComments: true, new CleanWebpackPlugin({
collapseWhitespace: true, verbose: true,
removeRedundantAttributes: true, protectWebpackAssets: false,
useShortDoctype: true, cleanAfterEveryBuildPatterns: ["main.js", "main.css"],
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
}), }),
new HtmlWebpackInlineSourcePlugin(),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:8].css" 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",
}),
}), }),
new SWPrecacheWebpackPlugin({
cacheId: "cellxgene",
filename: "service-worker.js"
})
], ],
performance: { performance: {
maxEntrypointSize: 2000000, maxEntrypointSize: 2000000,
maxAssetSize: 2000000 maxAssetSize: 2000000,
} },
}; };
module.exports = merge(sharedConfig, prodConfig);
@@ -0,0 +1,69 @@
const path = require("path");
const fs = require("fs");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ObsoleteWebpackPlugin = require("webpack-obsolete-plugin");
const src = path.resolve("src");
const nodeModules = path.resolve("node_modules");
const publicPath = "";
const rawObsoleteHTMLTemplate = fs.readFileSync(
`${__dirname}/obsoleteHTMLTemplate.html`,
"utf8"
);
const obsoleteHTMLTemplate = rawObsoleteHTMLTemplate.replace(/'/g, '"');
module.exports = {
entry: [
"core-js",
"regenerator-runtime/runtime",
"whatwg-fetch",
"abort-controller/polyfill",
"./src/index",
],
output: {
path: path.resolve("build"),
publicPath,
},
module: {
rules: [
{
test: /\.css$/,
include: src,
exclude: [path.resolve(src, "index.css")],
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[name]__[local]___[contenthash:base64:5]",
},
importLoaders: 1,
},
},
],
},
{
test: /index\.css$/,
include: [path.resolve(src, "index.css")],
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.json$/,
include: [src, nodeModules],
loader: "json-loader",
exclude: /manifest.json$/,
},
],
},
plugins: [
new ObsoleteWebpackPlugin({
name: "obsolete",
template: obsoleteHTMLTemplate,
promptOnNonTargetBrowser: false,
}),
],
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 312 B

+25 -8
View File
@@ -1,13 +1,27 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>cellxgene</title> <title>CELL&times;GENE | Annotate</title>
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700" rel="stylesheet">
<style> <style>
html, body, p, h1, h2, h3, h4, h5, h6, span, button, input, label, text, div { html,
font-family: 'Roboto Condensed','Helvetica Neue','Helvetica','Arial',sans-serif; body,
p,
h1,
h2,
h3,
h4,
h5,
h6,
span,
button,
input,
label,
text,
div {
font-family: "Roboto Condensed", "Helvetica Neue", "Helvetica", "Arial",
sans-serif;
font-size: 14px; font-size: 14px;
} }
body { body {
@@ -21,9 +35,12 @@
</style> </style>
</head> </head>
<body> <body>
<noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript> <noscript
>If you're seeing this message, that means
<strong>JavaScript has been disabled on your browser</strong>, please
<strong>enable JS</strong> to make this app work.</noscript
>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>
+44 -14
View File
@@ -1,15 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>cellxgene</title> <title>CELL&times;GENE | Annotate</title>
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700" rel="stylesheet">
<style> <style>
html, body, p, h1, h2, h3, h4, h5, h6, span, button, input, label, text, div { html,
font-family: 'Roboto Condensed','Helvetica Neue','Helvetica','Arial',sans-serif; body,
p,
h1,
h2,
h3,
h4,
h5,
h6,
span,
button,
input,
label,
text,
div {
font-family: "Roboto Condensed", "Helvetica Neue", "Helvetica", "Arial",
sans-serif;
font-size: 14px; font-size: 14px;
} }
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -21,15 +36,30 @@
</style> </style>
</head> </head>
<body> <body>
<script type="text/javascript"> <script type="text/javascript">
window.CELLXGENE = {}; window.CELLXGENE = {};
window.CELLXGENE.API = { window.CELLXGENE.API = {
prefix: "{{ prefix | safe }}", prefix: `${location.origin}${location.pathname}api/`,
version: "v0.2/" version: "v0.2/",
}; };
</script> </script>
<noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript> <noscript
>If you're seeing this message, that means
<strong>JavaScript has been disabled on your browser</strong>, please
<strong>enable JS</strong> to make this app work.
</noscript>
<div id="root"></div> <div id="root"></div>
{% for script in SCRIPTS -%}
<script type="text/javascript"
{{ ('integrity="%s"' % script.integrity) | safe if script.integrity }}
{{ ('crossorigin="%s"' % script.crossorigin) | safe if script.crossorigin }}
src="{{script.src | safe}}"
></script>
{%- endfor %}
{% for ils in INLINE_SCRIPTS %}
<script type="text/javascript" no-csp-hash>{% include ils %}</script>
{% endfor %}
</body> </body>
</html> </html>
+47
View File
@@ -0,0 +1,47 @@
/**
* `client/jest-puppeteer.config.js` is for configuring Puppeteer's launch config options
* `client/__tests__/e2e/puppeteer.setup.js` is for configuring `jest`, `browser`,
* and `page` objects
*/
const ENV_DEFAULT = require("../environment.default.json");
const jestEnv = process.env.JEST_ENV || ENV_DEFAULT.JEST_ENV;
const isHeadful =
process.env.HEADFUL === "true" || process.env.HEADLESS === "false";
const DEFAULT_LAUNCH_CONFIG = {
headless: !isHeadful,
args: ["--ignore-certificate-errors", "--ignore-ssl-errors"],
ignoreHTTPSErrors: true,
timeout: 90000,
defaultViewport: {
width: 1280,
height: 960,
},
};
const LAUNCH_CONFIG_BY_ENV = {
[ENV_DEFAULT.DEBUG]: {
...DEFAULT_LAUNCH_CONFIG,
headless: false,
slowMo: 100,
devtools: true,
defaultViewport: {
width: DEFAULT_LAUNCH_CONFIG.defaultViewport.width,
height: DEFAULT_LAUNCH_CONFIG.defaultViewport.height + 560,
},
},
[ENV_DEFAULT.DEV]: {
...DEFAULT_LAUNCH_CONFIG,
headless: false,
slowMo: 5,
},
};
const launchConfig = LAUNCH_CONFIG_BY_ENV[jestEnv] || DEFAULT_LAUNCH_CONFIG;
module.exports = {
browserContext: "incognito",
launch: launchConfig,
};
+28778 -10091
View File
File diff suppressed because it is too large Load Diff
+149 -87
View File
@@ -1,115 +1,158 @@
{ {
"name": "cellxgene", "name": "cellxgene",
"version": "0.2.2", "version": "1.3.0",
"license": "MIT", "license": "MIT",
"description": "cellxgene is a web application for the interactive exploration of single cell sequence data.", "description": "cellxgene is a web application for the interactive exploration of single cell sequence data.",
"repository": "https://github.com/chanzuckerberg/cellxgene", "repository": "https://github.com/chanzuckerberg/cellxgene",
"scripts": { "scripts": {
"build": "npm run clean && webpack --config configuration/webpack/webpack.config.prod.js", "build": "npm run clean && webpack --config",
"dev": "npm run clean && webpack --config configuration/webpack/webpack.config.dev.js",
"clean": "rimraf build", "clean": "rimraf build",
"start": "node server/development.js", "dev": "npm run build -- configuration/webpack/webpack.config.dev.js",
"lint": "eslint src", "e2e": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2e.test.js",
"test": "jest" "e2e-annotations": "jest --config __tests__/e2e/e2eJestConfig.json e2e/e2eAnnotations.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",
"prepare": "cd .. && husky install client/.husky"
}, },
"engineStrict": true, "engineStrict": true,
"engines": { "engines": {
"npm": ">=3.0.0" "npm": ">=9.6.7",
"node": "^18.17.0"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "./configuration/eslint/eslint.js" "extends": "./configuration/eslint/eslint.js"
}, },
"nyc": { "eslintIgnore": [
"sourceMap": false, "src/util/stateManager/matrix_generated.js"
"instrument": false ],
},
"resolutions": { "resolutions": {
"eslint-scope": "3.7.1" "eslint-scope": "3.7.1"
}, },
"browserslist": [
"Chrome > 60",
"iOS >= 10.3",
"Firefox >= 60",
"Edge >= 79",
"not Explorer > 0",
"not Safari > 0"
],
"dependencies": { "dependencies": {
"@blueprintjs/core": "^3.8.0", "@babel/eslint-parser": "^7.15.0",
"@blueprintjs/icons": "^3.3.0", "@blueprintjs/core": "^3.44.0",
"@blueprintjs/select": "^3.2.1", "@blueprintjs/icons": "^3.19.0",
"canvas-fit": "^1.5.0", "@blueprintjs/popover2": "^0.11.2",
"@blueprintjs/select": "^3.16.0",
"abort-controller": "^3.0.0",
"core-js": "^3.16.3",
"d3": "^4.10.0", "d3": "^4.10.0",
"d3-scale-chromatic": "^1.3.0", "d3-scale-chromatic": "^1.5.0",
"font-color-contrast": "^1.0.3", "flatbuffers": "^1.11.0",
"gl-mat4": "^1.1.4", "fuzzysort": "^1.1.4",
"gl-matrix": "^2.7.1", "gl-mat4": "^1.2.0",
"key-pressed": "0.0.1", "gl-matrix": "^3.3.0",
"lodash": "^4.17.4", "gl-vec3": "^1.1.3",
"memoize-one": "^4.0.0", "is-number": "^7.0.0",
"mouse-position": "^2.0.1", "lodash.clonedeep": "^4.5.0",
"mouse-pressed": "^1.0.0", "lodash.difference": "^4.5.0",
"normalize.css": "^8.0.0", "lodash.foreach": "^4.5.0",
"orbit-camera": "^1.0.0", "lodash.pull": "^4.1.0",
"query-string": "^6.1.0", "lodash.sortby": "^4.7.0",
"react": "^16.6.0", "lodash.uniq": "^4.5.0",
"react-autocomplete": "^1.7.2", "memoize-one": "^5.1.1",
"react-dom": "^16.6.0", "pako": "^2.0.3",
"react-helmet": "^5.2.0", "react": "^17.0.2",
"react-icons": "^3.2.2", "react-async": "^10.0.1",
"react-redux": "^5.1.0", "react-dom": "^17.0.2",
"redux": "^4.0.1", "react-flip-toolkit": "^7.0.12",
"redux-devtools-extension": "^2.13.5", "react-helmet": "^6.1.0",
"redux-thunk": "^2.2.0", "react-icons": "^4.2.0",
"regl": "^1.3.9", "react-redux": "^7.2.0",
"scroll-speed": "^1.0.0", "redux": "^4.0.5",
"urijs": "^1.19.0" "redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.7",
"regl": "^2.1.0",
"sha1": "^1.1.1",
"tinyqueue": "^2.0.3",
"whatwg-fetch": "^3.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.1.5", "@babel/core": "^7.25.2",
"@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.0.0", "@babel/plugin-proposal-decorators": "^7.13.15",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0", "@babel/plugin-proposal-export-namespace-from": "^7.10.4",
"@babel/plugin-proposal-function-bind": "^7.0.0", "@babel/plugin-proposal-function-bind": "^7.10.5",
"@babel/plugin-transform-react-constant-elements": "^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.1.0", "@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/preset-env": "^7.1.5", "@babel/plugin-transform-private-property-in-object": "^7.22.11",
"@babel/preset-react": "^7.0.0", "@babel/plugin-transform-react-constant-elements": "^7.13.13",
"@babel/register": "^7.0.0", "@babel/plugin-transform-runtime": "^7.13.15",
"@babel/runtime": "^7.1.5", "@babel/preset-env": "^7.22.20",
"babel-core": "^7.0.0-bridge.0", "@babel/preset-react": "^7.13.13",
"babel-eslint": "^10.0.1", "@babel/register": "^7.13.16",
"babel-jest": "^23.6.0", "@babel/runtime": "^7.13.16",
"babel-loader": "^8.0.0", "@blueprintjs/eslint-plugin": "^0.3.0",
"babel-plugin-istanbul": "^5.1.0", "@sentry/webpack-plugin": "^1.15.0",
"babel-preset-modern-browsers": "^12.0.0", "babel-jest": "^26.1.0",
"chalk": "^2.4.1", "babel-loader": "^8.1.0",
"connect-history-api-fallback": "^1.3.0", "babel-preset-modern-browsers": "^15.0.2",
"copy-webpack-plugin": "^4.6.0", "chalk": "^4.1.0",
"css-loader": "^1.0.1", "cheerio": "^1.0.0-rc.6",
"eslint": "^5.8.0", "clean-css": "^5.1.2",
"eslint-config-airbnb": "^17.1.0", "clean-webpack-plugin": "^4.0.0-alpha.0",
"eslint-config-prettier": "^3.1.0", "css-loader": "^5.2.4",
"eslint-loader": "^2.1.1", "css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^7.24.0",
"eslint-config-airbnb": "^18.2.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-filenames": "^1.3.2",
"eslint-plugin-import": "^2.14.0", "eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^21.27.2", "eslint-plugin-jest": "^24.3.5",
"eslint-plugin-jsx-a11y": "^6.1.1", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.11.1", "eslint-plugin-react": "^7.23.2",
"express": "^4.14.0", "eslint-plugin-react-hooks": "^4.0.8",
"file-loader": "^2.0.0", "expect-puppeteer": "^5.0.0",
"html-webpack-inline-source-plugin": "0.0.10", "express": "^4.17.1",
"html-webpack-plugin": "^3.2.0", "file-loader": "^6.0.0",
"jest": "^23.5.0", "html-webpack-plugin": "^5.3.1",
"json-loader": "^0.5.4", "husky": "^7.0.2",
"mini-css-extract-plugin": "^0.4.1", "jest": "^27.0.6",
"nyc": "^13.0.1", "jest-circus": "^27.0.6",
"rimraf": "^2.5.4", "jest-environment-puppeteer": "^5.0.1",
"serve-favicon": "^2.3.0", "jest-fetch-mock": "^3.0.3",
"style-loader": "^0.23.1", "jest-puppeteer": "^6.2.0",
"sw-precache-webpack-plugin": "^0.11.5", "json-loader": "^0.5.7",
"url-loader": "^1.1.0", "lint-staged": "^10.2.11",
"webpack": "^4.25.1", "lodash": "^4.17.21",
"webpack-cli": "^3.1.0", "lodash.every": "^4.6.0",
"webpack-dev-middleware": "^3.1.3" "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": "^10.4.0",
"rimraf": "^3.0.2",
"serve-favicon": "^2.5.0",
"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": { "jest": {
"testMatch": [ "testMatch": [
"**/__tests__/**/?(*.)(spec|test).js?(x)" "**/__tests__/**/?(*.)(spec|test).js?(x)"
], ],
"testURL": "http://localhost/" "setupFiles": [
"./__tests__/setupMissingGlobals.js"
],
"coverageDirectory": "./coverage/",
"collectCoverage": true
}, },
"babel": { "babel": {
"env": { "env": {
@@ -120,16 +163,35 @@
], ],
"plugins": [ "plugins": [
"@babel/plugin-proposal-function-bind", "@babel/plugin-proposal-function-bind",
"@babel/plugin-proposal-class-properties",
[ [
"@babel/plugin-proposal-decorators", "@babel/plugin-proposal-decorators",
{ {
"legacy": true "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-export-namespace-from",
"@babel/plugin-transform-react-constant-elements", "@babel/plugin-transform-react-constant-elements",
"@babel/plugin-transform-runtime" "@babel/plugin-transform-runtime",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator"
] ]
} }
} }
+28 -24
View File
@@ -1,49 +1,53 @@
/* eslint-disable */ const chalk = require("chalk");
// jshint esversion: 6 const express = require("express");
var path = require("path"); const favicon = require("serve-favicon");
var historyApiFallback = require("connect-history-api-fallback"); const webpack = require("webpack");
var chalk = require("chalk"); const devMiddleware = require("webpack-dev-middleware");
var express = require("express"); const config = require("../configuration/webpack/webpack.config.dev");
var favicon = require("serve-favicon"); const utils = require("./utils");
var webpack = require("webpack");
var config = require("../configuration/webpack/webpack.config.dev");
var utils = require("./utils");
process.env.NODE_ENV = "development"; process.env.NODE_ENV = "development";
var PORT = process.env.PORT || 3000; const CLIENT_PORT = process.env.CXG_CLIENT_PORT;
const { CXG_SERVER_PORT } = process.env;
const API = {
prefix: `http://localhost:${CXG_SERVER_PORT}/`,
};
// Set up compiler // Set up compiler
var compiler = webpack(config); const compiler = webpack(config);
compiler.plugin("invalid", () => { compiler.hooks.invalid.tap("invalid", () => {
utils.clearConsole(); utils.clearConsole();
console.log("Compiling..."); console.log("Compiling...");
}); });
compiler.plugin("done", stats => { compiler.hooks.done.tap("done", (stats) => {
utils.formatStats(stats, PORT); utils.formatStats(stats, CLIENT_PORT);
}); });
// Launch server // Launch server
var app = express(); const app = express();
app.use(historyApiFallback({ verbose: false }));
app.use( app.use(
require("webpack-dev-middleware")(compiler, { devMiddleware(compiler, {
logLevel: "warn", publicPath: config.output.publicPath,
publicPath: config.output.publicPath index: true,
}) })
); );
app.use(favicon("./favicon.png")); app.use(favicon("./favicon.png"));
app.get("*", (req, res) => { app.get("/logout", async (req, res) => {
res.sendFile(path.resolve("index.html")); try {
res.redirect(`${API.prefix}logout?dataset=http://localhost:${CLIENT_PORT}`);
} catch (err) {
console.error(err);
}
}); });
app.listen(PORT, err => { app.listen(CLIENT_PORT, (err) => {
if (err) { if (err) {
console.log(err); console.log(err);
return; return;
+7 -5
View File
@@ -8,7 +8,9 @@ function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1; return message.indexOf(friendlySyntaxErrorLabel) !== -1;
} }
function formatMessage(message) { function formatMessage(messageObject) {
let { message, details } = messageObject;
if (details) message = message + ": " + details;
return message return message
.replace("Module build failed: SyntaxError:", friendlySyntaxErrorLabel) .replace("Module build failed: SyntaxError:", friendlySyntaxErrorLabel)
.replace( .replace(
@@ -36,10 +38,10 @@ var formatStats = (stats, port) => {
var json = stats.toJson(); var json = stats.toJson();
var formattedErrors = json.errors.map( var formattedErrors = json.errors.map(
message => "Error in " + formatMessage(message) (message) => "Error in " + formatMessage(message)
); );
var formattedWarnings = json.warnings.map( var formattedWarnings = json.warnings.map(
message => "Warning in " + formatMessage(message) (message) => "Warning in " + formatMessage(message)
); );
if (hasErrors) { if (hasErrors) {
@@ -48,7 +50,7 @@ var formatStats = (stats, port) => {
if (formattedErrors.some(isLikelyASyntaxError)) { if (formattedErrors.some(isLikelyASyntaxError)) {
formattedErrors = formattedErrors.filter(isLikelyASyntaxError); formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
} }
formattedErrors.forEach(message => { formattedErrors.forEach((message) => {
console.log(message); console.log(message);
console.log(); console.log();
}); });
@@ -58,7 +60,7 @@ var formatStats = (stats, port) => {
if (hasWarnings) { if (hasWarnings) {
console.log(chalk.yellow("Compiled with warnings.")); console.log(chalk.yellow("Compiled with warnings."));
console.log(); console.log();
formattedWarnings.forEach(message => { formattedWarnings.forEach((message) => {
console.log(message); console.log(message);
console.log(); console.log();
}); });
+455
View File
@@ -0,0 +1,455 @@
/*
Action creators for user annotation
*/
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) => {
/*
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;
/* 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");
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);
}
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
);
dispatch({
type: "annotation: create category",
data: newCategoryName,
categoryToDuplicate,
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
});
};
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");
/* 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;
const obsCrossfilter = prevObsCrossfilter.renameObsColumn(
oldCategoryName,
newCategoryName
);
dispatch({
type: "annotation: category edited",
annoMatrix: obsCrossfilter.annoMatrix,
obsCrossfilter,
metadataField: oldCategoryName,
newCategoryText: newCategoryName,
data: newCategoryName,
});
};
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 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
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");
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: label current cell selection",
metadataField: categoryName,
label: labelName,
obsCrossfilter,
annoMatrix: obsCrossfilter.annoMatrix,
});
};
function writableAnnotations(annoMatrix) {
return annoMatrix.schema.annotations.obs.columns
.filter((s) => s.writable)
.map((s) => s.name);
}
export const needToSaveObsAnnotations = (annoMatrix, lastSavedAnnoMatrix) => {
/*
Return true if there are LIKELY user-defined annotation modifications between the two
annoMatrices. Technically not an action creator, but intimately intertwined
with the save process.
Two conditions will trigger a need to save:
* the collection of user-defined columns have changed
* the contents of the user-defined columns have change
*/
annoMatrix = annoMatrix.base();
// if the annoMatrix hasn't changed, we are guaranteed no changes to the matrix schema or contents.
if (annoMatrix === lastSavedAnnoMatrix) return false;
// if the schema has changed, we need to save
const currentWritable = writableAnnotations(annoMatrix);
if (difference(currentWritable, writableAnnotations(lastSavedAnnoMatrix))) {
return true;
}
// no schema changes; check for change in contents
return currentWritable.some(
(col) => annoMatrix.col(col) !== lastSavedAnnoMatrix.col(col)
);
};
export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
/*
Save the user-created obs annotations IF any have changed.
*/
const state = getState();
const { annotations, autosave } = state;
const { dataCollectionNameIsReadOnly, dataCollectionName } = annotations;
const { lastSavedAnnoMatrix, saveInProgress } = autosave;
const annoMatrix = state.annoMatrix.base();
if (saveInProgress || annoMatrix === lastSavedAnnoMatrix) return;
if (!needToSaveObsAnnotations(annoMatrix, lastSavedAnnoMatrix)) {
dispatch({
type: "writable obs annotations - save complete",
lastSavedAnnoMatrix: annoMatrix,
});
return;
}
/*
Else, we really do need to save
*/
dispatch({
type: "writable obs annotations - save started",
});
const df = await annoMatrix.fetch("obs", writableAnnotations(annoMatrix));
const matrix = MatrixFBS.encodeMatrixFBS(df);
const compressedMatrix = pako.deflate(matrix);
try {
const queryString =
!dataCollectionNameIsReadOnly && !!dataCollectionName
? `?annotation-collection-name=${encodeURIComponent(
dataCollectionName
)}`
: "";
const res = await fetch(
`${globals.API.prefix}${globals.API.version}annotations/obs${queryString}`,
{
method: "PUT",
body: compressedMatrix,
headers: new Headers({
"Content-Type": "application/octet-stream",
}),
credentials: "include",
}
);
if (res.ok) {
dispatch({
type: "writable obs annotations - save complete",
lastSavedAnnoMatrix: annoMatrix,
});
} else {
dispatch({
type: "writable obs annotations - save error",
message: `HTTP error ${res.status} - ${res.statusText}`,
res,
});
}
} catch (error) {
dispatch({
type: "writable obs annotations - save error",
message: error.toString(),
error,
});
}
};
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,
});
}
};
+47
View File
@@ -0,0 +1,47 @@
/*
action creators related to embeddings choice
*/
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
export async function _switchEmbedding(
prevAnnoMatrix,
prevCrossfilter,
newEmbeddingName
) {
/*
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,
prevCrossfilter.obsCrossfilter
).select("emb", newEmbeddingName, {
mode: "all",
});
return [annoMatrix, obsCrossfilter];
}
export const layoutChoiceAction =
(newLayoutChoice) => async (dispatch, getState) => {
/*
On layout choice, make sure we have selected all on the previous layout, AND the new
layout.
*/
const { annoMatrix: prevAnnoMatrix, 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;
}
+215 -254
View File
@@ -1,196 +1,141 @@
// jshint esversion: 6
import _ from "lodash";
import * as globals from "../globals"; import * as globals from "../globals";
import { Universe, kvCache } from "../util/stateManager"; import { AnnoMatrixLoader, AnnoMatrixObsCrossfilter } from "../annoMatrix";
import { import {
catchErrorsWrap, catchErrorsWrap,
doJsonRequest, doJsonRequest,
rangeEncodeIndices, dispatchNetworkErrorMessageToUser,
dispatchNetworkErrorMessageToUser
} from "../util/actionHelpers"; } from "../util/actionHelpers";
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;
}
/* /*
Bootstrap application with the initial data loading. return promise fetching user-configured colors
* /config - application configuration */
* /schema - schema of dataframe async function userColorsFetchAndLoad(dispatch) {
* /annotations/obs - all metadata annotation return fetchJson("colors").then((response) =>
dispatch({
type: "universe: user color load success",
userColors: loadUserColorConfig(response),
})
);
}
async function schemaFetch() {
return fetchJson("schema");
}
async function configFetch(dispatch) {
return fetchJson("config").then((response) => {
const config = { ...globals.configDefaults, ...response.config };
setGlobalConfig(config);
dispatch({
type: "configuration load complete",
config,
});
return config;
});
}
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
*/
const { schema } = annoMatrix;
const available = schema.layout.obs.map((v) => v.name);
available.forEach((embName) => annoMatrix.prefetch("emb", embName));
}
/*
Application bootstrap
*/ */
const doInitialDataLoad = () => const doInitialDataLoad = () =>
catchErrorsWrap(async dispatch => { catchErrorsWrap(async (dispatch) => {
dispatch({ type: "initial data load start" }); dispatch({ type: "initial data load start" });
try { try {
const requests = _([ const [config, schema] = await Promise.all([
"config", configFetch(dispatch),
"schema", schemaFetch(dispatch),
"annotations/obs", userColorsFetchAndLoad(dispatch),
"annotations/var", ]);
"layout/obs"
])
.map(r => `${globals.API.prefix}${globals.API.version}${r}`)
.map(url => doJsonRequest(url))
.value();
const results = await Promise.all(requests);
/* set config defaults */ genesetsFetch(dispatch, config);
const config = { ...globals.configDefaults, ...results[0].config };
const [, schema, obsAnno, varAnno, obsLayout] = [...results]; const baseDataUrl = `${globals.API.prefix}${globals.API.version}`;
const universe = Universe.createUniverseFromRestV02Response( const annoMatrix = new AnnoMatrixLoader(baseDataUrl, schema.schema);
config, const obsCrossfilter = new AnnoMatrixObsCrossfilter(annoMatrix);
schema, prefetchEmbeddings(annoMatrix);
obsAnno,
varAnno,
obsLayout
);
dispatch({ dispatch({
type: "configuration load complete", type: "annoMatrix: init complete",
config annoMatrix,
}); obsCrossfilter,
dispatch({
type: "initial data load complete (universe exists)",
universe
}); });
dispatch({ type: "initial data load complete" });
const defaultEmbedding = config?.parameters?.default_embedding;
const layoutSchema = schema?.schema?.layout?.obs ?? [];
if (
defaultEmbedding &&
layoutSchema.some((s) => s.name === defaultEmbedding)
) {
dispatch(embActions.layoutChoiceAction(defaultEmbedding));
}
} catch (error) { } catch (error) {
dispatch({ type: "initial data load error", error }); dispatch({ type: "initial data load error", error });
} }
}, true); }, true);
/*
Set the view (world) to current selection. Placeholder for an async action
which also does re-layout.
*/
const regraph = () => (dispatch, getState) => {
const { universe, world, crossfilter } = getState().controls;
dispatch({
type: "set World to current selection",
universe,
world,
crossfilter
});
};
// Throws
const dispatchExpressionErrors = (dispatch, res) => {
const msg = `Unexpected HTTP response while fetching expression data ${
res.status
}, ${res.statusText}`;
dispatchNetworkErrorMessageToUser(msg);
throw new Error(msg);
};
/*
Fetch expression vectors for each gene in genes. This is NOT an action
function, but rather a helper to be called from an action helper that
needs expression data.
Transparently utilizes cached data if it is already present.
*/
async function _doRequestExpressionData(dispatch, getState, genes) {
const state = getState();
const { universe } = state.controls;
/* preload data already in cache */
let expressionData = _.transform(
genes,
(expData, g) => {
const data = kvCache.get(universe.varDataCache, g);
if (data) {
expData[g] = data;
}
},
{}
); // --> { gene: data }
/* make a list of genes for which we do not have data */
const genesToFetch = _.filter(genes, g => expressionData[g] === undefined);
dispatch({ type: "expression load start" });
/* Fetch data for any genes not in cache */
if (genesToFetch.length) {
try {
// XXX: TODO - this could be using /data/var rather than /data/obs,
// as that would simplify the transformation in convertExpressionRESTv02ToObject
const res = await fetch(
`${globals.API.prefix}${globals.API.version}data/obs`,
{
method: "PUT",
body: JSON.stringify({
filter: {
var: {
annotation_value: [{ name: "name", values: genesToFetch }]
}
}
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
}
);
if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
// WILL throw
return dispatchExpressionErrors(dispatch, res);
}
const data = await res.json();
expressionData = {
...expressionData,
...Universe.convertExpressionRESTv02ToObject(universe, data)
};
} catch (error) {
dispatch({ type: "expression load error", error });
throw error; // rethrow
}
}
dispatch({ type: "expression load success", expressionData });
return expressionData;
}
function requestSingleGeneExpressionCountsForColoringPOST(gene) { function requestSingleGeneExpressionCountsForColoringPOST(gene) {
return async (dispatch, getState) => { return {
dispatch({ type: "get single gene expression for coloring started" }); type: "color by expression",
try { gene,
await _doRequestExpressionData(dispatch, getState, [gene]);
const { world } = getState().controls;
dispatch({
type: "color by expression",
gene,
data: {
[gene]: kvCache.get(world.varDataCache, gene)
}
});
} catch (error) {
dispatch({
type: "get single gene expression for coloring error",
error
});
}
}; };
} }
const requestUserDefinedGene = gene => async (dispatch, getState) => { const requestUserDefinedGene = (gene) => ({
dispatch({ type: "request user defined gene started" }); type: "request user defined gene success",
try { data: {
await await _doRequestExpressionData(dispatch, getState, [gene]); genes: [gene],
const { world } = getState().controls; },
});
/* then send the success case action through */
return dispatch({
type: "request user defined gene success",
data: {
genes: [gene],
expression: kvCache.get(world.varDataCache, gene)
}
});
} catch (error) {
return dispatch({
type: "request user defined gene error",
error
});
}
};
const dispatchDiffExpErrors = (dispatch, response) => { const dispatchDiffExpErrors = (dispatch, response) => {
switch (response.status) { switch (response.status) {
@@ -205,110 +150,126 @@ const dispatchDiffExpErrors = (dispatch, response) => {
); );
break; break;
default: { default: {
const msg = `Unexpected differential expression HTTP response ${ const msg = `Unexpected differential expression HTTP response ${response.status}, ${response.statusText}`;
response.status
}, ${response.statusText}`;
dispatchNetworkErrorMessageToUser(msg); dispatchNetworkErrorMessageToUser(msg);
dispatch({ dispatch({
type: "request differential expression error", type: "request differential expression error",
error: new Error(msg) error: new Error(msg),
}); });
} }
} }
}; };
const requestDifferentialExpression = (set1, set2, num_genes = 10) => async ( const requestDifferentialExpression =
dispatch, (set1, set2, num_genes = 50) =>
getState async (dispatch, getState) => {
) => { dispatch({ type: "request differential expression started" });
dispatch({ type: "request differential expression started" }); try {
try { /*
/*
Steps: Steps:
1. get the most differentially expressed genes 1. get the most differentially expressed genes
2. get expression data for each 2. get expression data for each
*/ */
const state = getState(); const { annoMatrix } = getState();
const { universe } = state.controls; const varIndexName = annoMatrix.schema.annotations.var.index;
const set1ByIndex = rangeEncodeIndices(
_.map(set1, s => universe.obsNameToIndexMap[s]) // Legal values are null, Array or TypedArray. Null is initial state.
); if (!set1) set1 = [];
const set2ByIndex = rangeEncodeIndices( if (!set2) set2 = [];
_.map(set2, s => universe.obsNameToIndexMap[s])
); // These lines ensure that we convert any TypedArray to an Array.
const res = await fetch( // This is necessary because JSON.stringify() does some very strange
`${globals.API.prefix}${globals.API.version}diffexp/obs`, // things with TypedArrays (they are marshalled to JSON objects, rather
{ // than being marshalled as a JSON array).
method: "POST", set1 = Array.isArray(set1) ? set1 : Array.from(set1);
headers: new Headers({ set2 = Array.isArray(set2) ? set2 : Array.from(set2);
Accept: "application/json",
"Content-Type": "application/json" const res = await fetch(
}), `${globals.API.prefix}${globals.API.version}diffexp/obs`,
body: JSON.stringify({ {
mode: "topN", method: "POST",
count: num_genes, headers: new Headers({
set1: { filter: { obs: { index: set1ByIndex } } }, Accept: "application/json",
set2: { filter: { obs: { index: set2ByIndex } } } "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") { const response = await res.json();
return dispatchDiffExpErrors(dispatch, res); 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 data = await res.json(); function fetchJson(pathAndQuery) {
// result is [ [varIdx, ...], ... ] return doJsonRequest(
const topNGenes = _.map(data, r => universe.varAnnotations[r[0]].name); `${globals.API.prefix}${globals.API.version}${pathAndQuery}`
);
/* }
Kick off secondary action to fetch all of the expression data for the
topN expressed genes.
*/
await _doRequestExpressionData(dispatch, getState, topNGenes);
/* 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
});
}
};
const resetInterface = () => (dispatch, getState) => {
const { universe } = getState().controls;
dispatch({
type: "clear all user defined genes"
});
dispatch({
type: "clear differential expression"
});
dispatch({
type: "reset colorscale"
});
dispatch({
type: "clear scatterplot"
});
dispatch({
type: "reset World to eq Universe",
universe
});
dispatch({
type: "increment graph render counter"
});
};
export default { export default {
regraph, doInitialDataLoad,
resetInterface,
requestSingleGeneExpressionCountsForColoringPOST,
requestDifferentialExpression, requestDifferentialExpression,
requestSingleGeneExpressionCountsForColoringPOST,
requestUserDefinedGene, requestUserDefinedGene,
doInitialDataLoad selectContinuousMetadataAction: selnActions.selectContinuousMetadataAction,
selectCategoricalMetadataAction: selnActions.selectCategoricalMetadataAction,
selectCategoricalAllMetadataAction:
selnActions.selectCategoricalAllMetadataAction,
graphBrushStartAction: selnActions.graphBrushStartAction,
graphBrushChangeAction: selnActions.graphBrushChangeAction,
graphBrushDeselectAction: selnActions.graphBrushDeselectAction,
graphBrushCancelAction: selnActions.graphBrushCancelAction,
graphBrushEndAction: selnActions.graphBrushEndAction,
graphLassoStartAction: selnActions.graphLassoStartAction,
graphLassoEndAction: selnActions.graphLassoEndAction,
graphLassoCancelAction: selnActions.graphLassoCancelAction,
graphLassoDeselectAction: selnActions.graphLassoDeselectAction,
clipAction: viewActions.clipAction,
subsetAction: viewActions.subsetAction,
resetSubsetAction: viewActions.resetSubsetAction,
annotationCreateCategoryAction: annoActions.annotationCreateCategoryAction,
annotationRenameCategoryAction: annoActions.annotationRenameCategoryAction,
annotationDeleteCategoryAction: annoActions.annotationDeleteCategoryAction,
annotationCreateLabelInCategory: annoActions.annotationCreateLabelInCategory,
annotationDeleteLabelFromCategory:
annoActions.annotationDeleteLabelFromCategory,
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,
}; };

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