From 86a6e519697fbc2586da0de0953b4c8e78f2b34e Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla Date: Tue, 5 Mar 2024 14:51:32 -0500 Subject: [PATCH 01/57] feat: make CNV file optional (#132) --- docs/docs/loading-data/through-data-config.md | 2 +- src/data/samples.ts | 2 +- src/main-spec.ts | 4 ++-- src/ui/sample-config-form.tsx | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/docs/loading-data/through-data-config.md b/docs/docs/loading-data/through-data-config.md index d4a0eace..fbbe6a18 100644 --- a/docs/docs/loading-data/through-data-config.md +++ b/docs/docs/loading-data/through-data-config.md @@ -23,7 +23,7 @@ For each sample, you need to prepare the following information in a JSON object. | `cancer` | `string` | Required. Type of a cancer. | | `assembly` | `'hg38'` or `'hg19'` | Required. Assembly. | | `sv` | `string` | Required. An URL of the SV bedpe file (`.bedpe`). | -| `cnv` | `string` | Required. An URL of the CNV text file (`.tsv`). | +| `cnv` | `string` | Optional. An URL of the CNV text file (`.tsv`). | | `drivers` | `string` | Optional. An URL of a file that contains drivers (`.tsv` or `.json`). | | `vcf` | `string` | Optional. An URL of the point mutation file (`.vcf`). | | `vcfIndex` | `string` | Optional. An URL of the point mutation index file (`.tbi`). | diff --git a/src/data/samples.ts b/src/data/samples.ts index a152bf6a..c4bc19ea 100644 --- a/src/data/samples.ts +++ b/src/data/samples.ts @@ -20,7 +20,7 @@ export type SampleType = { cancer: string; // cancer type assembly: Assembly; // hg19 or 38 sv: string; // URL of bedpe - cnv: string; // URL of txt + cnv?: string; // URL of txt drivers?: { [k: string]: string | number }[] | string; bam?: string; bai?: string; diff --git a/src/main-spec.ts b/src/main-spec.ts index e4c97a40..b4a52d5d 100644 --- a/src/main-spec.ts +++ b/src/main-spec.ts @@ -315,9 +315,9 @@ function getOverviewSpec(option: SpecOption): View[] { }, tracks.driver(id, driversToTsvUrl(drivers), width, 40, 'top'), tracks.boundary('driver', 'top'), - tracks.gain(id, cnv, width, 40, 'top', cnFields), + cnv ? [tracks.gain(id, cnv, width, 40, 'top', cnFields)] : [], tracks.boundary('gain', 'top'), - tracks.loh(id, cnv, width, 40, 'top', cnFields), + cnv ? [tracks.loh(id, cnv, width, 40, 'top', cnFields)] : [], tracks.boundary('loh', 'top'), tracks.sv(id, sv, width, 80, 'top', selectedSvId) ] diff --git a/src/ui/sample-config-form.tsx b/src/ui/sample-config-form.tsx index 29b0211e..4fdf6d9b 100644 --- a/src/ui/sample-config-form.tsx +++ b/src/ui/sample-config-form.tsx @@ -24,8 +24,8 @@ const testOkay = { id: (_: SampleConfig) => _.id, cancer: (_: SampleConfig) => _.cancer, sv: (_: SampleConfig) => isValidUrl(_.sv), - cnv: (_: SampleConfig) => isValidUrl(_.cnv), // optional + cnv: (_: SampleConfig) => !_.cnv || isValidUrl(_.cnv), drivers: (_: SampleConfig) => !_.drivers || isValidUrl(_.drivers), vcf: (_: SampleConfig) => !_.vcf || isValidUrl(_.vcf), vcfIndex: (_: SampleConfig) => !_.vcfIndex || isValidUrl(_.vcfIndex), @@ -155,14 +155,13 @@ export default function SampleConfigForm(props: { onAdd: (config: ValidSampleCon />
- CNV* (.txt) + CNV (.txt)
{/* Required */} setSampleConfig({ ...sampleConfig, cnv: e.currentTarget.value })} value={sampleConfig.cnv} /> From 4c9f8bca4d7e535e8e06860471ad17c2e7599ca4 Mon Sep 17 00:00:00 2001 From: sehilyi Date: Tue, 5 Mar 2024 15:01:14 -0500 Subject: [PATCH 02/57] fix: cnv track --- src/main-spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main-spec.ts b/src/main-spec.ts index b4a52d5d..63720449 100644 --- a/src/main-spec.ts +++ b/src/main-spec.ts @@ -315,9 +315,9 @@ function getOverviewSpec(option: SpecOption): View[] { }, tracks.driver(id, driversToTsvUrl(drivers), width, 40, 'top'), tracks.boundary('driver', 'top'), - cnv ? [tracks.gain(id, cnv, width, 40, 'top', cnFields)] : [], + ...(cnv ? [tracks.gain(id, cnv, width, 40, 'top', cnFields)] : []), tracks.boundary('gain', 'top'), - cnv ? [tracks.loh(id, cnv, width, 40, 'top', cnFields)] : [], + ...(cnv ? [tracks.loh(id, cnv, width, 40, 'top', cnFields)] : []), tracks.boundary('loh', 'top'), tracks.sv(id, sv, width, 80, 'top', selectedSvId) ] From 4a550fd545e5e4f8522a5d6aa5048e4bd4364b4c Mon Sep 17 00:00:00 2001 From: dominikglodzikhms <82227017+dominikglodzikhms@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:40:04 -0400 Subject: [PATCH 03/57] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4f98c17a..1a2642b7 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ Open the browser to see the local documentation. ```sh http://localhost:3000/docs ``` +Chromoscope is funded in part through a grant awarded by [Innovation in Cancer Informatics.](https://www.the-ici-fund.org/) + + ## Citation From f98d0a9c859ffa7c6acc8de4a6a48be0297a4c6e Mon Sep 17 00:00:00 2001 From: dominikglodzikhms <82227017+dominikglodzikhms@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:40:34 -0400 Subject: [PATCH 04/57] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a2642b7..ea83468f 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,11 @@ Open the browser to see the local documentation. ```sh http://localhost:3000/docs ``` -Chromoscope is funded in part through a grant awarded by [Innovation in Cancer Informatics.](https://www.the-ici-fund.org/) - - ## Citation Please cite the [following publication](10.31219/osf.io/pyqrx): > Sehi L’Yi, Dominika Maziec, Victoria Stevens, Trevor Manz, Alexander Veit, Michele Berselli, Peter J. Park, Dominik Głodzik, and Nils Gehlenborg. Chromoscope: interactive multiscale visualization for structural variation in human genomes. Nat Methods 20, 1834–1835 (2023). https://doi.org/10.1038/s41592-023-02056-x + +Chromoscope is funded in part through a grant awarded by [Innovation in Cancer Informatics.](https://www.the-ici-fund.org/) +> From 822f4c699e2f4aefdb516d99edad49f6dbcc0c00 Mon Sep 17 00:00:00 2001 From: dominikglodzikhms <82227017+dominikglodzikhms@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:40:59 -0400 Subject: [PATCH 05/57] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea83468f..35844f25 100644 --- a/README.md +++ b/README.md @@ -57,5 +57,5 @@ http://localhost:3000/docs Please cite the [following publication](10.31219/osf.io/pyqrx): > Sehi L’Yi, Dominika Maziec, Victoria Stevens, Trevor Manz, Alexander Veit, Michele Berselli, Peter J. Park, Dominik Głodzik, and Nils Gehlenborg. Chromoscope: interactive multiscale visualization for structural variation in human genomes. Nat Methods 20, 1834–1835 (2023). https://doi.org/10.1038/s41592-023-02056-x +## Funding Chromoscope is funded in part through a grant awarded by [Innovation in Cancer Informatics.](https://www.the-ici-fund.org/) -> From ae0fc4f032f84b0b73c5561a97080e3e29c575a6 Mon Sep 17 00:00:00 2001 From: SEHI L'YI Date: Wed, 3 Apr 2024 13:52:52 -0400 Subject: [PATCH 06/57] Update index.md --- docs/src/pages/about/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/pages/about/index.md b/docs/src/pages/about/index.md index 2bfbc51e..c3df522a 100644 --- a/docs/src/pages/about/index.md +++ b/docs/src/pages/about/index.md @@ -18,3 +18,7 @@ Chromoscope is developed and maintained by the members of [Department of Biomedi When citing Chromoscope in your paper, please cite the [following publication](https://10.31219/osf.io/pyqrx). > Sehi L’Yi, Dominika Maziec, Victoria Stevens, Trevor Manz, Alexander Veit, Michele Berselli, Peter J. Park, Dominik Głodzik, and Nils Gehlenborg. Chromoscope: interactive multiscale visualization for structural variation in human genomes. _Nat Methods_ 20, 1834–1835 (2023). https://doi.org/10.1038/s41592-023-02056-x + +## Funding + +Chromoscope is funded in part through a grant awarded by [Innovation in Cancer Informatics](https://www.the-ici-fund.org/) From bf823449533912806e1df257292d5e489a5b8741 Mon Sep 17 00:00:00 2001 From: SEHI L'YI Date: Wed, 17 Apr 2024 10:07:31 -0400 Subject: [PATCH 07/57] fix: update PTEN position based on hg38 (#140) --- src/data/driver.custom.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/driver.custom.json b/src/data/driver.custom.json index 6e59c89d..9d0bfb9c 100644 --- a/src/data/driver.custom.json +++ b/src/data/driver.custom.json @@ -90,7 +90,7 @@ { "gene": "PTEN", "chr": "chr10", - "pos": 89677278, + "pos": 87917284, "category": "deletion", "biallelic": "yes" } From 3104b02943ba9db0a4495eabf6beb98da67c851c Mon Sep 17 00:00:00 2001 From: SEHI L'YI Date: Wed, 17 Apr 2024 10:07:42 -0400 Subject: [PATCH 08/57] feat: make pe_support optional in sv bedpe data (#137) --- docs/docs/loading-data/data-formats.md | 6 +++--- src/data/samples.ts | 2 +- src/track/sv.ts | 12 ------------ 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/docs/docs/loading-data/data-formats.md b/docs/docs/loading-data/data-formats.md index 908a8159..1f847d18 100644 --- a/docs/docs/loading-data/data-formats.md +++ b/docs/docs/loading-data/data-formats.md @@ -9,7 +9,7 @@ This page describes file formats used in Chromoscope. To find a list of required ## Structural Variants (BEDPE) -The structural variants are stored in a BEDPE file. The following columns are used in the browser: +The structural variants are stored in a headed BEDPE file. The order of the columns does not need to be in the exact same order. This is a The following columns are used in the browser: | Property | Type | Note | |---|---|---| @@ -43,7 +43,7 @@ In Chromosope, strands are mapped with the following types of SVs. ## CNV (TSV) -The CNV is stored in a tab-delimited file that is visualized as three tracks: CNV, Gain, and LOH. +The CNV is stored in a headed tab-delimited file that is visualized as three tracks: CNV, Gain, and LOH. The order of the columns does not need to be in the exact same order. | Property | Type | Note | |---|---|---| @@ -63,7 +63,7 @@ https://s3.amazonaws.com/gosling-lang.org/data/SV/7a921087-8e62-4a93-a757-fd8cdb ## Drivers (TSV or JSON) -The drivers are stored in a tab-delimited file. When this file is present, the browser will show drivers that are included in the file only. +The drivers are stored in a headed tab-delimited file. When this file is present, the browser will show drivers that are included in the file only. The order of the columns does not need to be in the exact same order. diff --git a/src/data/samples.ts b/src/data/samples.ts index c4bc19ea..53bea3f1 100644 --- a/src/data/samples.ts +++ b/src/data/samples.ts @@ -1,4 +1,4 @@ -import { Assembly } from 'gosling.js/dist/src/core/gosling.schema'; +import { Assembly } from 'gosling.js/dist/src/gosling-schema'; import _7a921087 from '../script/img/7a921087-8e62-4a93-a757-fd8cdbe1eb8f.jpeg'; import _84ca6ab0 from '../script/img/84ca6ab0-9edc-4636-9d27-55cdba334d7d.jpeg'; import _7d332cb1 from '../script/img/7d332cb1-ba25-47e4-8bf8-d25e14f40d59.jpeg'; diff --git a/src/track/sv.ts b/src/track/sv.ts index 93e71ff5..c18f9bb5 100644 --- a/src/track/sv.ts +++ b/src/track/sv.ts @@ -102,18 +102,6 @@ export default function sv( url, type: 'csv', separator: '\t', - headerNames: [ - 'chrom1', - 'start1', - 'end1', - 'chrom2', - 'start2', - 'end2', - 'sv_id', - 'pe_support', - 'strand1', - 'strand2' - ], genomicFieldsToConvert: [ { chromosomeField: 'chrom1', From 83c293a7ac8af89eb9ca81e3c8bc4b7c81dcfcd3 Mon Sep 17 00:00:00 2001 From: dominikglodzikhms <82227017+dominikglodzikhms@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:02:35 -0400 Subject: [PATCH 09/57] Update driver.custom.json --- src/data/driver.custom.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/data/driver.custom.json b/src/data/driver.custom.json index 9d0bfb9c..66ee7b96 100644 --- a/src/data/driver.custom.json +++ b/src/data/driver.custom.json @@ -1,7 +1,7 @@ [ { "chr": "chr2", - "pos": 47806320, + "pos": 47795017, "ref": "G", "alt": "A", "gene": "MSH6", @@ -13,7 +13,7 @@ }, { "chr": "chr6", - "pos": 157133136, + "pos": 156993402, "ref": "G", "alt": "C", "gene": "ARID1B", @@ -26,7 +26,7 @@ }, { "chr": "chr8", - "pos": 38428420, + "pos": 38439986, "ref": "G", "alt": "A", "gene": "FGFR1", @@ -38,7 +38,7 @@ }, { "chr": "chr13", - "pos": 32339132, + "pos": 32357888, "ref": "G", "alt": "T", "gene": "BRCA2", @@ -51,7 +51,7 @@ }, { "chr": "chr17", - "pos": 7675088, + "pos": 7677976, "ref": "C", "alt": "T", "gene": "TP53", @@ -64,7 +64,7 @@ }, { "chr": "chrX", - "pos": 77681733, + "pos": 77645546, "ref": "T", "alt": "C", "gene": "ATRX", @@ -78,19 +78,19 @@ { "gene": "CDKN2A", "chr": "chr9", - "pos": 21981527, + "pos": 21981538, "category": "deletion" }, { "gene": "MET", "chr": "chr7", - "pos": 116735291, + "pos": 116735286, "category": "amplification" }, { "gene": "PTEN", "chr": "chr10", - "pos": 87917284, + "pos": 87917777, "category": "deletion", "biallelic": "yes" } From b00adcab7867fdae8c835c547383569e75d3a6e8 Mon Sep 17 00:00:00 2001 From: dominikglodzikhms <82227017+dominikglodzikhms@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:30:27 -0400 Subject: [PATCH 10/57] Update genome-view.md --- docs/docs/visualizations/genome-view.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/visualizations/genome-view.md b/docs/docs/visualizations/genome-view.md index b848d6df..9f6bea84 100644 --- a/docs/docs/visualizations/genome-view.md +++ b/docs/docs/visualizations/genome-view.md @@ -11,5 +11,5 @@ The genome view shows the selected sample in a circular visualization. This uses ## Interactions -- You can move or resize an interactive brush (light blue) using the mouse. This is linked with a [variant view](./cohort-view) that is shown on the bottom of the genome view. +- You can move or resize an interactive brush (light blue) using the mouse. This is linked with a [variant view](./variant-view) that is shown on the bottom of the genome view. - You can move your mouse on top of a structural variant to see detailed information on a tooltip. From 4d0191dd216494af9772f5db5c55f956e2a94091 Mon Sep 17 00:00:00 2001 From: SEHI L'YI Date: Thu, 16 May 2024 09:56:24 -0400 Subject: [PATCH 11/57] feat: upgrade gosling.js to 0.17.0 (#143) --- package.json | 2 +- yarn.lock | 337 ++++----------------------------------------------- 2 files changed, 22 insertions(+), 317 deletions(-) diff --git a/package.json b/package.json index cb297822..669609bd 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@types/react-dom": "^17.0.11", "@types/react-router-dom": "^5.2.0", "buffer": "^6.0.3", - "gosling.js": "^0.11.0", + "gosling.js": "^0.17.0", "idb": "^7.0.2", "lodash": "^4.17.21", "path": "^0.12.7", diff --git a/yarn.lock b/yarn.lock index c9bbcfbd..b06b008b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -558,231 +558,11 @@ estree-walker "^2.0.1" picomatch "^2.2.2" -"@types/bezier-js@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@types/bezier-js/-/bezier-js-4.1.0.tgz#e1b023cc0a6e53492d0249a0711a3e73d1dfc8fe" - integrity sha512-ElU16s8E6Pr6magp8ihwH1O8pbUJASbMND/qgUc9RsLmP3lMLHiDMRXdjtaObwW5GPtOVYOsXDUIhTIluT+yaw== - -"@types/d3-array@*": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.5.tgz#857c1afffd3f51319bbc5b301956aca68acaa7b8" - integrity sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A== - -"@types/d3-axis@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.2.tgz#96e11d51256baf5bdb2fa73a17d302993e79df07" - integrity sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-brush@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.2.tgz#a610aad5a1e76c375be63e11c5eee1ed9fd2fb40" - integrity sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-chord@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.2.tgz#cf6f05ad2d8faaad524e9e6f454b4fd06b200930" - integrity sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw== - -"@types/d3-color@*": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4" - integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA== - -"@types/d3-contour@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.2.tgz#d8a0e4d12ec14f7d2bb6e59f3fbc1a527457d0b2" - integrity sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ== - dependencies: - "@types/d3-array" "*" - "@types/geojson" "*" - -"@types/d3-delaunay@*": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz#006b7bd838baec1511270cb900bf4fc377bbbf41" - integrity sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ== - -"@types/d3-dispatch@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz#b2fa80bab3bcead68680766e966f59cd6cb9a69f" - integrity sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg== - -"@types/d3-drag@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.2.tgz#5562da3e7b33d782c2c1f9e65c5e91bb01ee82cf" - integrity sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-dsv@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.1.tgz#c51a3505cee42653454b74a00f8713dc3548c362" - integrity sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw== - -"@types/d3-ease@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" - integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== - -"@types/d3-fetch@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.2.tgz#fe1f335243e07c9bd520c9a71756fed8330c54b1" - integrity sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA== - dependencies: - "@types/d3-dsv" "*" - -"@types/d3-force@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.4.tgz#2d50bd2b695f709797e1745644f6bc123e6e5f5a" - integrity sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw== - -"@types/d3-format@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" - integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg== - -"@types/d3-geo@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.3.tgz#535e5f24be13722964c52354301be09b752f5d6e" - integrity sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw== - dependencies: - "@types/geojson" "*" - -"@types/d3-hierarchy@*": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b3a446b5437faededb30ac32b7cc0486559ab1e2" - integrity sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A== - -"@types/d3-interpolate@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" - integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== - dependencies: - "@types/d3-color" "*" - -"@types/d3-path@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" - integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== - -"@types/d3-polygon@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93" - integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw== - -"@types/d3-quadtree@*": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5" - integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw== - -"@types/d3-random@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953" - integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ== - -"@types/d3-scale-chromatic@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" - integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw== - -"@types/d3-scale@*": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5" - integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ== - dependencies: - "@types/d3-time" "*" - -"@types/d3-selection@*": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.5.tgz#27cd53b7672d405025e2414d98532d7934c16ebd" - integrity sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w== - -"@types/d3-shape@*": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.1.tgz#15cc497751dac31192d7aef4e67a8d2c62354b95" - integrity sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A== - dependencies: - "@types/d3-path" "*" - -"@types/d3-time-format@*": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946" - integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw== - -"@types/d3-time@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" - integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== - -"@types/d3-timer@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" - integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== - -"@types/d3-transition@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.3.tgz#d4ac37d08703fb039c87f92851a598ba77400402" - integrity sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA== - dependencies: - "@types/d3-selection" "*" - -"@types/d3-zoom@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.3.tgz#5c29006a61ff7ca512fe21398c66ad95dd846674" - integrity sha512-OWk1yYIIWcZ07+igN6BeoG6rqhnJ/pYe+R1qWFM2DtW49zsoSjgb9G5xB0ZXA8hh2jAzey1XuRmMSoXdKw8MDA== - dependencies: - "@types/d3-interpolate" "*" - "@types/d3-selection" "*" - -"@types/d3@^7.0.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.0.tgz#fc5cac5b1756fc592a3cf1f3dc881bf08225f515" - integrity sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA== - dependencies: - "@types/d3-array" "*" - "@types/d3-axis" "*" - "@types/d3-brush" "*" - "@types/d3-chord" "*" - "@types/d3-color" "*" - "@types/d3-contour" "*" - "@types/d3-delaunay" "*" - "@types/d3-dispatch" "*" - "@types/d3-drag" "*" - "@types/d3-dsv" "*" - "@types/d3-ease" "*" - "@types/d3-fetch" "*" - "@types/d3-force" "*" - "@types/d3-format" "*" - "@types/d3-geo" "*" - "@types/d3-hierarchy" "*" - "@types/d3-interpolate" "*" - "@types/d3-path" "*" - "@types/d3-polygon" "*" - "@types/d3-quadtree" "*" - "@types/d3-random" "*" - "@types/d3-scale" "*" - "@types/d3-scale-chromatic" "*" - "@types/d3-selection" "*" - "@types/d3-shape" "*" - "@types/d3-time" "*" - "@types/d3-time-format" "*" - "@types/d3-timer" "*" - "@types/d3-transition" "*" - "@types/d3-zoom" "*" - "@types/earcut@^2.1.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/earcut/-/earcut-2.1.1.tgz#573a0af609f17005c751f6f4ffec49cfe358ea51" integrity sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ== -"@types/geojson@*": - version "7946.0.10" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" - integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== - "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" @@ -793,16 +573,6 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== -"@types/lodash@^4.14.151": - version "4.14.195" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" - integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== - -"@types/node@^18.6.2": - version "18.16.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.19.tgz#cb03fca8910fdeb7595b755126a8a78144714eea" - integrity sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA== - "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -818,11 +588,6 @@ resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2" integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw== -"@types/rbush@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6" - integrity sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg== - "@types/react-dom@^17.0.11": version "17.0.11" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" @@ -861,11 +626,6 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== -"@types/uuid@^8.3.1": - version "8.3.4" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" - integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== - "@typescript-eslint/eslint-plugin@^4.29.2": version "4.29.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz#f54dc0a32b8f61c6024ab8755da05363b733838d" @@ -1459,7 +1219,7 @@ d3-dsv@^2.0.0: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== -"d3-format@1 - 3": +"d3-format@1 - 3", d3-format@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== @@ -1701,11 +1461,6 @@ email-addresses@^3.0.1: resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.1.0.tgz#cabf7e085cbdb63008a70319a74e6136188812fb" integrity sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg== -emitter-component@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6" - integrity sha512-G+mpdiAySMuB7kesVRLuyvYRqDmshB7ReKEVuyBPkzQlmiDiLrt7hHHIy4Aff552bgknVN7B2/d3lzhGO5dvpQ== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2433,15 +2188,10 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -gosling-theme@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/gosling-theme/-/gosling-theme-0.0.10.tgz#5a4c2d0d24f90fe47c293335cd67832be6cf4d54" - integrity sha512-GRm+XGeSGhKBjM/bNyeUkwp6tjnFKpNbywr58WzzA2qSA4RFPOoitS03R7xpYxqLXBGMFgahkyYd9Mostd2Csw== - -gosling.js@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/gosling.js/-/gosling.js-0.11.0.tgz#a9cca2db816cb03e487c85dfd1f575a1a0c9d5e0" - integrity sha512-i4aZF/mnZtgeyAS3pRFMrId75IpteuAziVqXR3mm4kaKimilIanXNfa2oxARGYynhSYh+CDcIc3ruAWOtYlhCQ== +gosling.js@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/gosling.js/-/gosling.js-0.17.0.tgz#68d9520b78c7a9a97a3db512ad836b9a3abf6cb1" + integrity sha512-AM63ZID/B/A5UUPqj72QBQRGYiuvEt8apOEvv0NPjCT5n8yxy2urNjvLqfGUOiwCMmQNfHuUDTtj0B1bNRPL1A== dependencies: "@gmod/bam" "^1.1.18" "@gmod/bbi" "^3.0.1" @@ -2449,12 +2199,6 @@ gosling.js@^0.11.0: "@gmod/gff" "^1.3.0" "@gmod/tabix" "^1.5.6" "@gmod/vcf" "^5.0.10" - "@types/bezier-js" "^4.1.0" - "@types/d3" "^7.0.0" - "@types/lodash" "^4.14.151" - "@types/node" "^18.6.2" - "@types/rbush" "^3.0.0" - "@types/uuid" "^8.3.1" allotment "^1.19.0" bezier-js "4.0.3" buffer "^6.0.3" @@ -2462,28 +2206,26 @@ gosling.js@^0.11.0: d3-array "^2.5.1" d3-color "^2.0.0" d3-dsv "^2.0.0" + d3-format "^3.1.0" d3-scale "^3.2.1" d3-scale-chromatic "^2.0.0" d3-shape "^2.0.0" events "^3.3.0" fflate "^0.7.1" generic-filehandle "^3.0.1" - gosling-theme "^0.0.10" - higlass "^1.12.4" + higlass "^1.13.4" higlass-register "^0.3.0" higlass-text "^0.1.1" json-stringify-pretty-compact "^2.0.0" jspdf "^2.3.1" lodash-es "^4.17.21" - mixwith "^0.1.1" + monaco-editor "^0.27.0" nanoevents "^7.0.1" pubsub-js "^1.9.3" quick-lru "^6.1.1" rbush "^3.0.1" - react-grid-layout "^1.2.5" stream-browserify "^3.0.0" threads "^1.6.4" - uuid "^8.3.2" graceful-fs@^4.1.6: version "4.2.11" @@ -2556,10 +2298,10 @@ higlass-text@^0.1.1: higlass-register "^0.3.0" slugid "^2.0.0" -higlass@^1.12.4: - version "1.12.4" - resolved "https://registry.yarnpkg.com/higlass/-/higlass-1.12.4.tgz#e494908a2316055ba31f01dfba4205da930eaf33" - integrity sha512-MfjB6RHjTAqWxi5PjoyetnIigdzjcnTBzOdCSaU5ek6G3ewytuXBLD6KIOKUGBRT2D8cfb4Cz/TP6ZzSDBd3kw== +higlass@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/higlass/-/higlass-1.13.4.tgz#3bc837713bce99cc8e8e05fea8a60555e18cec56" + integrity sha512-mIr+Yi5aR0zBAB9iqlqTz/K3ZusrGz8f20g5DHaaRZ/FzMOcFsdL+9ESIPndPZrN2Y/GQ4CHilNjPTTSncIOTg== dependencies: ajv "^6.10.0" box-intersect "^1.0.1" @@ -2589,15 +2331,12 @@ higlass@^1.12.4: pub-sub-es "^2.0.1" react-checkbox-tree "^1.7.3" react-color "^2.13.8" - react-contextmenu "^2.9.2" react-grid-layout "^0.16.6" - react-resizable "^1.8.0" react-simple-code-editor "^0.9.10" react-sortable-hoc "^1.10.1" reactcss "^1.2.3" robust-point-in-polygon "^1.0.3" - slugid "^3.1.0" - stream "0.0.2" + slugid "^3.2.0" url-parse "^1.4.3" vkbeautify "^0.99.3" @@ -3110,10 +2849,10 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mixwith@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/mixwith/-/mixwith-0.1.1.tgz#c8995918c5b61fbfda9ad377a857cd47750541c0" - integrity sha512-DQsf/liljH/9e+94jR+xfK8vlKceeKdOM9H9UEXLwGuvEEpO6debNtJ9yt1ZKzPKPrwqGxzMdu0BR1fnQb6i4A== +monaco-editor@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.27.0.tgz#4b69108bb1dc1f60174c5dcdf51bc5306ab5ba26" + integrity sha512-UhwP78Wb8w0ZSYoKXQNTV/0CHObp6NS3nCt51QfKE6sKyBo5PBsvuDOHoI2ooBakc6uIwByRLHVeT7+yXQe2fQ== ms@2.1.2: version "2.1.2" @@ -3160,7 +2899,7 @@ node-releases@^1.1.73: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.74.tgz#e5866488080ebaa70a93b91144ccde06f3c3463e" integrity sha512-caJBVempXZPepZoZAPCWRTNxYQ+xtG/KAi4ozTA5A+nJ7IU+kLQCbqaUjb5Rwy14M9upBWiQ4NutcmW04LJSRw== -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -3588,14 +3327,6 @@ react-color@^2.13.8: reactcss "^1.2.0" tinycolor2 "^1.4.1" -react-contextmenu@^2.9.2: - version "2.14.0" - resolved "https://registry.yarnpkg.com/react-contextmenu/-/react-contextmenu-2.14.0.tgz#d8966f30614b9b780b928be4c8d92bd740d55cdd" - integrity sha512-ktqMOuad6sCFNJs/ltEwppN8F0YeXmqoZfwycgtZR/MxOXMYx1xgYC44SzWH259HdGyshk1/7sXGuIRwj9hzbw== - dependencies: - classnames "^2.2.5" - object-assign "^4.1.0" - react-dom@16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" @@ -3614,7 +3345,7 @@ react-draggable@3.x: classnames "^2.2.5" prop-types "^15.6.0" -react-draggable@^4.0.0, react-draggable@^4.0.3: +react-draggable@^4.0.3: version "4.4.5" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c" integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g== @@ -3633,17 +3364,6 @@ react-grid-layout@^0.16.6: react-draggable "3.x" react-resizable "1.x" -react-grid-layout@^1.2.5: - version "1.3.4" - resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff" - integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw== - dependencies: - clsx "^1.1.1" - lodash.isequal "^4.0.0" - prop-types "^15.8.1" - react-draggable "^4.0.0" - react-resizable "^3.0.4" - react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -3654,7 +3374,7 @@ react-refresh@^0.10.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3" integrity sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ== -react-resizable@1.x, react-resizable@^1.8.0: +react-resizable@1.x: version "1.11.1" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.11.1.tgz#02ca6850afa7a22c1b3e623e64aef71ee252af69" integrity sha512-S70gbLaAYqjuAd49utRHibtHLrHXInh7GuOR+6OO6RO6uleQfuBnWmZjRABfqNEx3C3Z6VPLg0/0uOYFrkfu9Q== @@ -3662,14 +3382,6 @@ react-resizable@1.x, react-resizable@^1.8.0: prop-types "15.x" react-draggable "^4.0.3" -react-resizable@^3.0.4: - version "3.0.5" - resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1" - integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w== - dependencies: - prop-types "15.x" - react-draggable "^4.0.3" - react-router-dom@^5.2.0: version "5.3.3" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.3.tgz#8779fc28e6691d07afcaf98406d3812fe6f11199" @@ -3969,7 +3681,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -slugid@^2.0.0, slugid@^3.0.0, slugid@^3.1.0: +slugid@^2.0.0, slugid@^3.0.0, slugid@^3.2.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slugid/-/slugid-3.0.0.tgz#57cb692641a20feb4a1d774b2ad11594c9aa6125" integrity sha512-MIir9SJS6BQFjFg8DHGMgPkDg21huT0+kYHiS6NkkWZXHSEdgIECo3nUVEG3GDkmecBTy+zRs6pCvPhpk4bKcg== @@ -4004,13 +3716,6 @@ stream-browserify@^3.0.0: inherits "~2.0.4" readable-stream "^3.5.0" -stream@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/stream/-/stream-0.0.2.tgz#7f5363f057f6592c5595f00bc80a27f5cec1f0ef" - integrity sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g== - dependencies: - emitter-component "^1.1.1" - string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" From d1c1969694e5e5317239d62177915e823a98e499 Mon Sep 17 00:00:00 2001 From: Cesar Ferreyra-Mansilla Date: Thu, 16 May 2024 12:12:38 -0400 Subject: [PATCH 12/57] feat: minimal_mode for iframe embedding (#138) --- src/App.css | 261 +++++++ src/App.tsx | 1397 ++++++++++++++++++++----------------- src/icon.ts | 8 + src/main-spec.ts | 5 +- src/ui/ExportDropdown.tsx | 90 +++ 5 files changed, 1138 insertions(+), 623 deletions(-) create mode 100644 src/ui/ExportDropdown.tsx diff --git a/src/App.css b/src/App.css index 533e4501..03dd2148 100644 --- a/src/App.css +++ b/src/App.css @@ -224,6 +224,7 @@ a:hover { border: 1px solid grey; position: absolute; left: 3px; + scroll-margin-top: 50px; } .nav-dropdown:focus { @@ -747,3 +748,263 @@ a:hover { opacity: 1; background-color: #7aaded; } + +.track-mouseover-menu { + z-index: 999; +} + +/* Styles for the variant view controls */ +.variant-view-controls { + position: absolute; + display: flex; + flex-direction: row; + height: 30px; + justify-content: center; + left: 3px; + + .chromosome-select { + position: relative; + height: auto; + left: 0px; + } + .gene-search { + position: relative; + left: 0px; + padding: 0px; + width: auto; + height: auto; + display: flex; + + svg { + position: absolute; + top: 50%; + transform: translate(0px, -50%); + left: 8px; + margin: auto; + margin-left: 0px; + } + input { + position: relative; + left: 0; + margin-left: 0; + border: none; + width: 180px; + padding-left: 35px; + } + } + .directional-controls { + display: flex; + margin: auto 0px auto 16px; + gap: 16px; + .control-group { + display: flex; + .control { + position: relative; + left: 0px; + margin-left: 0px; + line-height: 28px; + } + } + } +} + +/* Minimal Mode styles */ +.minimal_mode { + .gosling-panel { + overflow-y: scroll; + overflow-x: hidden; + } + + .sample-label { + left: 300px; + top: 8px; + } + + .navigation-buttons { + position: fixed; + z-index: 998; + display: flex; + flex-direction: column; + top: 3px; + left: 3px; + } + + .navigation-button { + background-color: #f6f6f6; + cursor: pointer; + font-size: 1rem; + font-family: Inter; + height: 40px; + width: 210px; + padding: 2px 10px; + border: 1px solid #d3d3d3; + } + + .navigation-button:hover:not(:disabled) { + background-color: #ebebeb; + } + .navigation-button:active:not(:disabled) { + background-color: #e6e4e4; + } + .navigation-button:first-of-type { + border-radius: 8px 8px 0px 0px; + } + .navigation-button:last-of-type { + border-radius: 0px 0px 8px 8px; + } + + /* Force scrollbar to show */ + ::-webkit-scrollbar { + -webkit-appearance: none; + width: 10px; + } + + ::-webkit-scrollbar-thumb { + width: 10px; + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.5); + box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); + } + ::-webkit-scrollbar:hover { + cursor: pointer; + } + + /* Styles for the navigation on the right side of visualization */ + .external-links { + position: absolute; + z-index: 998; + height: auto; + width: auto; + top: 3px; + right: 12px; + + .external-links-nav { + display: flex; + flex-direction: column; + justify-content: space-between; + + .open-in-chromoscope-link { + background-color: #f6f6f6; + font-size: 0.9rem; + font-family: Inter; + font-weight: 400; + display: flex; + height: 35px; + justify-content: center; + border: 1px solid #d3d3d3; + border-radius: 8px; + + .link-group { + margin: auto; + + .external-link-icon { + margin: auto; + margin-left: 8px; + fill: black; + stroke: black; + } + } + } + + .open-in-chromoscope-link:hover { + text-decoration: none; + cursor: pointer; + background-color: #ebebeb; + } + + .open-in-chromoscope-link:active { + background-color: #e6e4e4; + } + + .export-links { + border-radius: 4px; + margin-top: 4px; + + .export-dropdown { + height: auto; + background-color: #f6f6f6; + right: 0px; + border-radius: 8px; + border: 1px solid #d3d3d3; + transition: all 100ms; + overflow: hidden; + + .export-button { + width: 210px; + height: 35px; + border-radius: inherit; + border: 0px solid; + font-weight: 400; + background-color: transparent; + + .export-title { + font-size: 0.9rem; + font-family: Inter; + } + + .button.triangle-down { + width: 11px; + height: 7px; + margin-left: 8px; + } + } + + .export-button:hover { + cursor: pointer; + background-color: #ebebeb; + } + + .export-button:active { + background-color: #e6e4e4; + } + + .nav-list { + list-style-type: none; + padding: 0px 10px; + display: flex; + flex-direction: row; + height: 50px; + background-color: white; + margin: 0px 8px 8px 8px; + border-radius: 3px; + + .nav-list-item { + display: flex; + margin: auto; + } + + .title-btn { + display: flex; + position: relative; + width: 25px; + height: 25px; + margin-left: 0px; + } + + .title-btn.png { + padding: 0px; + border: none; + background-color: transparent; + } + } + } + + .export-dropdown.open { + background-color: #ebebeb; + border-radius: 8px; + border: 1px solid #c3c3c3; + transition: all 100ms; + + .export-button { + border: none; + } + } + } + } + } + + .variant-view-controls { + left: 50%; + transform: translate(-50%, 0px); + } +} diff --git a/src/App.tsx b/src/App.tsx index e307c00a..431fb7c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import HorizontalLine from './ui/horizontal-line'; import SampleConfigForm from './ui/sample-config-form'; import { BrowserDatabase } from './browser-log'; import legend from './legend.png'; +import { ExportDropdown } from './ui/ExportDropdown'; const db = new Database(); const log = new BrowserDatabase(); @@ -31,7 +32,6 @@ const DATABSE_THUMBNAILS = await db.get(); const GENERATED_THUMBNAILS = {}; const INIT_VIS_PANEL_WIDTH = window.innerWidth; -const VIS_PADDING = 60; const ZOOM_PADDING = 200; const ZOOM_DURATION = 500; @@ -48,6 +48,14 @@ const allDrivers = [ function App(props: RouteComponentProps) { // URL parameters const urlParams = new URLSearchParams(props.location.search); + const isMinimalMode = urlParams.get('minimal_mode') === 'true'; + const VIS_PADDING = { + top: isMinimalMode ? 0 : 60, + right: isMinimalMode ? 0 : 60, + bottom: isMinimalMode ? 0 : 60, + left: isMinimalMode ? 0 : 60 + }; + // !! instead of using `urlParams.get('external')`, we directly parse the external URL in order to include // any inlined parameters of the external link (e.g., private AWS link with authentication info.) let externalUrl = null; @@ -94,8 +102,10 @@ function App(props: RouteComponentProps) { const [filteredSamples, setFilteredSamples] = useState(selectedSamples); const [showOverview, setShowOverview] = useState(true); const [showPutativeDriver, setShowPutativeDriver] = useState(true); - const [interactiveMode, setInteractiveMode] = useState(false); - const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - VIS_PADDING * 2); + const [interactiveMode, setInteractiveMode] = useState(isMinimalMode ?? false); + const [visPanelWidth, setVisPanelWidth] = useState( + INIT_VIS_PANEL_WIDTH - (isMinimalMode ? 10 : VIS_PADDING.left * 2) + ); const [overviewChr, setOverviewChr] = useState(''); const [genomeViewChr, setGenomeViewChr] = useState(''); const [drivers, setDrivers] = useState( @@ -104,7 +114,7 @@ function App(props: RouteComponentProps) { const [selectedSvId, setSelectedSvId] = useState(''); const [breakpoints, setBreakpoints] = useState<[number, number, number, number]>([1, 100, 1, 100]); const [bpIntervals, setBpIntervals] = useState<[number, number, number, number] | undefined>(); - const [mouseOnVis, setMouseOnVis] = useState(false); + const [mouseOnVis, setMouseOnVis] = useState(isMinimalMode ?? false); const [jumpButtonInfo, setJumpButtonInfo] = useState<{ id: string; x: number; y: number; direction: 'leftward' | 'rightward'; zoomTo: () => void }>(); const mousePos = useRef({ x: -100, y: -100 }); @@ -297,14 +307,31 @@ function App(props: RouteComponentProps) { } }, [genomeViewChr]); - // change the width of the visualization panel + // change the width of the visualization panel and register intersection observer useEffect(() => { window.addEventListener( 'resize', debounce(() => { - setVisPanelWidth(window.innerWidth - VIS_PADDING * 2); + setVisPanelWidth(window.innerWidth - VIS_PADDING.left * 2); }, 500) ); + + // Lower opacity of legend image as it leaves viewport + if (isMinimalMode) { + const legendElement = document.querySelector('.genome-view-legend'); + const options = { + root: document.querySelector('.minimal_mode'), + rootMargin: '-250px 0px 0px 0px', + threshold: [0.9, 0.75, 0.5, 0.25, 0] + }; + + const observer = new IntersectionObserver(entry => { + // Set intersection ratio as opacity (round up to one decimal place) + legendElement.style.opacity = '' + Math.ceil(10 * entry[0].intersectionRatio) / 10; + }, options); + + observer.observe(legendElement); + } }, []); const getThumbnail = (d: SampleType) => { @@ -488,7 +515,8 @@ function App(props: RouteComponentProps) { breakpoints: breakpoints, crossChr: false, bpIntervals, - svReads + svReads, + spacing: isMinimalMode ? 100 : 40 }); currentSpec.current = JSON.stringify(spec); // console.log('spec', spec); @@ -612,21 +640,24 @@ function App(props: RouteComponentProps) { return (
{ const top = e.clientY; const left = e.clientX; const width = window.innerWidth; const height = window.innerHeight; - if ( - VIS_PADDING < top && - top < height - VIS_PADDING && - VIS_PADDING < left && - left < width - VIS_PADDING - ) { - setMouseOnVis(true); - } else { - setMouseOnVis(false); + if (!isMinimalMode) { + if ( + VIS_PADDING.top < top && + top < height - VIS_PADDING.top && + VIS_PADDING.left < left && + left < width - VIS_PADDING.left + ) { + setMouseOnVis(true); + } else { + setMouseOnVis(false); + } } mousePos.current = { x: left, y: top }; }} @@ -637,139 +668,153 @@ function App(props: RouteComponentProps) { setJumpButtonInfo(undefined); }} > - - { - setShowSamples(true); - }} - > - Menu - - -
- - CHROMOSCOPE - - { - setShowAbout(true); - }} - > - - - - About - - {' | '} - {/* {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1) + ' • ' + demo.id} */} - {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1)} - {demo.id} - gosRef.current?.api.exportPng()}> - - Export Image - {ICONS.PNG.path.map(p => ( - - ))} - - + {!isMinimalMode && ( { - const a = document.createElement('a'); - a.setAttribute( - 'href', - `data:text/plain;charset=utf-8,${encodeURIComponent( - getHtmlTemplate(currentSpec.current) - )}` - ); - a.download = 'visualization.html'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + style={{ + height: '50px', + width: '100%', + background: 'white', + position: 'absolute', + zIndex: 999, + opacity: 0.8 }} - style={{ marginLeft: 40 }} - > - - Export HTML - {ICONS.HTML.path.map(p => ( - - ))} - - - + )} + {!isMinimalMode && ( + { - const a = document.createElement('a'); - a.setAttribute( - 'href', - `data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}` - ); - a.download = 'visualization.json'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + setShowSamples(true); }} - style={{ marginLeft: 70 }} > - - Export Gosling Spec (JSON) - {ICONS.JSON.path.map(p => ( - - ))} - - - { - const { xDomain } = gosRef.current.hgApi.api.getLocation(`${demo.id}-mid-ideogram`); - if (xDomain) { - // urlParams.set('demoIndex', demoIndex.current + ''); - // urlParams.set('domain', xDomain.join('-')); - let newUrl = window.location.origin + window.location.pathname + '?'; - newUrl += `demoIndex=${demoIndex.current}`; - newUrl += `&domain=${xDomain.join('-')}`; - if (externalDemoUrl.current) { - newUrl += `&external=${externalDemoUrl.current}`; - } else if (externalUrl) { - newUrl += `&external=${externalUrl}`; - } - navigator.clipboard - .writeText(newUrl) - .then(() => - alert('The URL of the current session has been copied to your clipboard.') + Menu + + + )} +
+ {!isMinimalMode && ( + <> + + CHROMOSCOPE + + { + setShowAbout(true); + }} + > + + + + About + + {' | '} + {/* {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1) + ' • ' + demo.id} */} + {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1)} + {demo.id} + + )} + {!isMinimalMode && ( + <> + gosRef.current?.api.exportPng()}> + + Export Image + {ICONS.PNG.path.map(p => ( + + ))} + + + { + const a = document.createElement('a'); + a.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent( + getHtmlTemplate(currentSpec.current) + )}` ); - } - }} - style={{ marginLeft: 100 }} - > - - Export Link - - - - + a.download = 'visualization.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }} + style={{ marginLeft: 40 }} + > + + Export HTML + {ICONS.HTML.path.map(p => ( + + ))} + + + { + const a = document.createElement('a'); + a.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}` + ); + a.download = 'visualization.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }} + style={{ marginLeft: 70 }} + > + + Export Gosling Spec (JSON) + {ICONS.JSON.path.map(p => ( + + ))} + + + { + const { xDomain } = gosRef.current.hgApi.api.getLocation(`${demo.id}-mid-ideogram`); + if (xDomain) { + // urlParams.set('demoIndex', demoIndex.current + ''); + // urlParams.set('domain', xDomain.join('-')); + let newUrl = window.location.origin + window.location.pathname + '?'; + newUrl += `demoIndex=${demoIndex.current}`; + newUrl += `&domain=${xDomain.join('-')}`; + if (externalDemoUrl.current) { + newUrl += `&external=${externalDemoUrl.current}`; + } else if (externalUrl) { + newUrl += `&external=${externalUrl}`; + } + navigator.clipboard + .writeText(newUrl) + .then(() => + alert( + 'The URL of the current session has been copied to your clipboard.' + ) + ); + } + }} + style={{ marginLeft: 100 }} + > + + Export Link + + + + + + )} {!isChrome() ? ( ) : null} - - - GitHub - - - GitHub - - - - - - Documentation - -
-
-
-
{ - if (e.target === e.currentTarget) setShowSamples(false); - }} - > - { - setShowSamples(false); - }} - > - Close - - -
- CHROMOSCOPE - { - setShowAbout(true); - }} - > - - - - About - - {' | '} Samples - setFilterSampleBy(e.target.value)} - hidden - /> -
+ {!isMinimalMode && ( + <> Documentation -
+
+ {!isMinimalMode && ( +
+
{ + if (e.target === e.currentTarget) setShowSamples(false); + }} > - {generateThumbnails ? 'Stop Generating Thumbnails' : 'Generate Missing Thumbnails'} - -
-
-
- { - fetch(url).then(response => - response.text().then(d => { - let externalDemo = JSON.parse(d); - if (Array.isArray(externalDemo) && externalDemo.length >= 0) { - setFilteredSamples(externalDemo); - externalDemo = - externalDemo[ - demoIndex.current < externalDemo.length - ? demoIndex.current - : 0 - ]; - } - if (externalDemo) { - externalDemoUrl.current = url; - setDemo(externalDemo); - } - }) - ); - }} - /> - - { - setFilteredSamples([ - { - ...config, - group: 'default' - }, - ...filteredSamples - ]); + { + setShowSamples(false); }} - /> + > + Close + + +
+ CHROMOSCOPE + { + setShowAbout(true); + }} + > + + + + About + + {' | '} Samples + setFilterSampleBy(e.target.value)} + hidden + /> +
+ + + GitHub + + + GitHub + + + + + + Documentation + + +
+
+
+ { + fetch(url).then(response => + response.text().then(d => { + let externalDemo = JSON.parse(d); + if (Array.isArray(externalDemo) && externalDemo.length >= 0) { + setFilteredSamples(externalDemo); + externalDemo = + externalDemo[ + demoIndex.current < externalDemo.length + ? demoIndex.current + : 0 + ]; + } + if (externalDemo) { + externalDemoUrl.current = url; + setDemo(externalDemo); + } + }) + ); + }} + /> + + { + setFilteredSamples([ + { + ...config, + group: 'default' + }, + ...filteredSamples + ]); + }} + /> +
+
{`Total of ${filteredSamples.length} samples loaded`}
+
{smallOverviewWrapper}
-
{`Total of ${filteredSamples.length} samples loaded`}
-
{smallOverviewWrapper}
-
+ )}
{goslingComponent} @@ -968,277 +1024,374 @@ function App(props: RouteComponentProps) { {jumpButtonInfo.direction === 'leftward' ? '←' : '→'} ) : null} -
+ {!isMinimalMode && ( +
+ )} + {isMinimalMode ? ( +
+ + +
+ ) : null} + { + // External links and export buttons + isMinimalMode ? ( +
+ +
+ ) : null + }
- - - - - - { - // const keyword = e.target.value; - // if(keyword !== "" && !keyword.startsWith("c")) { - // gosRef.current.api.suggestGene(keyword, (suggestions) => { - // setGeneSuggestions(suggestions); - // }); - // setSuggestionPosition({ - // left: searchBoxRef.current.getBoundingClientRect().left, - // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, - // }); - // } else { - // setGeneSuggestions([]); - // } - // setSearchKeyword(keyword); - // }} - onKeyDown={e => { - const keyword = (e.target as HTMLTextAreaElement).value; - switch (e.key) { - case 'ArrowUp': - break; - case 'ArrowDown': - break; - case 'Enter': - // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 - gosRef.current.hgApi.api.zoomToGene( - `${demo.id}-mid-ideogram`, - keyword, - 10000, - 1000 - ); - break; - case 'Esc': - case 'Escape': - break; - } - }} - /> - - - - + +
+ + + + { + // const keyword = e.target.value; + // if(keyword !== "" && !keyword.startsWith("c")) { + // gosRef.current.api.suggestGene(keyword, (suggestions) => { + // setGeneSuggestions(suggestions); + // }); + // setSuggestionPosition({ + // left: searchBoxRef.current.getBoundingClientRect().left, + // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, + // }); + // } else { + // setGeneSuggestions([]); + // } + // setSearchKeyword(keyword); + // }} + onKeyDown={e => { + const keyword = (e.target as HTMLTextAreaElement).value; + switch (e.key) { + case 'ArrowUp': + break; + case 'ArrowDown': + break; + case 'Enter': + // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 + gosRef.current.hgApi.api.zoomToGene( + `${demo.id}-mid-ideogram`, + keyword, + 10000, + 1000 + ); + break; + case 'Esc': + case 'Escape': + break; + } + }} + /> +
+
+
+ + +
+
+ + +
+
+
-
- {!interactiveMode - ? 'Click inside to use interactions on visualizations' - : 'Click outside to deactivate interactions and scroll the page'} -
-
+ {!isMinimalMode && ( +
+ {!interactiveMode + ? 'Click inside to use interactions on visualizations' + : 'Click outside to deactivate interactions and scroll the page'} +
+ )} + {!isMinimalMode && ( +
+ )}
setShowAbout(false)} /> -
- -

- Chromoscope - {' | '}About -

+ {!isMinimalMode && ( +
+ +

+ Chromoscope + {' | '}About +

-

- Whole genome sequencing is now routinely used to profile mutations in DNA in the soma and in the - germline, informing molecular diagnoses of disease and therapeutic decisions. Structural - variants (SVs) are the main new type of alterations we see more of, and they are often - diagnostic, prognostic, or therapy-informing. However, the size and complexity of SV data, - combined with the difficulty of obtaining accurate SV calls, pose challenges in the - interpretation of SVs, requiring tedious visual inspection of potentially pathogenic variants - with multiple visualization tools. -

+

+ Whole genome sequencing is now routinely used to profile mutations in DNA in the soma and in + the germline, informing molecular diagnoses of disease and therapeutic decisions. Structural + variants (SVs) are the main new type of alterations we see more of, and they are often + diagnostic, prognostic, or therapy-informing. However, the size and complexity of SV data, + combined with the difficulty of obtaining accurate SV calls, pose challenges in the + interpretation of SVs, requiring tedious visual inspection of potentially pathogenic + variants with multiple visualization tools. +

-

- To overcome the problems with the interpretation of SVs, we developed Chromoscope, an - open-source web-based application for the interactive visualization of structural variants. - Chromoscope has several innovative features which unlock the insights from whole genome - sequencing: visualization at multiple scale levels simultaneously, effective navigation across - scales, easy setup for loading users' large datasets, and a feature to export, share, and - further customize visualizations. We anticipate that Chromoscope will accelerate the exploration - and interpretation of SVs by a broad range of scientists and clinicians, leading to new insights - into genomic biomarkers. -

-

Learn more about Chromoscope

- -

The Team

-
    -
  • - Sehi L'Yi - {hidiveLabRef} -
  • -
  • - Dominika Maziec - {parkLabRef} -
  • -
  • - Victoria Stevens - {parkLabRef} -
  • -
  • - Trevor Manz - {hidiveLabRef} -
  • -
  • - Alexander Veit - {parkLabRef} -
  • -
  • - Michele Berselli - {parkLabRef} -
  • -
  • - Peter J Park - {parkLabRef} -
  • -
  • - Dominik Glodzik - {parkLabRef} -
  • -
  • - Nils Gehlenborg - {hidiveLabRef} -
  • -
-
-
+ )}
{ diff --git a/src/icon.ts b/src/icon.ts index 32d46f52..76a7a041 100644 --- a/src/icon.ts +++ b/src/icon.ts @@ -333,5 +333,13 @@ export const ICONS: Record = { ], stroke: 'currentColor', fill: 'none' + }, + TRIANGLE_DOWN: { + width: 11, + height: 7, + viewBox: '0 0 11 7', + path: ['M0.5 1H10.5L5.5 6L0.5 1Z', 'M5.5 6L0.5 1H10.5L5.5 6ZM5.5 6V5.28571'], + stroke: 'currentColor', + fill: 'none' } }; diff --git a/src/main-spec.ts b/src/main-spec.ts index 63720449..d580920f 100644 --- a/src/main-spec.ts +++ b/src/main-spec.ts @@ -19,10 +19,11 @@ export interface SpecOption extends SampleType { svReads: { name: string; type: string }[]; crossChr: boolean; bpIntervals: [number, number, number, number] | undefined; + spacing: number; } function generateSpec(opt: SpecOption): GoslingSpec { - const { assembly, id, bam, bai, width, selectedSvId, breakpoints, bpIntervals } = opt; + const { assembly, id, bam, bai, width, selectedSvId, breakpoints, bpIntervals, spacing } = opt; const topViewWidth = Math.min(width, 600); const midViewWidth = width; @@ -39,7 +40,7 @@ function generateSpec(opt: SpecOption): GoslingSpec { arrangement: 'vertical', centerRadius: 0.5, assembly, - spacing: 40, + spacing, style: { outlineWidth: 1, outline: 'lightgray', diff --git a/src/ui/ExportDropdown.tsx b/src/ui/ExportDropdown.tsx new file mode 100644 index 00000000..6bdbbcf4 --- /dev/null +++ b/src/ui/ExportDropdown.tsx @@ -0,0 +1,90 @@ +import React, { useState, useEffect } from 'react'; +import { ICONS } from '../icon'; +import { getHtmlTemplate } from '../html-template'; + +type ExportButtonProps = { + title: string; + icon: string; +}; + +const ExportButton = ({ title, icon }: ExportButtonProps) => { + return ( + + {title} + {ICONS[icon].path.map(p => ( + + ))} + + ); +}; + +type ExportDropdownProps = { + gosRef: React.RefObject; + currentSpec: React.MutableRefObject; +}; + +export const ExportDropdown = ({ gosRef, currentSpec }: ExportDropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
setIsOpen(!isOpen)} + aria-expanded={isOpen} + > + + {isOpen ? ( + + ) : null} +
+ ); +}; From 3c6278e3a8b06b8b6452d6a038f32c261c478b25 Mon Sep 17 00:00:00 2001 From: tsertijn Date: Mon, 1 Jul 2024 14:57:50 +0200 Subject: [PATCH 13/57] Create baf --- src/track/baf | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/track/baf diff --git a/src/track/baf b/src/track/baf new file mode 100644 index 00000000..2400d7d1 --- /dev/null +++ b/src/track/baf @@ -0,0 +1 @@ +#baf From 9b141e8516beba52b0efd40288ee2ba2119d70fe Mon Sep 17 00:00:00 2001 From: tsertijn Date: Mon, 1 Jul 2024 14:58:35 +0200 Subject: [PATCH 14/57] Rename baf to baf.ts --- src/track/{baf => baf.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/track/{baf => baf.ts} (100%) diff --git a/src/track/baf b/src/track/baf.ts similarity index 100% rename from src/track/baf rename to src/track/baf.ts From 8657d2e000a9bdf5c7f158ee6adde08ef52bb127 Mon Sep 17 00:00:00 2001 From: tsertijn Date: Mon, 1 Jul 2024 15:02:54 +0200 Subject: [PATCH 15/57] Update baf.ts start coding --- src/track/baf.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/track/baf.ts b/src/track/baf.ts index 2400d7d1..86ad1915 100644 --- a/src/track/baf.ts +++ b/src/track/baf.ts @@ -1 +1,4 @@ -#baf +import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function Baf( From a9259b0d0728ed5a1eabd6b378af27e17dd549ad Mon Sep 17 00:00:00 2001 From: tsertijn Date: Fri, 5 Jul 2024 11:06:16 +0200 Subject: [PATCH 16/57] Delete src/track directory --- src/track/baf.ts | 4 - src/track/boundary.ts | 44 --------- src/track/cnv.ts | 50 ---------- src/track/coverage.ts | 35 ------- src/track/driver.ts | 65 ------------- src/track/gain.ts | 39 -------- src/track/indel.ts | 113 ---------------------- src/track/index.ts | 23 ----- src/track/loh.ts | 35 ------- src/track/mutation.ts | 36 ------- src/track/sv.ts | 217 ------------------------------------------ 11 files changed, 661 deletions(-) delete mode 100644 src/track/baf.ts delete mode 100644 src/track/boundary.ts delete mode 100644 src/track/cnv.ts delete mode 100644 src/track/coverage.ts delete mode 100644 src/track/driver.ts delete mode 100644 src/track/gain.ts delete mode 100644 src/track/indel.ts delete mode 100644 src/track/index.ts delete mode 100644 src/track/loh.ts delete mode 100644 src/track/mutation.ts delete mode 100644 src/track/sv.ts diff --git a/src/track/baf.ts b/src/track/baf.ts deleted file mode 100644 index 86ad1915..00000000 --- a/src/track/baf.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from './index'; - -export default function Baf( diff --git a/src/track/boundary.ts b/src/track/boundary.ts deleted file mode 100644 index 52860ba0..00000000 --- a/src/track/boundary.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from '.'; - -const hex = 'lightgray'; - -export default function boundary(parent: string, mode: TrackMode): Partial { - return { - id: `${parent}-${mode}-boundary`, - data: { - type: 'json', - chromosomeField: 'c', - genomicFields: ['p'], - values: [ - { c: 'chr2', p: 0 }, - { c: 'chr3', p: 0 }, - { c: 'chr4', p: 0 }, - { c: 'chr5', p: 0 }, - { c: 'chr6', p: 0 }, - { c: 'chr7', p: 0 }, - { c: 'chr8', p: 0 }, - { c: 'chr9', p: 0 }, - { c: 'chr10', p: 0 }, - { c: 'chr11', p: 0 }, - { c: 'chr12', p: 0 }, - { c: 'chr13', p: 0 }, - { c: 'chr14', p: 0 }, - { c: 'chr15', p: 0 }, - { c: 'chr16', p: 0 }, - { c: 'chr17', p: 0 }, - { c: 'chr18', p: 0 }, - { c: 'chr19', p: 0 }, - { c: 'chr20', p: 0 }, - { c: 'chr21', p: 0 }, - { c: 'chrX', p: 0 }, - { c: 'chrY', p: 0 } - ] - }, - mark: mode === 'mid' ? 'rule' : 'rect', - x: { field: 'p', type: 'genomic' }, - color: { value: hex }, - opacity: { value: 0.5 }, - overlayOnPreviousTrack: true - }; -} diff --git a/src/track/cnv.ts b/src/track/cnv.ts deleted file mode 100644 index a7e87c35..00000000 --- a/src/track/cnv.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { SingleTrack, OverlaidTracks } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from './index'; - -export default function cnv( - sampleId: string, - cnvUrl: string, - width: number, - height: number, - mode: TrackMode, - cnFields: [string, string, string] = ['total_cn', 'major_cn', 'minor_cn'] -): OverlaidTracks { - const [total_cn, major_cn, minor_cn] = cnFields; - return { - id: `${sampleId}-${mode}-cnv`, - title: mode === 'small' ? '' : 'Copy Number Variants', - style: { background: '#FFFFFF' }, - data: { - separator: '\t', - url: cnvUrl, - type: 'csv', - chromosomeField: 'chromosome', - genomicFields: ['start', 'end'] - }, - mark: 'rect', - x: { field: 'start', type: 'genomic' }, - xe: { field: 'end', type: 'genomic' }, - alignment: 'overlay', - tracks: [ - { - y: { field: total_cn, type: 'quantitative', axis: 'right', grid: true, range: [0 + 10, height - 10] }, - color: { value: '#808080' } - } - // { - // y: { field: 'minor_cn', type: 'quantitative', axis: 'right', grid: true }, - // color: { value: 'red' }, - // } - ], - tooltip: [ - { field: total_cn, type: 'quantitative' }, - { field: major_cn, type: 'quantitative' }, - { field: minor_cn, type: 'quantitative' } - ], - size: { value: 5 }, - stroke: { value: '#808080' }, - strokeWidth: { value: 1 }, - opacity: { value: 0.7 }, - width, - height - }; -} diff --git a/src/track/coverage.ts b/src/track/coverage.ts deleted file mode 100644 index 5cb0dbaf..00000000 --- a/src/track/coverage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; -import { SpecOption } from '../main-spec'; - -export default function coverage(option: SpecOption, isLeft: boolean): Partial { - const { id, bam, bai, width, svReads, crossChr, bpIntervals } = option; - return { - id: `${id}-bottom-${isLeft ? 'left' : 'right'}-coverage`, - title: 'Coverage', - data: { - type: 'bam', - url: bam, - indexUrl: bai - }, - dataTransform: [ - { - type: 'coverage', - startField: 'start', - endField: 'end' - } - ], - mark: 'bar', - x: { field: 'start', type: 'genomic' }, - xe: { field: 'end', type: 'genomic' }, - y: { - field: 'coverage', - type: 'quantitative', - axis: 'right', - grid: true - }, - color: { value: 'lightgray' }, - stroke: { value: 'gray' }, - width, - height: 80 - }; -} diff --git a/src/track/driver.ts b/src/track/driver.ts deleted file mode 100644 index 0b54673c..00000000 --- a/src/track/driver.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from './index'; - -export default function driver( - sampleId: string, - url: string, - width: number, - height: number, - mode: TrackMode -): SingleTrack { - return { - id: `${sampleId}-${mode}-driver`, - title: 'Putative Driver', - // TODO: click events are not supported for layered tracks - // experimental: { - // mouseEvents: { - // click: true, - // mouseOver: true - // } - // }, - data: { - url, - type: 'csv', - separator: '\t', - chromosomeField: 'chr', - genomicFields: ['pos'] - }, - dataTransform: [ - { - type: 'replace', - field: 'biallelic', - replace: [ - { from: 'yes', to: '⊙ ' }, - { from: 'no', to: '· ' }, - { from: 'Yes', to: '⊙ ' }, - { from: 'No', to: '· ' } - ], - newField: 'prefix' - }, - { type: 'concat', fields: ['prefix', 'gene'], newField: 'geneWithPrefix', separator: '' } - // { type: 'displace', method: 'pile', boundingBox: { startField: 'pos', endField: 'pos', padding: 100, isPaddingBP: false }, newField: 'row', maxRows: 2 } - ], - mark: 'text', - x: { field: 'pos', type: 'genomic' }, - text: { field: 'geneWithPrefix', type: 'nominal' }, - color: { value: 'black' }, - row: { field: 'row', type: 'nominal' }, - style: { textFontWeight: 'normal' }, - size: { value: mode === 'top' ? 10 : 14 }, - tooltip: [ - { field: 'pos', alt: 'Position', type: 'genomic' }, - { field: 'ref', alt: 'REF', type: 'nominal' }, - { field: 'alt', alt: 'ALT', type: 'nominal' }, - { field: 'category', alt: 'Category', type: 'nominal' }, - { field: 'top_category', alt: 'Top Category', type: 'nominal' }, - { field: 'biallelic', alt: 'Biallelic', type: 'nominal' }, - { field: 'transcript_consequence', alt: 'Transcript Consequence', type: 'nominal' }, - { field: 'protein_mutation', alt: 'Protein Mutation', type: 'nominal' }, - { field: 'allele_fraction', alt: 'Allele Fraction', type: 'nominal' }, - { field: 'mutation_type', alt: 'Mutation Type', type: 'nominal' } - ], - width, - height: height - }; -} diff --git a/src/track/gain.ts b/src/track/gain.ts deleted file mode 100644 index 79c3974e..00000000 --- a/src/track/gain.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from './index'; - -export default function gain( - sampleId: string, - cnvUrl: string, - width: number, - height: number, - mode: TrackMode, - cnFields: [string, string, string] = ['total_cn', 'major_cn', 'minor_cn'] -): SingleTrack { - const [total_cn, major_cn, minor_cn] = cnFields; - return { - id: `${sampleId}-${mode}-gain`, - title: mode === 'small' ? '' : 'Gain', - style: { background: '#F6F6F6' }, - data: { - separator: '\t', - url: cnvUrl, - type: 'csv', - chromosomeField: 'chromosome', - genomicFields: ['start', 'end'] - }, - dataTransform: [ - // https://cancer.sanger.ac.uk/cosmic/help/cnv/overview - { - type: 'filter', - field: total_cn, - inRange: [5, 999] - } - ], - mark: 'rect', - x: { field: 'start', type: 'genomic' }, - xe: { field: 'end', type: 'genomic' }, - color: { value: '#5CB6EA' }, - width, - height - }; -} diff --git a/src/track/indel.ts b/src/track/indel.ts deleted file mode 100644 index 6550521d..00000000 --- a/src/track/indel.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { OverlaidTracks } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from './index'; - -export default function indel( - sampleId: string, - url: string, - indexUrl: string, - width: number, - height: number, - mode: TrackMode -): OverlaidTracks { - return { - id: `${sampleId}-${mode}-indel`, - style: { background: '#F6F6F6' }, - data: { - url, - indexUrl, - type: 'vcf', - sampleLength: 500 - }, - dataTransform: [ - { - type: 'concat', - fields: ['REF', 'ALT'], - separator: ' → ', - newField: 'LAB' - }, - { - type: 'replace', - field: 'MUTTYPE', - replace: [ - { from: 'insertion', to: 'Insertion' }, - { from: 'deletion', to: 'Deletion' } - ], - newField: 'MUTTYPE' - } - ], - alignment: 'overlay', - tracks: [ - { - size: { value: height / 2 - 1 }, - visibility: [ - { - target: 'track', - operation: 'GT', - measure: 'zoomLevel', - threshold: 1000 - } - ] - }, - { - xe: { field: 'POSEND', type: 'genomic', axis: 'top' }, - visibility: [ - { - target: 'track', - operation: 'LTET', - measure: 'zoomLevel', - threshold: 1000 - } - ] - } - // { - // mark: 'text', - // text: { field: 'LAB', type: 'nominal' }, - // xe: { field: 'POSEND', type: 'genomic', axis: 'top' }, - // color: { value: 'white' }, - // strokeWidth: { value: 0 }, - // opacity: { value: 1 }, - // visibility: [ - // { - // target: 'mark', - // operation: 'LT', - // measure: 'width', - // transitionPadding: 30, - // threshold: '|xe-x|' - // } - // ] - // } - ], - mark: 'rect', - x: { field: 'POS', type: 'genomic' }, - // stroke: { - // field: 'MUTTYPE', - // type: 'nominal', - // legend: false, - // domain: ['Insertion', 'Deletion'] - // }, - // strokeWidth: { value: 1 }, - color: { - field: 'MUTTYPE', - type: 'nominal', - legend: false, - domain: ['Insertion', 'Deletion'] - }, - row: { - field: 'MUTTYPE', - type: 'nominal', - legend: true, - domain: ['Insertion', 'Deletion'] - }, - tooltip: [ - { field: 'POS', type: 'genomic' }, - { field: 'POSEND', type: 'genomic' }, - { field: 'MUTTYPE', type: 'nominal' }, - { field: 'ALT', type: 'nominal' }, - { field: 'REF', type: 'nominal' }, - { field: 'QUAL', type: 'quantitative' } - ], - opacity: { value: 0.9 }, - width, - height - }; -} diff --git a/src/track/index.ts b/src/track/index.ts deleted file mode 100644 index 8c937149..00000000 --- a/src/track/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import driver from './driver'; -import cnv from './cnv'; -import coverage from './coverage'; -import gain from './gain'; -import loh from './loh'; -import sv from './sv'; -import mutation from './mutation'; -import indel from './indel'; -import boundary from './boundary'; - -export type TrackMode = 'small' | 'top' | 'mid'; - -export default { - driver, - cnv, - coverage, - gain, - loh, - sv, - mutation, - indel, - boundary -}; diff --git a/src/track/loh.ts b/src/track/loh.ts deleted file mode 100644 index da678df4..00000000 --- a/src/track/loh.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from './index'; - -export default function loh( - sampleId: string, - cnvUrl: string, - width: number, - height: number, - mode: TrackMode, - cnFields: [string, string, string] = ['total_cn', 'major_cn', 'minor_cn'] -): SingleTrack { - const [total_cn, major_cn, minor_cn] = cnFields; - return { - id: `${sampleId}-${mode}-loh`, - title: mode !== 'small' ? 'Loss of Heterozygosity (LOH)' : '', - style: { background: '#F6F6F6' }, - data: { - separator: '\t', - url: cnvUrl, - type: 'csv', - chromosomeField: 'chromosome', - genomicFields: ['start', 'end'] - }, - dataTransform: [ - { type: 'filter', field: minor_cn, inRange: [0, 0.01] }, - { type: 'filter', field: total_cn, oneOf: ['0'], not: true } - ], - mark: 'rect', - x: { field: 'start', type: 'genomic' }, - xe: { field: 'end', type: 'genomic' }, - color: { value: '#D6641E' }, - width, - height - }; -} diff --git a/src/track/mutation.ts b/src/track/mutation.ts deleted file mode 100644 index 5f753776..00000000 --- a/src/track/mutation.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; -import { TrackMode } from './index'; - -export default function mutation( - sampleId: string, - url: string, - indexUrl: string, - width: number, - height: number, - mode: TrackMode -): SingleTrack { - return { - id: `${sampleId}-${mode}-mutation`, - title: 'Point Mutation', - style: { background: '#FFFFFF', inlineLegend: true }, - data: { - type: 'vcf', - url, - indexUrl, - sampleLength: 500 - }, - dataTransform: [{ field: 'DISTPREV', type: 'filter', oneOf: [0], not: true }], - mark: 'point', - x: { field: 'POS', type: 'genomic' }, - color: { field: 'SUBTYPE', type: 'nominal', legend: true, domain: ['C>A', 'C>G', 'C>T', 'T>A', 'T>C', 'T>G'] }, - y: { field: 'DISTPREVLOGE', type: 'quantitative', axis: 'left', range: [10, height - 10] }, - opacity: { value: 0.9 }, - tooltip: [ - { field: 'DISTPREV', type: 'nominal', format: 's1', alt: 'Distance To Previous Mutation (BP)' }, - { field: 'POS', type: 'genomic' }, - { field: 'SUBTYPE', type: 'nominal' } - ], - width, - height - }; -} diff --git a/src/track/sv.ts b/src/track/sv.ts deleted file mode 100644 index c18f9bb5..00000000 --- a/src/track/sv.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { - FilterTransform, - OverlaidTracks, - SingleTrack, - StrReplaceTransform, - SvTypeTransform -} from 'gosling.js/dist/src/gosling-schema'; -import { consistentSv } from '../constants'; -import defaults from '../default-encoding'; -import { TrackMode } from '.'; - -const TRI_SIZE = 5; - -const svInfer: [SvTypeTransform, StrReplaceTransform] = [ - { - type: 'svType', - firstBp: { - chrField: 'chrom1', - posField: 'start1', - strandField: 'strand1' - }, - secondBp: { - chrField: 'chrom2', - posField: 'start2', - strandField: 'strand2' - }, - newField: 'svclass' - }, - { - type: 'replace', - field: 'svclass', - replace: [ - ...Object.entries(consistentSv).map(([from, to]) => { - return { from, to }; - }) - ], - newField: 'svclass' - } -]; - -type SvType = 'Translocation' | 'Duplication' | 'Deletion' | 'Inversion (TtT)' | 'Inversion (HtH)'; -function filterSv(oneOf: SvType[], not: boolean): FilterTransform { - return { - type: 'filter', - field: 'svclass', - oneOf, - not - }; -} - -export default function sv( - sampleId: string, - url: string, - width: number, - height: number, - mode: TrackMode, - selectedSvId: string -): OverlaidTracks { - const _baselineYMap: { [k in SvType]: { y: number; ye: number } } = { - Translocation: { y: (height / 5) * 4, ye: height }, - Deletion: { y: height / 5, ye: 1 }, - Duplication: { y: height / 5, ye: (height / 5) * 2 }, - 'Inversion (TtT)': { y: (height / 5) * 3, ye: (height / 5) * 2 }, - 'Inversion (HtH)': { y: (height / 5) * 3, ye: (height / 5) * 4 } - }; - const arcs = (sv: SvType, selected: boolean): Partial => { - const { y, ye } = _baselineYMap[sv]; - return { - // TODO: the right end does not sometimes show the arc. - dataTransform: [ - ...svInfer, - { - type: 'filter', - field: 'sv_id', - oneOf: [selectedSvId], - not: !selected - }, - filterSv([sv], false) - ], - x: { field: 'start1', type: 'genomic' }, - xe: { field: 'end2', type: 'genomic' }, - y: { value: y }, - ye: { value: ye }, - flipY: true, - ...(selected ? { opacity: { value: 1 }, strokeWidth: { value: 2 } } : {}), - ...(selected && sv === 'Translocation' ? { stroke: { value: 'grey' } } : {}) - }; - }; - const svs = [...defaults.color.svclass.domain]; - return { - id: `${sampleId}-${mode}-sv`, - alignment: 'overlay', - experimental: { - mouseEvents: { - click: true, - mouseOver: true, - groupMarksByField: 'sv_id' - }, - performanceMode: true - }, - data: { - url, - type: 'csv', - separator: '\t', - genomicFieldsToConvert: [ - { - chromosomeField: 'chrom1', - genomicFields: ['start1', 'end1'] - }, - { - chromosomeField: 'chrom2', - genomicFields: ['start2', 'end2'] - } - ] - }, - mark: 'withinLink', - tracks: [ - ...svs.map(d => arcs(d as any, false)), - ...svs.map(d => arcs(d as any, true)), - ...((mode !== 'mid' - ? [] - : [ - { - dataTransform: [{ type: 'filter', field: 'strand1', oneOf: ['+'] }], - mark: 'triangleLeft', - x: { field: 'start1', type: 'genomic' }, - size: { value: TRI_SIZE }, - y: { value: height }, - stroke: { value: 0 }, - style: { align: 'right' } - }, - { - dataTransform: [{ type: 'filter', field: 'strand1', oneOf: ['-'] }], - mark: 'triangleRight', - x: { field: 'start1', type: 'genomic' }, - size: { value: TRI_SIZE }, - y: { value: height }, - stroke: { value: 0 }, - style: { align: 'left' } - }, - { - dataTransform: [{ type: 'filter', field: 'strand2', oneOf: ['+'] }], - mark: 'triangleLeft', - x: { field: 'end2', type: 'genomic' }, - size: { value: TRI_SIZE }, - y: { value: height }, - stroke: { value: 0 }, - style: { align: 'right' } - }, - { - dataTransform: [{ type: 'filter', field: 'strand2', oneOf: ['-'] }], - mark: 'triangleRight', - x: { field: 'end2', type: 'genomic' }, - size: { value: TRI_SIZE }, - y: { value: height }, - stroke: { value: 0 }, - style: { align: 'left' } - } - ]) as OverlaidTracks[]), - ...((mode !== 'mid' - ? [] - : [ - { - dataTransform: [...svInfer, { type: 'filter', field: 'sv_id', oneOf: [selectedSvId] }], - mark: 'rule', - x: { field: 'start1', type: 'genomic' }, - color: { value: 'black' }, - strokeWidth: { value: 1 }, - opacity: { value: 1 }, - style: { dashed: [3, 3] } - }, - { - dataTransform: [...svInfer, { type: 'filter', field: 'sv_id', oneOf: [selectedSvId] }], - mark: 'rule', - x: { field: 'end2', type: 'genomic' }, - color: { value: 'black' }, - strokeWidth: { value: 1 }, - opacity: { value: 1 }, - style: { dashed: [3, 3] } - } - ]) as OverlaidTracks[]) - ], - y: { value: height / 5 }, - color: { - field: 'svclass', - type: 'nominal', - legend: mode === 'mid', - domain: ['Gain', 'LOH', ...defaults.color.svclass.domain], - range: ['#5CB6EA', '#D6641E', ...defaults.color.svclass.range] - }, - stroke: { - field: 'svclass', - type: 'nominal', - domain: defaults.color.svclass.domain, - range: defaults.color.svclass.range - }, - strokeWidth: { value: 1 }, - opacity: { value: 0.7 }, - tooltip: [ - { field: 'start1', type: 'genomic' }, - { field: 'end2', type: 'genomic' }, - { field: 'strand1', type: 'nominal' }, - { field: 'strand2', type: 'nominal' }, - { field: 'svclass', type: 'nominal' }, - { field: 'sv_id', type: 'nominal' }, - { field: 'pe_support', type: 'nominal' } - ], - style: { - linkStyle: 'elliptical', - linkMinHeight: 0.7, - mouseOver: { stroke: '#242424', strokeWidth: 1 }, - withinLinkVerticalLines: true - }, - width, - height - }; -} From bed22cf74841855c44a16275275f12c87bb74991 Mon Sep 17 00:00:00 2001 From: tsertijn Date: Fri, 5 Jul 2024 11:08:50 +0200 Subject: [PATCH 17/57] Add files via upload edited files were annoying with git --- src/track/Bi_allel_example.ts | 56 +++++++++ src/track/baf.ts | 48 ++++++++ src/track/boundary.ts | 44 +++++++ src/track/cnv.ts | 50 ++++++++ src/track/coverage.ts | 35 ++++++ src/track/driver.ts | 65 ++++++++++ src/track/gain.ts | 39 ++++++ src/track/indel.ts | 113 ++++++++++++++++++ src/track/index.ts | 27 +++++ src/track/loh.ts | 35 ++++++ src/track/mutation.ts | 36 ++++++ src/track/sv.ts | 217 ++++++++++++++++++++++++++++++++++ 12 files changed, 765 insertions(+) create mode 100644 src/track/Bi_allel_example.ts create mode 100644 src/track/baf.ts create mode 100644 src/track/boundary.ts create mode 100644 src/track/cnv.ts create mode 100644 src/track/coverage.ts create mode 100644 src/track/driver.ts create mode 100644 src/track/gain.ts create mode 100644 src/track/indel.ts create mode 100644 src/track/index.ts create mode 100644 src/track/loh.ts create mode 100644 src/track/mutation.ts create mode 100644 src/track/sv.ts diff --git a/src/track/Bi_allel_example.ts b/src/track/Bi_allel_example.ts new file mode 100644 index 00000000..d3b020b1 --- /dev/null +++ b/src/track/Bi_allel_example.ts @@ -0,0 +1,56 @@ +import { OverlaidTracks } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function biAlleleFrequency( + sampleId: string, + url: string, + width: number, + height: number, + mode: TrackMode +): OverlaidTracks { + return { + id: `${sampleId}-${mode}-bi-allele-frequency`, + title: mode === 'small' ? '' : 'Bi Allele Frequency', + style: { background: '#FFFFFF' }, + data: { + type: 'beddb', + url: url, + genomicFields: [ + { index: 1, name: 'start' }, + { index: 2, name: 'end' } + ], + valueFields: [ + { index: 0, name: 'chromosome', type: 'nominal' }, + { index: 3, name: 'BAF', type: 'quantitative' } + //{index: 4, name: "priority", type: "quantitative"}, + ] + }, + mark: 'point', + x: { field: 'start', type: 'genomic' }, + alignment: 'overlay', + tracks: [ + { + y: { + field: 'baf', + type: 'quantitative', + axis: 'right', + grid: true, + domain: [-10, 110] + } + } + ], + tooltip: [ + { field: 'BAF', type: 'quantitative' }, + { field: 'chromosome', type: 'quantitative' }, + { field: 'start', type: 'quantitative' }, + { field: 'end', type: 'quantitative' } + ], + size: { value: 1 }, + stroke: { value: '#808080' }, + color: { value: '#808080' }, + strokeWidth: { value: 1 }, + opacity: { value: 0.7 }, + width, + height + }; +} diff --git a/src/track/baf.ts b/src/track/baf.ts new file mode 100644 index 00000000..703a4225 --- /dev/null +++ b/src/track/baf.ts @@ -0,0 +1,48 @@ +import { SingleTrack, OverlaidTracks } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; +export default function baf( + sampleId: string, + url: string, + width: number, + height: number, + mode: TrackMode +): OverlaidTracks { + return { + id: `${sampleId}-${mode}-baf`, + title: mode === 'small' ? '' : 'Bi-allele Frequency', + style: { background: '#FFFFFF' }, + data: { + separator: '\t', + url, + type: 'csv', + chromosomeField: 'chromosome', + genomicFields: ['start', 'end'] + }, + mark: 'rect', + x: { field: 'start', type: 'genomic', axis: 'bottom' }, + xe: { field: 'end', type: 'genomic' }, + //x1: {field: 'chromosome', type: 'genomic', "axis": "bottom"}, + alignment: 'overlay', + tracks: [ + { + mark: 'point', + size: { value: 3 }, + y: { field: 'BAF', type: 'quantitative', axis: 'right', grid: true, domain: [-0.1, 1.1] }, + color: { value: '#3ebecf' } + } + ], + tooltip: [ + { field: 'BAF', type: 'quantitative' }, + { field: 'REF', type: 'nominal' }, + { field: 'ALT', type: 'nominal' }, + { field: 'start', type: 'quantitative' }, + { field: 'chromosome', type: 'nominal' } + ], + size: { value: 5 }, + stroke: { value: '#808080' }, + strokeWidth: { value: 0 }, + opacity: { value: 0.7 }, + width, + height + }; +} diff --git a/src/track/boundary.ts b/src/track/boundary.ts new file mode 100644 index 00000000..52860ba0 --- /dev/null +++ b/src/track/boundary.ts @@ -0,0 +1,44 @@ +import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from '.'; + +const hex = 'lightgray'; + +export default function boundary(parent: string, mode: TrackMode): Partial { + return { + id: `${parent}-${mode}-boundary`, + data: { + type: 'json', + chromosomeField: 'c', + genomicFields: ['p'], + values: [ + { c: 'chr2', p: 0 }, + { c: 'chr3', p: 0 }, + { c: 'chr4', p: 0 }, + { c: 'chr5', p: 0 }, + { c: 'chr6', p: 0 }, + { c: 'chr7', p: 0 }, + { c: 'chr8', p: 0 }, + { c: 'chr9', p: 0 }, + { c: 'chr10', p: 0 }, + { c: 'chr11', p: 0 }, + { c: 'chr12', p: 0 }, + { c: 'chr13', p: 0 }, + { c: 'chr14', p: 0 }, + { c: 'chr15', p: 0 }, + { c: 'chr16', p: 0 }, + { c: 'chr17', p: 0 }, + { c: 'chr18', p: 0 }, + { c: 'chr19', p: 0 }, + { c: 'chr20', p: 0 }, + { c: 'chr21', p: 0 }, + { c: 'chrX', p: 0 }, + { c: 'chrY', p: 0 } + ] + }, + mark: mode === 'mid' ? 'rule' : 'rect', + x: { field: 'p', type: 'genomic' }, + color: { value: hex }, + opacity: { value: 0.5 }, + overlayOnPreviousTrack: true + }; +} diff --git a/src/track/cnv.ts b/src/track/cnv.ts new file mode 100644 index 00000000..00a5477e --- /dev/null +++ b/src/track/cnv.ts @@ -0,0 +1,50 @@ +import { SingleTrack, OverlaidTracks } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function cnv( + sampleId: string, + cnvUrl: string, + width: number, + height: number, + mode: TrackMode, + cnFields: [string, string, string] = ['total_cn', 'major_cn', 'minor_cn'] +): OverlaidTracks { + const [total_cn, major_cn, minor_cn] = cnFields; + return { + id: `${sampleId}-${mode}-cnv`, + title: mode === 'small' ? '' : 'Copy Number Variants', + style: { background: '#FFFFFF' }, + data: { + separator: '\t', + url: cnvUrl, + type: 'csv', + chromosomeField: 'chromosome', + genomicFields: ['start', 'end'] + }, + mark: 'rect', + x: { field: 'start', type: 'genomic' }, + xe: { field: 'end', type: 'genomic' }, + alignment: 'overlay', + tracks: [ + { + y: { field: total_cn, type: 'quantitative', axis: 'right', grid: true, range: [0 + 10, height - 10] }, + color: { value: '#808080' } + } + // { + // y: { field: 'minor_cn', type: 'quantitative', axis: 'right', grid: true }, + // color: { value: 'red' }, + //} + ], + tooltip: [ + { field: total_cn, type: 'quantitative' }, + { field: major_cn, type: 'quantitative' }, + { field: minor_cn, type: 'quantitative' } + ], + size: { value: 5 }, + stroke: { value: '#808080' }, + strokeWidth: { value: 1 }, + opacity: { value: 0.7 }, + width, + height + }; +} diff --git a/src/track/coverage.ts b/src/track/coverage.ts new file mode 100644 index 00000000..5cb0dbaf --- /dev/null +++ b/src/track/coverage.ts @@ -0,0 +1,35 @@ +import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; +import { SpecOption } from '../main-spec'; + +export default function coverage(option: SpecOption, isLeft: boolean): Partial { + const { id, bam, bai, width, svReads, crossChr, bpIntervals } = option; + return { + id: `${id}-bottom-${isLeft ? 'left' : 'right'}-coverage`, + title: 'Coverage', + data: { + type: 'bam', + url: bam, + indexUrl: bai + }, + dataTransform: [ + { + type: 'coverage', + startField: 'start', + endField: 'end' + } + ], + mark: 'bar', + x: { field: 'start', type: 'genomic' }, + xe: { field: 'end', type: 'genomic' }, + y: { + field: 'coverage', + type: 'quantitative', + axis: 'right', + grid: true + }, + color: { value: 'lightgray' }, + stroke: { value: 'gray' }, + width, + height: 80 + }; +} diff --git a/src/track/driver.ts b/src/track/driver.ts new file mode 100644 index 00000000..0b54673c --- /dev/null +++ b/src/track/driver.ts @@ -0,0 +1,65 @@ +import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function driver( + sampleId: string, + url: string, + width: number, + height: number, + mode: TrackMode +): SingleTrack { + return { + id: `${sampleId}-${mode}-driver`, + title: 'Putative Driver', + // TODO: click events are not supported for layered tracks + // experimental: { + // mouseEvents: { + // click: true, + // mouseOver: true + // } + // }, + data: { + url, + type: 'csv', + separator: '\t', + chromosomeField: 'chr', + genomicFields: ['pos'] + }, + dataTransform: [ + { + type: 'replace', + field: 'biallelic', + replace: [ + { from: 'yes', to: '⊙ ' }, + { from: 'no', to: '· ' }, + { from: 'Yes', to: '⊙ ' }, + { from: 'No', to: '· ' } + ], + newField: 'prefix' + }, + { type: 'concat', fields: ['prefix', 'gene'], newField: 'geneWithPrefix', separator: '' } + // { type: 'displace', method: 'pile', boundingBox: { startField: 'pos', endField: 'pos', padding: 100, isPaddingBP: false }, newField: 'row', maxRows: 2 } + ], + mark: 'text', + x: { field: 'pos', type: 'genomic' }, + text: { field: 'geneWithPrefix', type: 'nominal' }, + color: { value: 'black' }, + row: { field: 'row', type: 'nominal' }, + style: { textFontWeight: 'normal' }, + size: { value: mode === 'top' ? 10 : 14 }, + tooltip: [ + { field: 'pos', alt: 'Position', type: 'genomic' }, + { field: 'ref', alt: 'REF', type: 'nominal' }, + { field: 'alt', alt: 'ALT', type: 'nominal' }, + { field: 'category', alt: 'Category', type: 'nominal' }, + { field: 'top_category', alt: 'Top Category', type: 'nominal' }, + { field: 'biallelic', alt: 'Biallelic', type: 'nominal' }, + { field: 'transcript_consequence', alt: 'Transcript Consequence', type: 'nominal' }, + { field: 'protein_mutation', alt: 'Protein Mutation', type: 'nominal' }, + { field: 'allele_fraction', alt: 'Allele Fraction', type: 'nominal' }, + { field: 'mutation_type', alt: 'Mutation Type', type: 'nominal' } + ], + width, + height: height + }; +} diff --git a/src/track/gain.ts b/src/track/gain.ts new file mode 100644 index 00000000..79c3974e --- /dev/null +++ b/src/track/gain.ts @@ -0,0 +1,39 @@ +import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function gain( + sampleId: string, + cnvUrl: string, + width: number, + height: number, + mode: TrackMode, + cnFields: [string, string, string] = ['total_cn', 'major_cn', 'minor_cn'] +): SingleTrack { + const [total_cn, major_cn, minor_cn] = cnFields; + return { + id: `${sampleId}-${mode}-gain`, + title: mode === 'small' ? '' : 'Gain', + style: { background: '#F6F6F6' }, + data: { + separator: '\t', + url: cnvUrl, + type: 'csv', + chromosomeField: 'chromosome', + genomicFields: ['start', 'end'] + }, + dataTransform: [ + // https://cancer.sanger.ac.uk/cosmic/help/cnv/overview + { + type: 'filter', + field: total_cn, + inRange: [5, 999] + } + ], + mark: 'rect', + x: { field: 'start', type: 'genomic' }, + xe: { field: 'end', type: 'genomic' }, + color: { value: '#5CB6EA' }, + width, + height + }; +} diff --git a/src/track/indel.ts b/src/track/indel.ts new file mode 100644 index 00000000..6550521d --- /dev/null +++ b/src/track/indel.ts @@ -0,0 +1,113 @@ +import { OverlaidTracks } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function indel( + sampleId: string, + url: string, + indexUrl: string, + width: number, + height: number, + mode: TrackMode +): OverlaidTracks { + return { + id: `${sampleId}-${mode}-indel`, + style: { background: '#F6F6F6' }, + data: { + url, + indexUrl, + type: 'vcf', + sampleLength: 500 + }, + dataTransform: [ + { + type: 'concat', + fields: ['REF', 'ALT'], + separator: ' → ', + newField: 'LAB' + }, + { + type: 'replace', + field: 'MUTTYPE', + replace: [ + { from: 'insertion', to: 'Insertion' }, + { from: 'deletion', to: 'Deletion' } + ], + newField: 'MUTTYPE' + } + ], + alignment: 'overlay', + tracks: [ + { + size: { value: height / 2 - 1 }, + visibility: [ + { + target: 'track', + operation: 'GT', + measure: 'zoomLevel', + threshold: 1000 + } + ] + }, + { + xe: { field: 'POSEND', type: 'genomic', axis: 'top' }, + visibility: [ + { + target: 'track', + operation: 'LTET', + measure: 'zoomLevel', + threshold: 1000 + } + ] + } + // { + // mark: 'text', + // text: { field: 'LAB', type: 'nominal' }, + // xe: { field: 'POSEND', type: 'genomic', axis: 'top' }, + // color: { value: 'white' }, + // strokeWidth: { value: 0 }, + // opacity: { value: 1 }, + // visibility: [ + // { + // target: 'mark', + // operation: 'LT', + // measure: 'width', + // transitionPadding: 30, + // threshold: '|xe-x|' + // } + // ] + // } + ], + mark: 'rect', + x: { field: 'POS', type: 'genomic' }, + // stroke: { + // field: 'MUTTYPE', + // type: 'nominal', + // legend: false, + // domain: ['Insertion', 'Deletion'] + // }, + // strokeWidth: { value: 1 }, + color: { + field: 'MUTTYPE', + type: 'nominal', + legend: false, + domain: ['Insertion', 'Deletion'] + }, + row: { + field: 'MUTTYPE', + type: 'nominal', + legend: true, + domain: ['Insertion', 'Deletion'] + }, + tooltip: [ + { field: 'POS', type: 'genomic' }, + { field: 'POSEND', type: 'genomic' }, + { field: 'MUTTYPE', type: 'nominal' }, + { field: 'ALT', type: 'nominal' }, + { field: 'REF', type: 'nominal' }, + { field: 'QUAL', type: 'quantitative' } + ], + opacity: { value: 0.9 }, + width, + height + }; +} diff --git a/src/track/index.ts b/src/track/index.ts new file mode 100644 index 00000000..58f00ecf --- /dev/null +++ b/src/track/index.ts @@ -0,0 +1,27 @@ +import driver from './driver'; +import cnv from './cnv'; +import coverage from './coverage'; +import gain from './gain'; +import loh from './loh'; +import sv from './sv'; +import mutation from './mutation'; +import indel from './indel'; +import boundary from './boundary'; +import baf from './baf'; +import biAlleleFrequency from './Bi_allel_example'; + +export type TrackMode = 'small' | 'top' | 'mid'; + +export default { + driver, + cnv, + coverage, + gain, + loh, + sv, + mutation, + indel, + boundary, + baf, + biAlleleFrequency +}; diff --git a/src/track/loh.ts b/src/track/loh.ts new file mode 100644 index 00000000..da678df4 --- /dev/null +++ b/src/track/loh.ts @@ -0,0 +1,35 @@ +import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function loh( + sampleId: string, + cnvUrl: string, + width: number, + height: number, + mode: TrackMode, + cnFields: [string, string, string] = ['total_cn', 'major_cn', 'minor_cn'] +): SingleTrack { + const [total_cn, major_cn, minor_cn] = cnFields; + return { + id: `${sampleId}-${mode}-loh`, + title: mode !== 'small' ? 'Loss of Heterozygosity (LOH)' : '', + style: { background: '#F6F6F6' }, + data: { + separator: '\t', + url: cnvUrl, + type: 'csv', + chromosomeField: 'chromosome', + genomicFields: ['start', 'end'] + }, + dataTransform: [ + { type: 'filter', field: minor_cn, inRange: [0, 0.01] }, + { type: 'filter', field: total_cn, oneOf: ['0'], not: true } + ], + mark: 'rect', + x: { field: 'start', type: 'genomic' }, + xe: { field: 'end', type: 'genomic' }, + color: { value: '#D6641E' }, + width, + height + }; +} diff --git a/src/track/mutation.ts b/src/track/mutation.ts new file mode 100644 index 00000000..5f753776 --- /dev/null +++ b/src/track/mutation.ts @@ -0,0 +1,36 @@ +import { SingleTrack } from 'gosling.js/dist/src/gosling-schema'; +import { TrackMode } from './index'; + +export default function mutation( + sampleId: string, + url: string, + indexUrl: string, + width: number, + height: number, + mode: TrackMode +): SingleTrack { + return { + id: `${sampleId}-${mode}-mutation`, + title: 'Point Mutation', + style: { background: '#FFFFFF', inlineLegend: true }, + data: { + type: 'vcf', + url, + indexUrl, + sampleLength: 500 + }, + dataTransform: [{ field: 'DISTPREV', type: 'filter', oneOf: [0], not: true }], + mark: 'point', + x: { field: 'POS', type: 'genomic' }, + color: { field: 'SUBTYPE', type: 'nominal', legend: true, domain: ['C>A', 'C>G', 'C>T', 'T>A', 'T>C', 'T>G'] }, + y: { field: 'DISTPREVLOGE', type: 'quantitative', axis: 'left', range: [10, height - 10] }, + opacity: { value: 0.9 }, + tooltip: [ + { field: 'DISTPREV', type: 'nominal', format: 's1', alt: 'Distance To Previous Mutation (BP)' }, + { field: 'POS', type: 'genomic' }, + { field: 'SUBTYPE', type: 'nominal' } + ], + width, + height + }; +} diff --git a/src/track/sv.ts b/src/track/sv.ts new file mode 100644 index 00000000..c18f9bb5 --- /dev/null +++ b/src/track/sv.ts @@ -0,0 +1,217 @@ +import { + FilterTransform, + OverlaidTracks, + SingleTrack, + StrReplaceTransform, + SvTypeTransform +} from 'gosling.js/dist/src/gosling-schema'; +import { consistentSv } from '../constants'; +import defaults from '../default-encoding'; +import { TrackMode } from '.'; + +const TRI_SIZE = 5; + +const svInfer: [SvTypeTransform, StrReplaceTransform] = [ + { + type: 'svType', + firstBp: { + chrField: 'chrom1', + posField: 'start1', + strandField: 'strand1' + }, + secondBp: { + chrField: 'chrom2', + posField: 'start2', + strandField: 'strand2' + }, + newField: 'svclass' + }, + { + type: 'replace', + field: 'svclass', + replace: [ + ...Object.entries(consistentSv).map(([from, to]) => { + return { from, to }; + }) + ], + newField: 'svclass' + } +]; + +type SvType = 'Translocation' | 'Duplication' | 'Deletion' | 'Inversion (TtT)' | 'Inversion (HtH)'; +function filterSv(oneOf: SvType[], not: boolean): FilterTransform { + return { + type: 'filter', + field: 'svclass', + oneOf, + not + }; +} + +export default function sv( + sampleId: string, + url: string, + width: number, + height: number, + mode: TrackMode, + selectedSvId: string +): OverlaidTracks { + const _baselineYMap: { [k in SvType]: { y: number; ye: number } } = { + Translocation: { y: (height / 5) * 4, ye: height }, + Deletion: { y: height / 5, ye: 1 }, + Duplication: { y: height / 5, ye: (height / 5) * 2 }, + 'Inversion (TtT)': { y: (height / 5) * 3, ye: (height / 5) * 2 }, + 'Inversion (HtH)': { y: (height / 5) * 3, ye: (height / 5) * 4 } + }; + const arcs = (sv: SvType, selected: boolean): Partial => { + const { y, ye } = _baselineYMap[sv]; + return { + // TODO: the right end does not sometimes show the arc. + dataTransform: [ + ...svInfer, + { + type: 'filter', + field: 'sv_id', + oneOf: [selectedSvId], + not: !selected + }, + filterSv([sv], false) + ], + x: { field: 'start1', type: 'genomic' }, + xe: { field: 'end2', type: 'genomic' }, + y: { value: y }, + ye: { value: ye }, + flipY: true, + ...(selected ? { opacity: { value: 1 }, strokeWidth: { value: 2 } } : {}), + ...(selected && sv === 'Translocation' ? { stroke: { value: 'grey' } } : {}) + }; + }; + const svs = [...defaults.color.svclass.domain]; + return { + id: `${sampleId}-${mode}-sv`, + alignment: 'overlay', + experimental: { + mouseEvents: { + click: true, + mouseOver: true, + groupMarksByField: 'sv_id' + }, + performanceMode: true + }, + data: { + url, + type: 'csv', + separator: '\t', + genomicFieldsToConvert: [ + { + chromosomeField: 'chrom1', + genomicFields: ['start1', 'end1'] + }, + { + chromosomeField: 'chrom2', + genomicFields: ['start2', 'end2'] + } + ] + }, + mark: 'withinLink', + tracks: [ + ...svs.map(d => arcs(d as any, false)), + ...svs.map(d => arcs(d as any, true)), + ...((mode !== 'mid' + ? [] + : [ + { + dataTransform: [{ type: 'filter', field: 'strand1', oneOf: ['+'] }], + mark: 'triangleLeft', + x: { field: 'start1', type: 'genomic' }, + size: { value: TRI_SIZE }, + y: { value: height }, + stroke: { value: 0 }, + style: { align: 'right' } + }, + { + dataTransform: [{ type: 'filter', field: 'strand1', oneOf: ['-'] }], + mark: 'triangleRight', + x: { field: 'start1', type: 'genomic' }, + size: { value: TRI_SIZE }, + y: { value: height }, + stroke: { value: 0 }, + style: { align: 'left' } + }, + { + dataTransform: [{ type: 'filter', field: 'strand2', oneOf: ['+'] }], + mark: 'triangleLeft', + x: { field: 'end2', type: 'genomic' }, + size: { value: TRI_SIZE }, + y: { value: height }, + stroke: { value: 0 }, + style: { align: 'right' } + }, + { + dataTransform: [{ type: 'filter', field: 'strand2', oneOf: ['-'] }], + mark: 'triangleRight', + x: { field: 'end2', type: 'genomic' }, + size: { value: TRI_SIZE }, + y: { value: height }, + stroke: { value: 0 }, + style: { align: 'left' } + } + ]) as OverlaidTracks[]), + ...((mode !== 'mid' + ? [] + : [ + { + dataTransform: [...svInfer, { type: 'filter', field: 'sv_id', oneOf: [selectedSvId] }], + mark: 'rule', + x: { field: 'start1', type: 'genomic' }, + color: { value: 'black' }, + strokeWidth: { value: 1 }, + opacity: { value: 1 }, + style: { dashed: [3, 3] } + }, + { + dataTransform: [...svInfer, { type: 'filter', field: 'sv_id', oneOf: [selectedSvId] }], + mark: 'rule', + x: { field: 'end2', type: 'genomic' }, + color: { value: 'black' }, + strokeWidth: { value: 1 }, + opacity: { value: 1 }, + style: { dashed: [3, 3] } + } + ]) as OverlaidTracks[]) + ], + y: { value: height / 5 }, + color: { + field: 'svclass', + type: 'nominal', + legend: mode === 'mid', + domain: ['Gain', 'LOH', ...defaults.color.svclass.domain], + range: ['#5CB6EA', '#D6641E', ...defaults.color.svclass.range] + }, + stroke: { + field: 'svclass', + type: 'nominal', + domain: defaults.color.svclass.domain, + range: defaults.color.svclass.range + }, + strokeWidth: { value: 1 }, + opacity: { value: 0.7 }, + tooltip: [ + { field: 'start1', type: 'genomic' }, + { field: 'end2', type: 'genomic' }, + { field: 'strand1', type: 'nominal' }, + { field: 'strand2', type: 'nominal' }, + { field: 'svclass', type: 'nominal' }, + { field: 'sv_id', type: 'nominal' }, + { field: 'pe_support', type: 'nominal' } + ], + style: { + linkStyle: 'elliptical', + linkMinHeight: 0.7, + mouseOver: { stroke: '#242424', strokeWidth: 1 }, + withinLinkVerticalLines: true + }, + width, + height + }; +} From 266334597c1a39b5205c8e139e60af8a864e5460 Mon Sep 17 00:00:00 2001 From: tsertijn Date: Fri, 5 Jul 2024 11:10:44 +0200 Subject: [PATCH 18/57] Delete src/App.tsx problems git push --- src/App.tsx | 1535 --------------------------------------------------- 1 file changed, 1535 deletions(-) delete mode 100644 src/App.tsx diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 431fb7c8..00000000 --- a/src/App.tsx +++ /dev/null @@ -1,1535 +0,0 @@ -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { GoslingComponent, GoslingRef, embed } from 'gosling.js'; -import { debounce, sample } from 'lodash'; -import type { RouteComponentProps } from 'react-router-dom'; -import generateSpec from './main-spec'; -import ErrorBoundary from './error'; -import _allDrivers from './data/driver.json'; -import _customDrivers from './data/driver.custom.json'; -import samples, { SampleType } from './data/samples'; -import getOneOfSmallMultiplesSpec from './small-multiples-spec'; -import { CHROMOSOMES, THEME, WHOLE_CHROMOSOME_STR } from './constants'; -import { ICONS } from './icon'; -import './App.css'; -import { INTERNAL_SAVED_THUMBNAILS } from './data/external-thumbnails'; -import { isChrome } from './utils'; -import THUMBNAIL_PLACEHOLDER from './script/img/placeholder.png'; -import { Database } from './database'; -import { getHtmlTemplate } from './html-template'; -import { EXTERNAL_THUMBNAILS } from './data/stevens-mpnst'; -import CancerSelector from './ui/cancer-selector'; -import HorizontalLine from './ui/horizontal-line'; -import SampleConfigForm from './ui/sample-config-form'; -import { BrowserDatabase } from './browser-log'; -import legend from './legend.png'; -import { ExportDropdown } from './ui/ExportDropdown'; - -const db = new Database(); -const log = new BrowserDatabase(); - -const DB_DO_NOT_SHOW_ABOUT_BY_DEFAULT = (await log.get())?.doNotShowAboutByDefault ?? false; -const DATABSE_THUMBNAILS = await db.get(); -const GENERATED_THUMBNAILS = {}; - -const INIT_VIS_PANEL_WIDTH = window.innerWidth; -const ZOOM_PADDING = 200; -const ZOOM_DURATION = 500; - -const allDrivers = [ - ...(_allDrivers as any), - ..._customDrivers.map(d => { - return { ...d, sample_id: 'SRR7890905' }; - }), - ..._customDrivers.map(d => { - return { ...d, sample_id: 'SRR7890905_Hartwig' }; - }) -]; - -function App(props: RouteComponentProps) { - // URL parameters - const urlParams = new URLSearchParams(props.location.search); - const isMinimalMode = urlParams.get('minimal_mode') === 'true'; - const VIS_PADDING = { - top: isMinimalMode ? 0 : 60, - right: isMinimalMode ? 0 : 60, - bottom: isMinimalMode ? 0 : 60, - left: isMinimalMode ? 0 : 60 - }; - - // !! instead of using `urlParams.get('external')`, we directly parse the external URL in order to include - // any inlined parameters of the external link (e.g., private AWS link with authentication info.) - let externalUrl = null; - if (props.location.search.includes('external=')) { - externalUrl = props.location.search.split('external=')[1]; - // remove known parameters - externalUrl = externalUrl.split('&demoIndex')[0]; - externalUrl = externalUrl.split('&example')[0]; - externalUrl = externalUrl.split('&domain')[0]; - } - const exampleId = urlParams.get('example'); - const xDomain = urlParams.get('domain') - ? urlParams - .get('domain') - .split('-') - .map(d => +d) - : null; - const demoIndex = useRef(+urlParams.get('demoIndex') ?? 0); - const [showSmallMultiples, setShowSmallMultiples] = useState(externalUrl === null); - const [ready, setReady] = useState(externalUrl === null); - - const selectedSamples = useMemo( - () => (!exampleId ? samples.filter(d => d.group === 'default') : samples.filter(d => d.group === exampleId)), - [exampleId] - ); - - const gosRef = useRef(); - - // demo - const [demo, setDemo] = useState( - selectedSamples[demoIndex.current < selectedSamples.length ? demoIndex.current : 0] - ); - const externalDemoUrl = useRef(); - - const currentSpec = useRef(); - - // interactions - const [showSamples, setShowSamples] = useState(urlParams.get('showSamples') !== 'false' && !xDomain); - const [showAbout, setShowAbout] = useState(false); - const [thumbnailForceGenerate, setThumbnailForceGenerate] = useState(false); - const [generateThumbnails, setGenerateThumbnails] = useState(false); - const [doneGeneratingThumbnails, setDoneGeneratingThumbnails] = useState(false); - const [filterSampleBy, setFilterSampleBy] = useState(''); - const [filteredSamples, setFilteredSamples] = useState(selectedSamples); - const [showOverview, setShowOverview] = useState(true); - const [showPutativeDriver, setShowPutativeDriver] = useState(true); - const [interactiveMode, setInteractiveMode] = useState(isMinimalMode ?? false); - const [visPanelWidth, setVisPanelWidth] = useState( - INIT_VIS_PANEL_WIDTH - (isMinimalMode ? 10 : VIS_PADDING.left * 2) - ); - const [overviewChr, setOverviewChr] = useState(''); - const [genomeViewChr, setGenomeViewChr] = useState(''); - const [drivers, setDrivers] = useState( - typeof demo.drivers === 'string' && demo.drivers.split('.').pop() === 'json' ? [] : getFilteredDrivers(demo.id) - ); - const [selectedSvId, setSelectedSvId] = useState(''); - const [breakpoints, setBreakpoints] = useState<[number, number, number, number]>([1, 100, 1, 100]); - const [bpIntervals, setBpIntervals] = useState<[number, number, number, number] | undefined>(); - const [mouseOnVis, setMouseOnVis] = useState(isMinimalMode ?? false); - const [jumpButtonInfo, setJumpButtonInfo] = - useState<{ id: string; x: number; y: number; direction: 'leftward' | 'rightward'; zoomTo: () => void }>(); - const mousePos = useRef({ x: -100, y: -100 }); - const prevJumpId = useRef(''); - - // SV data - const leftReads = useRef<{ [k: string]: number | string }[]>([]); - const rightReads = useRef<{ [k: string]: number | string }[]>([]); - const [svReads, setSvReads] = useState<{ name: string; type: string }[]>([]); - - function getFilteredDrivers(demoId: string) { - return (allDrivers as any).filter((d: any) => d.sample_id === demoId && +d.pos); - } - - // update demo - useEffect(() => { - if (typeof demo.drivers === 'string' && demo.drivers.split('.').pop() === 'json') { - // we want to change this json file to json value - fetch(demo.drivers).then(response => - response.text().then(d => { - const customDrivers = JSON.parse(d); - // TODO: these need to be supported in other types of data - customDrivers.forEach(d => { - const optionalFields = [ - 'ref', - 'alt', - 'category', - 'top_category', - 'transcript_consequence', - 'protein-mutation', - 'allele_fraction', - 'mutation_type', - 'biallelic' - ]; - optionalFields.forEach(f => { - if (!d[f]) { - d[f] = ''; - } - }); - if (typeof d['biallelic'] === 'string' && d['biallelic'].toUpperCase() === 'YES') { - d['biallelic'] = 'yes'; - } - if (typeof d['biallelic'] === 'string' && d['biallelic'].toUpperCase() === 'NO') { - d['biallelic'] = 'no'; - } - }); - - setDrivers(customDrivers); - }) - ); - } else { - const filteredDrivers = getFilteredDrivers(demo.id); - setDrivers(filteredDrivers); - } - - setOverviewChr(''); - setGenomeViewChr(''); - setSelectedSvId(''); - leftReads.current = []; - rightReads.current = []; - }, [demo]); - - useEffect(() => { - if (externalUrl) { - fetch(externalUrl).then(response => - response.text().then(d => { - let externalDemo = JSON.parse(d); - if (Array.isArray(externalDemo) && externalDemo.length >= 0) { - setFilteredSamples(externalDemo); - externalDemo = externalDemo[demoIndex.current < externalDemo.length ? demoIndex.current : 0]; - } else { - setFilteredSamples([externalDemo]); - } - if (externalDemo) { - setDemo(externalDemo); - } - setShowSmallMultiples(true); - setReady(true); - }) - ); - } - }, []); - - useEffect(() => { - prevJumpId.current = jumpButtonInfo?.id; - }, [jumpButtonInfo]); - - useEffect(() => { - setFilteredSamples( - filterSampleBy === '' ? selectedSamples : selectedSamples.filter(d => d.id.includes(filterSampleBy)) - ); - }, [filterSampleBy]); - - useEffect(() => { - if (!gosRef.current || !demo.bai || !demo.bam) return; - - gosRef.current.api.subscribe('rawData', (type: string, e: any) => { - if (e.id.includes('bam') && (leftReads.current.length === 0 || rightReads.current.length === 0)) { - const isThisPotentiallyJsonRuleData = typeof e.data[0]?.name === 'undefined'; - if (isThisPotentiallyJsonRuleData) { - return; - } - - /// DEBUG - // console.log(e.id, e.data); - /// - - // This means we just received a BAM data that is just rendered - if (e.id.includes('left') && leftReads.current.length === 0) { - leftReads.current = e.data; - } else if (e.id.includes('right') && rightReads.current.length === 0) { - rightReads.current = e.data; - } - - // !! This is to drop duplicated data records. - // Multiple tracks overlaid on alignment tracks makes duplicated data records. - leftReads.current = Array.from(new Set(leftReads.current.map(d => JSON.stringify(d)))).map(d => - JSON.parse(d) - ); - rightReads.current = Array.from(new Set(rightReads.current.map(d => JSON.stringify(d)))).map(d => - JSON.parse(d) - ); - - // Reads on both views prepared? - if (leftReads.current.length !== 0 && rightReads.current.length !== 0) { - const mates = leftReads.current - .filter(l => rightReads.current.filter(r => r.name === l.name && r.id !== l.id).length !== 0) - .map(d => d.name as string); - - const matesWithSv = mates.map(name => { - const matesOnLeft = leftReads.current.filter(d => d.name === name); - const matesOnRight = rightReads.current.filter(d => d.name === name); - - if (matesOnLeft.length !== 1 || matesOnRight.length !== 1) { - // We do not do anything for this case for now. - return { name, type: 'unknown' }; - } - - // console.log(matesOnLeft[0], matesOnRight[0]); - const ld = matesOnLeft[0].strand; - const rd = matesOnRight[0].strand; - - if (matesOnLeft[0].chrName !== matesOnRight[0].chrName) return { name, type: 'Translocation' }; - if (ld === '+' && rd === '-') return { name, type: 'Deletion' }; - if (ld === '-' && rd === '-') return { name, type: 'Inversion (TtT)' }; - if (ld === '+' && rd === '+') return { name, type: 'Inversion (HtH)' }; - if (ld === '-' && rd === '+') return { name, type: 'Duplication' }; - return { name, type: 'unknown' }; - }); - - if ( - matesWithSv - .map(d => d.name) - .sort() - .join() !== - svReads - .map(d => d.name) - .sort() - .join() - ) { - setSvReads(matesWithSv); - } - } - } - }); - - return () => { - gosRef.current.api.unsubscribe('rawData'); - }; - }, [gosRef, svReads, demo]); - - useEffect(() => { - if (!overviewChr) return; - - if (overviewChr.includes('chr')) { - gosRef.current?.api.zoomTo(`${demo.id}-top-ideogram`, overviewChr, 0, 0); - setTimeout(() => setGenomeViewChr(overviewChr), 0); - } else { - gosRef.current?.api.zoomToExtent(`${demo.id}-top-ideogram`, ZOOM_DURATION); - } - }, [overviewChr]); - - useEffect(() => { - if (!genomeViewChr) return; - - if (genomeViewChr.includes('chr')) { - gosRef.current?.api.zoomTo(`${demo.id}-mid-ideogram`, genomeViewChr, ZOOM_PADDING, ZOOM_DURATION); - } else { - gosRef.current?.api.zoomToExtent(`${demo.id}-mid-ideogram`, ZOOM_DURATION); - } - }, [genomeViewChr]); - - // change the width of the visualization panel and register intersection observer - useEffect(() => { - window.addEventListener( - 'resize', - debounce(() => { - setVisPanelWidth(window.innerWidth - VIS_PADDING.left * 2); - }, 500) - ); - - // Lower opacity of legend image as it leaves viewport - if (isMinimalMode) { - const legendElement = document.querySelector('.genome-view-legend'); - const options = { - root: document.querySelector('.minimal_mode'), - rootMargin: '-250px 0px 0px 0px', - threshold: [0.9, 0.75, 0.5, 0.25, 0] - }; - - const observer = new IntersectionObserver(entry => { - // Set intersection ratio as opacity (round up to one decimal place) - legendElement.style.opacity = '' + Math.ceil(10 * entry[0].intersectionRatio) / 10; - }, options); - - observer.observe(legendElement); - } - }, []); - - const getThumbnail = (d: SampleType) => { - return ( - d.thumbnail || - INTERNAL_SAVED_THUMBNAILS[d.id] || - EXTERNAL_THUMBNAILS[d.id] || - DATABSE_THUMBNAILS.find(db => db.id === d.id)?.dataUrl || - GENERATED_THUMBNAILS[d.id] - ); - }; - - const AvailabilityIcon = (isAvailable: boolean) => { - return ( - - Export Image - {(isAvailable ? ICONS.CHECKSQUARE : ICONS.SQUARE).path.map(p => ( - - ))} - - ); - }; - const smallOverviewWrapper = useMemo(() => { - // !! Uncomment the following lines to generated specs for making thumbnails. - // console.log( - // 'overviewSpec', - // filteredSamples.map(d => - // getOneOfSmallMultiplesSpec({ - // cnvUrl: d.cnv, - // svUrl: d.sv, - // width: 1200, - // title: d.cancer.charAt(0).toUpperCase() + d.cancer.slice(1), - // subtitle: d.id, // '' + d.id.slice(0, 20) + (d.id.length >= 20 ? '...' : ''), - // cnFields: d.cnFields ?? ['total_cn', 'major_cn', 'minor_cn'] - // }) - // ), - // filteredSamples.map(d => `node gosling-screenshot.js output/${d.id}.json img/${d.id}.jpeg`).join('\n') - // ); - // return []; - /* Load image if necessary */ - const noThumbnail = filteredSamples.filter(d => !getThumbnail(d))[0]; - if (noThumbnail && generateThumbnails) { - const { id } = noThumbnail; - const spec = getOneOfSmallMultiplesSpec({ - cnvUrl: noThumbnail.cnv, - svUrl: noThumbnail.sv, - width: 600, - title: noThumbnail.cancer.charAt(0).toUpperCase() + noThumbnail.cancer.slice(1), - subtitle: id, - cnFields: noThumbnail.cnFields ?? ['total_cn', 'major_cn', 'minor_cn'] - }); - const hidden = document.getElementById('hidden-gosling'); - embed(hidden, spec, { padding: 0, margin: 10 }).then(api => { - setTimeout(() => { - const { canvas } = api.getCanvas(); - const dataUrl = canvas.toDataURL('image/png'); - GENERATED_THUMBNAILS[noThumbnail.id] = dataUrl; - db.add(id, dataUrl); - setThumbnailForceGenerate(!thumbnailForceGenerate); - }, 10000); - }); - } - if (noThumbnail) { - setDoneGeneratingThumbnails(false); - } else { - setDoneGeneratingThumbnails(true); - } - return filteredSamples.map((d, i) => ( -
{ - demoIndex.current = i; - setShowSamples(false); - setTimeout(() => { - setDemo(d); - }, 300); - }} - className={demo === d ? 'selected-overview' : 'unselected-overview'} - > -
- {d.cancer.charAt(0).toUpperCase() + d.cancer.slice(1).split(' ')[0]} -
-
- {'' + d.id.slice(0, 20) + (d.id.length >= 20 ? '...' : '')} -
-
- {getThumbnail(d) ? ( - - ) : ( - //
- // = 20 ? '...' : ''), - // cnFields: d.cnFields ?? ['total_cn', 'major_cn', 'minor_cn'] - // })} - // /> - //
- <> - - - {generateThumbnails ? 'Loading...' : 'Thumbnail Missing'} - - - )} - {d.assembly ?? 'hg38'} -
-
-
{AvailabilityIcon(true)}SV
-
- {AvailabilityIcon(!!d.vcf && !!d.vcfIndex)}Point Mutation -
-
- {AvailabilityIcon(!!d.vcf2 && !!d.vcf2Index)}Indel -
-
- {AvailabilityIcon(!!d.bam && !!d.bai)}Read Alignment -
- {d.note ?
{d.note}
: null} -
-
- )); - // smallOverviewGoslingComponents.map(([component, spec], i) => ( - //
{ - // setShowSamples(false); - // setTimeout(() => { - // setDemoIdx(i); - // setSelectedSvId(''); - // }, 300); - // }} - // className={demoIdx === i ? 'selected-overview' : 'unselected-overview'} - // > - // {component} - //
- // )); - }, [demo, filteredSamples, thumbnailForceGenerate, generateThumbnails]); - - const hidiveLabRef = ( - <> - {' ('} - - HiDIVE Lab - - {')'} - - ); - const parkLabRef = ( - <> - {' ('} - - Park Lab - - {')'} - - ); - const goslingComponent = useMemo(() => { - const loadingCustomJSONDrivers = typeof demo.drivers === 'string' && demo.drivers.split('.').pop() === 'json'; - const isStillLoadingDrivers = loadingCustomJSONDrivers && drivers.length == 0; - if (!ready || isStillLoadingDrivers) return null; - - const useCustomDrivers = loadingCustomJSONDrivers || !demo.drivers; - const spec = generateSpec({ - ...demo, - showOverview, - xDomain: xDomain as [number, number], - xOffset: 0, - showPutativeDriver, - width: visPanelWidth, - drivers: useCustomDrivers ? drivers : demo.drivers, - selectedSvId, - breakpoints: breakpoints, - crossChr: false, - bpIntervals, - svReads, - spacing: isMinimalMode ? 100 : 40 - }); - currentSpec.current = JSON.stringify(spec); - // console.log('spec', spec); - return ( - - ); - // !! Removed `demo` not to update twice since `drivers` are updated right after a demo update. - }, [ready, xDomain, visPanelWidth, drivers, showOverview, showPutativeDriver, selectedSvId, breakpoints, svReads]); - - useLayoutEffect(() => { - if (!gosRef.current) return; - - gosRef.current.api.subscribe('click', (_, e) => { - // console.log(e); - - let x = +e.data[0].start1; - let xe = +e.data[0].end1; - let x1 = +e.data[0].start2; - let x1e = +e.data[0].end2; - - // safetly swap - if (x > x1) { - x = +e.data[0].start2; - xe = +e.data[0].end2; - x1 = +e.data[0].start1; - x1e = +e.data[0].end1; - } - - let zoomStart = x; - let zoomEnd = x1e; - let padding = (zoomEnd - zoomStart) / 4.0; - if (e.data[0].svclass === 'Translocation') { - zoomStart = x; - zoomEnd = xe; - padding = 10000; - } - - gosRef.current.api.zoomTo( - `${demo.id}-mid-ideogram`, - `chr1:${zoomStart}-${zoomEnd}`, - padding, - ZOOM_DURATION - ); - - // we will show the bam files, so set the initial positions - setBreakpoints([+x - ZOOM_PADDING, +xe + ZOOM_PADDING, +x1 - ZOOM_PADDING, +x1e + ZOOM_PADDING]); - setBpIntervals([x, xe, x1, x1e]); - setSelectedSvId(e.data[0].sv_id + ''); - - // move to the bottom - setTimeout( - () => document.getElementById('gosling-panel')?.scrollTo({ top: 1000000, behavior: 'smooth' }), - 2000 - ); - - leftReads.current = []; - rightReads.current = []; - }); - - gosRef.current.api.subscribe('mouseOver', (_, e) => { - const sanitizedChr = (c: string | number) => { - return `${c}`.replace('chr', ''); - }; - const calDir = (c1: string | number, c2: string | number) => { - c1 = sanitizedChr(c1); - c2 = sanitizedChr(c2); - if (+c1 && +c1 <= 9) { - c1 = '0' + c1; - } - if (+c2 && +c2 <= 9) { - c2 = '0' + c2; - } - return c1 < c2 ? 'rightward' : 'leftward'; - }; - if (e.id.includes('-mid-sv') && e.data[0].svclass === 'Translocation') { - const { chromosome: c, position: p } = e.genomicPosition; - const padding = 100000; - if (sanitizedChr(c) === sanitizedChr(e.data[0].chrom1)) { - const direction = calDir(c, e.data[0].chrom2); - const id = e.data[0].sv_id + '-' + direction; - if (id === prevJumpId.current) return; - const { start2, end2 } = e.data[0]; - setJumpButtonInfo({ - id, - x: mousePos.current.x, - y: mousePos.current.y, - direction, - zoomTo: () => gosRef.current.api.zoomTo(e.id, `chr1:${start2}-${end2}`, padding, ZOOM_DURATION) - }); - } else { - const direction = calDir(c, e.data[0].chrom1); - const id = e.data[0].sv_id + '-' + direction; - if (id === prevJumpId.current) return; - const { start1, end1 } = e.data[0]; - setJumpButtonInfo({ - id, - x: mousePos.current.x, - y: mousePos.current.y, - direction, - zoomTo: () => gosRef.current.api.zoomTo(e.id, `chr1:${start1}-${end1}`, padding, ZOOM_DURATION) - }); - } - } else { - setJumpButtonInfo(undefined); - } - }); - - return () => { - gosRef.current?.api.unsubscribe('click'); - gosRef.current?.api.unsubscribe('mouseOver'); - }; - }); - - return ( - -
{ - const top = e.clientY; - const left = e.clientX; - const width = window.innerWidth; - const height = window.innerHeight; - if (!isMinimalMode) { - if ( - VIS_PADDING.top < top && - top < height - VIS_PADDING.top && - VIS_PADDING.left < left && - left < width - VIS_PADDING.left - ) { - setMouseOnVis(true); - } else { - setMouseOnVis(false); - } - } - mousePos.current = { x: left, y: top }; - }} - onWheel={() => setJumpButtonInfo(undefined)} - onClick={() => { - if (!mouseOnVis && interactiveMode) setInteractiveMode(false); - else if (mouseOnVis && !interactiveMode) setInteractiveMode(true); - setJumpButtonInfo(undefined); - }} - > - {!isMinimalMode && ( - - )} - {!isMinimalMode && ( - { - setShowSamples(true); - }} - > - Menu - - - )} -
- {!isMinimalMode && ( - <> - - CHROMOSCOPE - - { - setShowAbout(true); - }} - > - - - - About - - {' | '} - {/* {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1) + ' • ' + demo.id} */} - {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1)} - {demo.id} - - )} - {!isMinimalMode && ( - <> - gosRef.current?.api.exportPng()}> - - Export Image - {ICONS.PNG.path.map(p => ( - - ))} - - - { - const a = document.createElement('a'); - a.setAttribute( - 'href', - `data:text/plain;charset=utf-8,${encodeURIComponent( - getHtmlTemplate(currentSpec.current) - )}` - ); - a.download = 'visualization.html'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - }} - style={{ marginLeft: 40 }} - > - - Export HTML - {ICONS.HTML.path.map(p => ( - - ))} - - - { - const a = document.createElement('a'); - a.setAttribute( - 'href', - `data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}` - ); - a.download = 'visualization.json'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - }} - style={{ marginLeft: 70 }} - > - - Export Gosling Spec (JSON) - {ICONS.JSON.path.map(p => ( - - ))} - - - { - const { xDomain } = gosRef.current.hgApi.api.getLocation(`${demo.id}-mid-ideogram`); - if (xDomain) { - // urlParams.set('demoIndex', demoIndex.current + ''); - // urlParams.set('domain', xDomain.join('-')); - let newUrl = window.location.origin + window.location.pathname + '?'; - newUrl += `demoIndex=${demoIndex.current}`; - newUrl += `&domain=${xDomain.join('-')}`; - if (externalDemoUrl.current) { - newUrl += `&external=${externalDemoUrl.current}`; - } else if (externalUrl) { - newUrl += `&external=${externalUrl}`; - } - navigator.clipboard - .writeText(newUrl) - .then(() => - alert( - 'The URL of the current session has been copied to your clipboard.' - ) - ); - } - }} - style={{ marginLeft: 100 }} - > - - Export Link - - - - - - )} - {!isChrome() ? ( - - ⚠️ Chromoscope is optimized for Google Chrome - - ) : null} - {!isMinimalMode && ( - <> - - - GitHub - - - GitHub - - - - - - Documentation - - - )} -
-
- {!isMinimalMode && ( -
-
{ - if (e.target === e.currentTarget) setShowSamples(false); - }} - > - { - setShowSamples(false); - }} - > - Close - - -
- CHROMOSCOPE - { - setShowAbout(true); - }} - > - - - - About - - {' | '} Samples - setFilterSampleBy(e.target.value)} - hidden - /> -
- - - GitHub - - - GitHub - - - - - - Documentation - - -
-
-
- { - fetch(url).then(response => - response.text().then(d => { - let externalDemo = JSON.parse(d); - if (Array.isArray(externalDemo) && externalDemo.length >= 0) { - setFilteredSamples(externalDemo); - externalDemo = - externalDemo[ - demoIndex.current < externalDemo.length - ? demoIndex.current - : 0 - ]; - } - if (externalDemo) { - externalDemoUrl.current = url; - setDemo(externalDemo); - } - }) - ); - }} - /> - - { - setFilteredSamples([ - { - ...config, - group: 'default' - }, - ...filteredSamples - ]); - }} - /> -
-
{`Total of ${filteredSamples.length} samples loaded`}
-
{smallOverviewWrapper}
-
-
- )} -
- {goslingComponent} - {jumpButtonInfo ? ( - - ) : null} - {!isMinimalMode && ( -
- )} - {isMinimalMode ? ( -
- - -
- ) : null} - { - // External links and export buttons - isMinimalMode ? ( -
- -
- ) : null - } -
- -
- -
- - - - { - // const keyword = e.target.value; - // if(keyword !== "" && !keyword.startsWith("c")) { - // gosRef.current.api.suggestGene(keyword, (suggestions) => { - // setGeneSuggestions(suggestions); - // }); - // setSuggestionPosition({ - // left: searchBoxRef.current.getBoundingClientRect().left, - // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, - // }); - // } else { - // setGeneSuggestions([]); - // } - // setSearchKeyword(keyword); - // }} - onKeyDown={e => { - const keyword = (e.target as HTMLTextAreaElement).value; - switch (e.key) { - case 'ArrowUp': - break; - case 'ArrowDown': - break; - case 'Enter': - // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 - gosRef.current.hgApi.api.zoomToGene( - `${demo.id}-mid-ideogram`, - keyword, - 10000, - 1000 - ); - break; - case 'Esc': - case 'Escape': - break; - } - }} - /> -
-
-
- - -
-
- - -
-
-
-
-
-
- {!isMinimalMode && ( -
- {!interactiveMode - ? 'Click inside to use interactions on visualizations' - : 'Click outside to deactivate interactions and scroll the page'} -
- )} - {!isMinimalMode && ( -
- )} -
- {'ⓘ No read alignment data available for this sample.'} -
-
setShowAbout(false)} - /> - {!isMinimalMode && ( -
- -

- Chromoscope - {' | '}About -

- -

- Whole genome sequencing is now routinely used to profile mutations in DNA in the soma and in - the germline, informing molecular diagnoses of disease and therapeutic decisions. Structural - variants (SVs) are the main new type of alterations we see more of, and they are often - diagnostic, prognostic, or therapy-informing. However, the size and complexity of SV data, - combined with the difficulty of obtaining accurate SV calls, pose challenges in the - interpretation of SVs, requiring tedious visual inspection of potentially pathogenic - variants with multiple visualization tools. -

- -

- To overcome the problems with the interpretation of SVs, we developed Chromoscope, an - open-source web-based application for the interactive visualization of structural variants. - Chromoscope has several innovative features which unlock the insights from whole genome - sequencing: visualization at multiple scale levels simultaneously, effective navigation - across scales, easy setup for loading users' large datasets, and a feature to export, - share, and further customize visualizations. We anticipate that Chromoscope will accelerate - the exploration and interpretation of SVs by a broad range of scientists and clinicians, - leading to new insights into genomic biomarkers. -

-

Learn more about Chromoscope

- -

The Team

-
    -
  • - Sehi L'Yi - {hidiveLabRef} -
  • -
  • - Dominika Maziec - {parkLabRef} -
  • -
  • - Victoria Stevens - {parkLabRef} -
  • -
  • - Trevor Manz - {hidiveLabRef} -
  • -
  • - Alexander Veit - {parkLabRef} -
  • -
  • - Michele Berselli - {parkLabRef} -
  • -
  • - Peter J Park - {parkLabRef} -
  • -
  • - Dominik Glodzik - {parkLabRef} -
  • -
  • - Nils Gehlenborg - {hidiveLabRef} -
  • -
- -
- )} -
{ - setTimeout( - () => document.getElementById('gosling-panel')?.scrollTo({ top: 0, behavior: 'smooth' }), - 0 - ); - }} - > - - Scroll To Top - - -
-
-
- - ); -} - -export default App; From eeb8f800dd02ccc0e04f94cc3fb7961295c050de Mon Sep 17 00:00:00 2001 From: tsertijn Date: Fri, 5 Jul 2024 11:11:06 +0200 Subject: [PATCH 19/57] Add files via upload --- src/App.tsx | 1399 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1399 insertions(+) create mode 100644 src/App.tsx diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..9d743886 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,1399 @@ +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { GoslingComponent, GoslingRef, embed } from 'gosling.js'; +import { debounce, sample } from 'lodash'; +import type { RouteComponentProps } from 'react-router-dom'; +import generateSpec from './main-spec'; +import ErrorBoundary from './error'; +import _allDrivers from './data/driver.json'; +import _customDrivers from './data/driver.custom.json'; +import samples, { SampleType } from './data/samples'; +import getOneOfSmallMultiplesSpec from './small-multiples-spec'; +import { CHROMOSOMES, THEME, WHOLE_CHROMOSOME_STR } from './constants'; +import { ICONS } from './icon'; +import './App.css'; +import { INTERNAL_SAVED_THUMBNAILS } from './data/external-thumbnails'; +import { isChrome } from './utils'; +import THUMBNAIL_PLACEHOLDER from './script/img/placeholder.png'; +import { Database } from './database'; +import { getHtmlTemplate } from './html-template'; +import { EXTERNAL_THUMBNAILS } from './data/stevens-mpnst'; +import CancerSelector from './ui/cancer-selector'; +import HorizontalLine from './ui/horizontal-line'; +import SampleConfigForm from './ui/sample-config-form'; +import { BrowserDatabase } from './browser-log'; +import legend from './legend.png'; +import UrlsafeCodec from './lib/urlsafe-codec'; +//import GenomicTable from './ui/genomic-table'; + +const db = new Database(); +const log = new BrowserDatabase(); + +const DB_DO_NOT_SHOW_ABOUT_BY_DEFAULT = (await log.get())?.doNotShowAboutByDefault ?? false; +const DATABSE_THUMBNAILS = await db.get(); +const GENERATED_THUMBNAILS = {}; + +const INIT_VIS_PANEL_WIDTH = window.innerWidth; +const VIS_PADDING = 60; +const ZOOM_PADDING = 200; +const ZOOM_DURATION = 500; + +const allDrivers = [ + ...(_allDrivers as any), + ..._customDrivers.map(d => { + return { ...d, sample_id: 'SRR7890905' }; + }), + ..._customDrivers.map(d => { + return { ...d, sample_id: 'SRR7890905_Hartwig' }; + }) +]; + +function App(props: RouteComponentProps) { + // URL parameters + const urlParams = new URLSearchParams(props.location.search); + // !! instead of using `urlParams.get('external')`, we directly parse the external URL in order to include + // any inlined parameters of the external link (e.g., private AWS link with authentication info.) + let externalUrl = null; + if (props.location.search.includes('external=')) { + externalUrl = props.location.search.split('external=')[1]; + // remove known parameters + externalUrl = externalUrl.split('&demoIndex')[0]; + externalUrl = externalUrl.split('&example')[0]; + externalUrl = externalUrl.split('&domain')[0]; + } + const exampleId = urlParams.get('example'); + const xDomain = urlParams.get('domain') + ? urlParams + .get('domain') + .split('-') + .map(d => +d) + : null; + const demoIndex = useRef(+urlParams.get('demoIndex') ?? 0); + const [showSmallMultiples, setShowSmallMultiples] = useState(externalUrl === null); + const [ready, setReady] = useState(externalUrl === null); + + const selectedSamples = useMemo( + () => (!exampleId ? samples.filter(d => d.group === 'default') : samples.filter(d => d.group === exampleId)), + [exampleId] + ); + + const gosRef = useRef(); + + // demo + const [demo, setDemo] = useState( + selectedSamples[demoIndex.current < selectedSamples.length ? demoIndex.current : 0] + ); + const externalDemoUrl = useRef(); + + const currentSpec = useRef(); + + // interactions + const [showSamples, setShowSamples] = useState(urlParams.get('showSamples') !== 'false' && !xDomain); + const [showAbout, setShowAbout] = useState(false); + const [thumbnailForceGenerate, setThumbnailForceGenerate] = useState(false); + const [generateThumbnails, setGenerateThumbnails] = useState(false); + const [doneGeneratingThumbnails, setDoneGeneratingThumbnails] = useState(false); + const [filterSampleBy, setFilterSampleBy] = useState(''); + const [filteredSamples, setFilteredSamples] = useState(selectedSamples); + const [showOverview, setShowOverview] = useState(true); + const [showPutativeDriver, setShowPutativeDriver] = useState(true); + const [interactiveMode, setInteractiveMode] = useState(false); + const [visPanelWidth, setVisPanelWidth] = useState(INIT_VIS_PANEL_WIDTH - VIS_PADDING * 2); + const [overviewChr, setOverviewChr] = useState(''); + const [genomeViewChr, setGenomeViewChr] = useState(''); + const [drivers, setDrivers] = useState( + typeof demo.drivers === 'string' && demo.drivers.split('.').pop() === 'json' ? [] : getFilteredDrivers(demo.id) + ); + const [selectedSvId, setSelectedSvId] = useState(''); + const [breakpoints, setBreakpoints] = useState<[number, number, number, number]>([1, 100, 1, 100]); + const [bpIntervals, setBpIntervals] = useState<[number, number, number, number] | undefined>(); + const [mouseOnVis, setMouseOnVis] = useState(false); + const [jumpButtonInfo, setJumpButtonInfo] = + useState<{ id: string; x: number; y: number; direction: 'leftward' | 'rightward'; zoomTo: () => void }>(); + const mousePos = useRef({ x: -100, y: -100 }); + const prevJumpId = useRef(''); + + // SV data + const leftReads = useRef<{ [k: string]: number | string }[]>([]); + const rightReads = useRef<{ [k: string]: number | string }[]>([]); + const [svReads, setSvReads] = useState<{ name: string; type: string }[]>([]); + + function getFilteredDrivers(demoId: string) { + return (allDrivers as any).filter((d: any) => d.sample_id === demoId && +d.pos); + } + + // update demo + useEffect(() => { + if (typeof demo.drivers === 'string' && demo.drivers.split('.').pop() === 'json') { + // we want to change this json file to json value + fetch(demo.drivers).then(response => + response.text().then(d => { + const customDrivers = JSON.parse(d); + // TODO: these need to be supported in other types of data + customDrivers.forEach(d => { + const optionalFields = [ + 'ref', + 'alt', + 'category', + 'top_category', + 'transcript_consequence', + 'protein-mutation', + 'allele_fraction', + 'mutation_type', + 'biallelic' + ]; + optionalFields.forEach(f => { + if (!d[f]) { + d[f] = ''; + } + }); + if (typeof d['biallelic'] === 'string' && d['biallelic'].toUpperCase() === 'YES') { + d['biallelic'] = 'yes'; + } + if (typeof d['biallelic'] === 'string' && d['biallelic'].toUpperCase() === 'NO') { + d['biallelic'] = 'no'; + } + }); + + setDrivers(customDrivers); + }) + ); + } else { + const filteredDrivers = getFilteredDrivers(demo.id); + setDrivers(filteredDrivers); + } + + setOverviewChr(''); + setGenomeViewChr(''); + setSelectedSvId(''); + leftReads.current = []; + rightReads.current = []; + }, [demo]); + + function isWebAddress(url) { + return url.startsWith('http://') || url.startsWith('https://'); + } + + useEffect(() => { + const fetchData = async url => { + let responseText; + let externalDemo; + if (isWebAddress(url)) { + responseText = await fetch(url).then(response => response.text()); + externalDemo = JSON.parse(responseText); + } else { + externalDemo = await UrlsafeCodec.decode(url); + } + processDemoData(externalDemo); + }; + + function processDemoData(demoData) { + if (Array.isArray(demoData) && demoData.length >= 0) { + setFilteredSamples(demoData); + demoData = demoData[demoIndex.current < demoData.length ? demoIndex.current : 0]; + } else { + setFilteredSamples([demoData]); + } + if (demoData) { + setDemo(demoData); + } + setShowSmallMultiples(true); + setReady(true); + } + + if (externalUrl) { + fetchData(externalUrl); + } + }, []); + + useEffect(() => { + prevJumpId.current = jumpButtonInfo?.id; + }, [jumpButtonInfo]); + + useEffect(() => { + setFilteredSamples( + filterSampleBy === '' ? selectedSamples : selectedSamples.filter(d => d.id.includes(filterSampleBy)) + ); + }, [filterSampleBy]); + + useEffect(() => { + if (!gosRef.current || !demo.bai || !demo.bam) return; + + gosRef.current.api.subscribe('rawData', (type: string, e: any) => { + if (e.id.includes('bam') && (leftReads.current.length === 0 || rightReads.current.length === 0)) { + const isThisPotentiallyJsonRuleData = typeof e.data[0]?.name === 'undefined'; + if (isThisPotentiallyJsonRuleData) { + return; + } + + /// DEBUG + // console.log(e.id, e.data); + /// + + // This means we just received a BAM data that is just rendered + if (e.id.includes('left') && leftReads.current.length === 0) { + leftReads.current = e.data; + } else if (e.id.includes('right') && rightReads.current.length === 0) { + rightReads.current = e.data; + } + + // !! This is to drop duplicated data records. + // Multiple tracks overlaid on alignment tracks makes duplicated data records. + leftReads.current = Array.from(new Set(leftReads.current.map(d => JSON.stringify(d)))).map(d => + JSON.parse(d) + ); + rightReads.current = Array.from(new Set(rightReads.current.map(d => JSON.stringify(d)))).map(d => + JSON.parse(d) + ); + + // Reads on both views prepared? + if (leftReads.current.length !== 0 && rightReads.current.length !== 0) { + const mates = leftReads.current + .filter(l => rightReads.current.filter(r => r.name === l.name && r.id !== l.id).length !== 0) + .map(d => d.name as string); + + const matesWithSv = mates.map(name => { + const matesOnLeft = leftReads.current.filter(d => d.name === name); + const matesOnRight = rightReads.current.filter(d => d.name === name); + + if (matesOnLeft.length !== 1 || matesOnRight.length !== 1) { + // We do not do anything for this case for now. + return { name, type: 'unknown' }; + } + + // console.log(matesOnLeft[0], matesOnRight[0]); + const ld = matesOnLeft[0].strand; + const rd = matesOnRight[0].strand; + + if (matesOnLeft[0].chrName !== matesOnRight[0].chrName) return { name, type: 'Translocation' }; + if (ld === '+' && rd === '-') return { name, type: 'Deletion' }; + if (ld === '-' && rd === '-') return { name, type: 'Inversion (TtT)' }; + if (ld === '+' && rd === '+') return { name, type: 'Inversion (HtH)' }; + if (ld === '-' && rd === '+') return { name, type: 'Duplication' }; + return { name, type: 'unknown' }; + }); + + if ( + matesWithSv + .map(d => d.name) + .sort() + .join() !== + svReads + .map(d => d.name) + .sort() + .join() + ) { + setSvReads(matesWithSv); + } + } + } + }); + + return () => { + gosRef.current.api.unsubscribe('rawData'); + }; + }, [gosRef, svReads, demo]); + + useEffect(() => { + if (!overviewChr) return; + + if (overviewChr.includes('chr')) { + gosRef.current?.api.zoomTo(`${demo.id}-top-ideogram`, overviewChr, 0, 0); + setTimeout(() => setGenomeViewChr(overviewChr), 0); + } else { + gosRef.current?.api.zoomToExtent(`${demo.id}-top-ideogram`, ZOOM_DURATION); + } + }, [overviewChr]); + + useEffect(() => { + if (!genomeViewChr) return; + + if (genomeViewChr.includes('chr')) { + gosRef.current?.api.zoomTo(`${demo.id}-mid-ideogram`, genomeViewChr, ZOOM_PADDING, ZOOM_DURATION); + } else { + gosRef.current?.api.zoomToExtent(`${demo.id}-mid-ideogram`, ZOOM_DURATION); + } + }, [genomeViewChr]); + + // change the width of the visualization panel + useEffect(() => { + window.addEventListener( + 'resize', + debounce(() => { + setVisPanelWidth(window.innerWidth - VIS_PADDING * 2); + }, 500) + ); + }, []); + + const getThumbnail = (d: SampleType) => { + return ( + d.thumbnail || + INTERNAL_SAVED_THUMBNAILS[d.id] || + EXTERNAL_THUMBNAILS[d.id] || + DATABSE_THUMBNAILS.find(db => db.id === d.id)?.dataUrl || + GENERATED_THUMBNAILS[d.id] + ); + }; + + const AvailabilityIcon = (isAvailable: boolean) => { + return ( + + Export Image + {(isAvailable ? ICONS.CHECKSQUARE : ICONS.SQUARE).path.map(p => ( + + ))} + + ); + }; + const smallOverviewWrapper = useMemo(() => { + // !! Uncomment the following lines to generated specs for making thumbnails. + // console.log( + // 'overviewSpec', + // filteredSamples.map(d => + // getOneOfSmallMultiplesSpec({ + // cnvUrl: d.cnv, + // svUrl: d.sv, + // width: 1200, + // title: d.cancer.charAt(0).toUpperCase() + d.cancer.slice(1), + // subtitle: d.id, // '' + d.id.slice(0, 20) + (d.id.length >= 20 ? '...' : ''), + // cnFields: d.cnFields ?? ['total_cn', 'major_cn', 'minor_cn'] + // }) + // ), + // filteredSamples.map(d => `node gosling-screenshot.js output/${d.id}.json img/${d.id}.jpeg`).join('\n') + // ); + // return []; + /* Load image if necessary */ + const noThumbnail = filteredSamples.filter(d => !getThumbnail(d))[0]; + if (noThumbnail && generateThumbnails) { + const { id } = noThumbnail; + const spec = getOneOfSmallMultiplesSpec({ + cnvUrl: noThumbnail.cnv, + svUrl: noThumbnail.sv, + width: 600, + title: noThumbnail.cancer.charAt(0).toUpperCase() + noThumbnail.cancer.slice(1), + subtitle: id, + cnFields: noThumbnail.cnFields ?? ['total_cn', 'major_cn', 'minor_cn'] + }); + const hidden = document.getElementById('hidden-gosling'); + embed(hidden, spec, { padding: 0, margin: 10 }).then(api => { + setTimeout(() => { + const { canvas } = api.getCanvas(); + const dataUrl = canvas.toDataURL('image/png'); + GENERATED_THUMBNAILS[noThumbnail.id] = dataUrl; + db.add(id, dataUrl); + setThumbnailForceGenerate(!thumbnailForceGenerate); + }, 10000); + }); + } + if (noThumbnail) { + setDoneGeneratingThumbnails(false); + } else { + setDoneGeneratingThumbnails(true); + } + return filteredSamples.map((d, i) => ( +
{ + demoIndex.current = i; + setShowSamples(false); + setTimeout(() => { + setDemo(d); + }, 300); + }} + className={demo === d ? 'selected-overview' : 'unselected-overview'} + > +
+ {d.cancer.charAt(0).toUpperCase() + d.cancer.slice(1).split(' ')[0]} +
+
+ {'' + d.id.slice(0, 20) + (d.id.length >= 20 ? '...' : '')} +
+
+ {getThumbnail(d) ? ( + + ) : ( + //
+ // = 20 ? '...' : ''), + // cnFields: d.cnFields ?? ['total_cn', 'major_cn', 'minor_cn'] + // })} + // /> + //
+ <> + + + {generateThumbnails ? 'Loading...' : 'Thumbnail Missing'} + + + )} + {d.assembly ?? 'hg38'} +
+
+
{AvailabilityIcon(true)}SV
+
+ {AvailabilityIcon(!!d.vcf && !!d.vcfIndex)}Point Mutation +
+
+ {AvailabilityIcon(!!d.vcf2 && !!d.vcf2Index)}Indel +
+
+ {AvailabilityIcon(!!d.bam && !!d.bai)}Read Alignment +
+ {d.note ?
{d.note}
: null} +
+
+ )); + // smallOverviewGoslingComponents.map(([component, spec], i) => ( + //
{ + // setShowSamples(false); + // setTimeout(() => { + // setDemoIdx(i); + // setSelectedSvId(''); + // }, 300); + // }} + // className={demoIdx === i ? 'selected-overview' : 'unselected-overview'} + // > + // {component} + //
+ // )); + }, [demo, filteredSamples, thumbnailForceGenerate, generateThumbnails]); + + const hidiveLabRef = ( + <> + {' ('} + + HiDIVE Lab + + {')'} + + ); + const parkLabRef = ( + <> + {' ('} + + Park Lab + + {')'} + + ); + const goslingComponent = useMemo(() => { + const loadingCustomJSONDrivers = typeof demo.drivers === 'string' && demo.drivers.split('.').pop() === 'json'; + const isStillLoadingDrivers = loadingCustomJSONDrivers && drivers.length == 0; + if (!ready || isStillLoadingDrivers) return null; + + const useCustomDrivers = loadingCustomJSONDrivers || !demo.drivers; + const spec = generateSpec({ + ...demo, + showOverview, + xDomain: xDomain as [number, number], + xOffset: 0, + showPutativeDriver, + width: visPanelWidth, + drivers: useCustomDrivers ? drivers : demo.drivers, + selectedSvId, + breakpoints: breakpoints, + crossChr: false, + bpIntervals, + svReads + }); + currentSpec.current = JSON.stringify(spec); + // console.log('spec', spec); + return ( +
+ +
+ ); + // !! Removed `demo` not to update twice since `drivers` are updated right after a demo update. + }, [ready, xDomain, visPanelWidth, drivers, showOverview, showPutativeDriver, selectedSvId, breakpoints, svReads]); + + useLayoutEffect(() => { + if (!gosRef.current) return; + + gosRef.current.api.subscribe('click', (_, e) => { + // console.log(e); + + let x = +e.data[0].start1; + let xe = +e.data[0].end1; + let x1 = +e.data[0].start2; + let x1e = +e.data[0].end2; + + // safetly swap + if (x > x1) { + x = +e.data[0].start2; + xe = +e.data[0].end2; + x1 = +e.data[0].start1; + x1e = +e.data[0].end1; + } + + let zoomStart = x; + let zoomEnd = x1e; + let padding = (zoomEnd - zoomStart) / 4.0; + if (e.data[0].svclass === 'Translocation') { + zoomStart = x; + zoomEnd = xe; + padding = 10000; + } + + gosRef.current.api.zoomTo( + `${demo.id}-mid-ideogram`, + `chr1:${zoomStart}-${zoomEnd}`, + padding, + ZOOM_DURATION + ); + + // we will show the bam files, so set the initial positions + setBreakpoints([+x - ZOOM_PADDING, +xe + ZOOM_PADDING, +x1 - ZOOM_PADDING, +x1e + ZOOM_PADDING]); + setBpIntervals([x, xe, x1, x1e]); + setSelectedSvId(e.data[0].sv_id + ''); + + // move to the bottom + setTimeout( + () => document.getElementById('gosling-panel')?.scrollTo({ top: 1000000, behavior: 'smooth' }), + 2000 + ); + + leftReads.current = []; + rightReads.current = []; + }); + + gosRef.current.api.subscribe('mouseOver', (_, e) => { + const sanitizedChr = (c: string | number) => { + return `${c}`.replace('chr', ''); + }; + const calDir = (c1: string | number, c2: string | number) => { + c1 = sanitizedChr(c1); + c2 = sanitizedChr(c2); + if (+c1 && +c1 <= 9) { + c1 = '0' + c1; + } + if (+c2 && +c2 <= 9) { + c2 = '0' + c2; + } + return c1 < c2 ? 'rightward' : 'leftward'; + }; + if (e.id.includes('-mid-sv') && e.data[0].svclass === 'Translocation') { + const { chromosome: c, position: p } = e.genomicPosition; + const padding = 100000; + if (sanitizedChr(c) === sanitizedChr(e.data[0].chrom1)) { + const direction = calDir(c, e.data[0].chrom2); + const id = e.data[0].sv_id + '-' + direction; + if (id === prevJumpId.current) return; + const { start2, end2 } = e.data[0]; + setJumpButtonInfo({ + id, + x: mousePos.current.x, + y: mousePos.current.y, + direction, + zoomTo: () => gosRef.current.api.zoomTo(e.id, `chr1:${start2}-${end2}`, padding, ZOOM_DURATION) + }); + } else { + const direction = calDir(c, e.data[0].chrom1); + const id = e.data[0].sv_id + '-' + direction; + if (id === prevJumpId.current) return; + const { start1, end1 } = e.data[0]; + setJumpButtonInfo({ + id, + x: mousePos.current.x, + y: mousePos.current.y, + direction, + zoomTo: () => gosRef.current.api.zoomTo(e.id, `chr1:${start1}-${end1}`, padding, ZOOM_DURATION) + }); + } + } else { + setJumpButtonInfo(undefined); + } + }); + + return () => { + gosRef.current?.api.unsubscribe('click'); + gosRef.current?.api.unsubscribe('mouseOver'); + }; + }); + + return ( + +
{ + const top = e.clientY; + const left = e.clientX; + const width = window.innerWidth; + const height = window.innerHeight; + if ( + VIS_PADDING < top && + top < height - VIS_PADDING && + VIS_PADDING < left && + left < width - VIS_PADDING + ) { + setMouseOnVis(true); + } else { + setMouseOnVis(false); + } + mousePos.current = { x: left, y: top }; + }} + onWheel={() => setJumpButtonInfo(undefined)} + onClick={() => { + if (!mouseOnVis && interactiveMode) setInteractiveMode(false); + else if (mouseOnVis && !interactiveMode) setInteractiveMode(true); + setJumpButtonInfo(undefined); + }} + > + + { + setShowSamples(true); + }} + > + Menu + + +
+ + CHROMOSCOPE + + { + setShowAbout(true); + }} + > + + + + About + + {' | '} + {/* {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1) + ' • ' + demo.id} */} + {demo.cancer.charAt(0).toUpperCase() + demo.cancer.slice(1)} + {demo.id} + gosRef.current?.api.exportPng()}> + + Export Image + {ICONS.PNG.path.map(p => ( + + ))} + + + { + const a = document.createElement('a'); + a.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent( + getHtmlTemplate(currentSpec.current) + )}` + ); + a.download = 'visualization.html'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }} + style={{ marginLeft: 40 }} + > + + Export HTML + {ICONS.HTML.path.map(p => ( + + ))} + + + { + const a = document.createElement('a'); + a.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent(currentSpec.current)}` + ); + a.download = 'visualization.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }} + style={{ marginLeft: 70 }} + > + + Export Gosling Spec (JSON) + {ICONS.JSON.path.map(p => ( + + ))} + + + { + const { xDomain } = gosRef.current.hgApi.api.getLocation(`${demo.id}-mid-ideogram`); + if (xDomain) { + // urlParams.set('demoIndex', demoIndex.current + ''); + // urlParams.set('domain', xDomain.join('-')); + let newUrl = window.location.origin + window.location.pathname + '?'; + newUrl += `demoIndex=${demoIndex.current}`; + newUrl += `&domain=${xDomain.join('-')}`; + if (externalDemoUrl.current) { + newUrl += `&external=${externalDemoUrl.current}`; + } else if (externalUrl) { + newUrl += `&external=${externalUrl}`; + } + navigator.clipboard + .writeText(newUrl) + .then(() => + alert('The URL of the current session has been copied to your clipboard.') + ); + } + }} + style={{ marginLeft: 100 }} + > + + Export Link + + + + + {!isChrome() ? ( + + ⚠️ Chromoscope is optimized for Google Chrome + + ) : null} + + + GitHub + + + GitHub + + + + + + Documentation + +
+
+
+
{ + if (e.target === e.currentTarget) setShowSamples(false); + }} + > + { + setShowSamples(false); + }} + > + Close + + +
+ CHROMOSCOPE + { + setShowAbout(true); + }} + > + + + + About + + {' | '} Samples + setFilterSampleBy(e.target.value)} + hidden + /> +
+ + + GitHub + + + GitHub + + + + + + Documentation + + +
+
+
+ { + fetch(url).then(response => + response.text().then(d => { + let externalDemo = JSON.parse(d); + if (Array.isArray(externalDemo) && externalDemo.length >= 0) { + setFilteredSamples(externalDemo); + externalDemo = + externalDemo[ + demoIndex.current < externalDemo.length + ? demoIndex.current + : 0 + ]; + } + if (externalDemo) { + externalDemoUrl.current = url; + setDemo(externalDemo); + } + }) + ); + }} + /> + + { + setFilteredSamples([ + { + ...config, + group: 'default' + }, + ...filteredSamples + ]); + }} + /> +
+
{`Total of ${filteredSamples.length} samples loaded`}
+
{smallOverviewWrapper}
+
+
+
+ {goslingComponent} + {jumpButtonInfo ? ( + + ) : null} +
+
+ + + + + + + { + // const keyword = e.target.value; + // if(keyword !== "" && !keyword.startsWith("c")) { + // gosRef.current.api.suggestGene(keyword, (suggestions) => { + // setGeneSuggestions(suggestions); + // }); + // setSuggestionPosition({ + // left: searchBoxRef.current.getBoundingClientRect().left, + // top: searchBoxRef.current.getBoundingClientRect().top + searchBoxRef.current.getBoundingClientRect().height, + // }); + // } else { + // setGeneSuggestions([]); + // } + // setSearchKeyword(keyword); + // }} + onKeyDown={e => { + const keyword = (e.target as HTMLTextAreaElement).value; + switch (e.key) { + case 'ArrowUp': + break; + case 'ArrowDown': + break; + case 'Enter': + // https://github.com/gosling-lang/gosling.js/blob/7555ab711023a0c3e2076a448756a9ba3eeb04f7/src/core/api.ts#L156 + gosRef.current.hgApi.api.zoomToGene( + `${demo.id}-mid-ideogram`, + keyword, + 10000, + 1000 + ); + break; + case 'Esc': + case 'Escape': + break; + } + }} + /> + + + + +
+
+
+
+ {!interactiveMode + ? 'Click inside to use interactions on visualizations' + : 'Click outside to deactivate interactions and scroll the page'} +
+
+
+ {'ⓘ No read alignment data available for this sample.'} +
+
setShowAbout(false)} + /> +
+ +

+ Chromoscope + {' | '}About +

+ +

+ Whole genome sequencing is now routinely used to profile mutations in DNA in the soma and in the + germline, informing molecular diagnoses of disease and therapeutic decisions. Structural + variants (SVs) are the main new type of alterations we see more of, and they are often + diagnostic, prognostic, or therapy-informing. However, the size and complexity of SV data, + combined with the difficulty of obtaining accurate SV calls, pose challenges in the + interpretation of SVs, requiring tedious visual inspection of potentially pathogenic variants + with multiple visualization tools. +

+ +

+ To overcome the problems with the interpretation of SVs, we developed Chromoscope, an + open-source web-based application for the interactive visualization of structural variants. + Chromoscope has several innovative features which unlock the insights from whole genome + sequencing: visualization at multiple scale levels simultaneously, effective navigation across + scales, easy setup for loading users' large datasets, and a feature to export, share, and + further customize visualizations. We anticipate that Chromoscope will accelerate the exploration + and interpretation of SVs by a broad range of scientists and clinicians, leading to new insights + into genomic biomarkers. +

+

Learn more about Chromoscope

+ +

The Team

+
    +
  • + Sehi L'Yi + {hidiveLabRef} +
  • +
  • + Dominika Maziec + {parkLabRef} +
  • +
  • + Victoria Stevens + {parkLabRef} +
  • +
  • + Trevor Manz + {hidiveLabRef} +
  • +
  • + Alexander Veit + {parkLabRef} +
  • +
  • + Michele Berselli + {parkLabRef} +
  • +
  • + Peter J Park + {parkLabRef} +
  • +
  • + Dominik Glodzik + {parkLabRef} +
  • +
  • + Nils Gehlenborg + {hidiveLabRef} +
  • +
+ +
+
{ + setTimeout( + () => document.getElementById('gosling-panel')?.scrollTo({ top: 0, behavior: 'smooth' }), + 0 + ); + }} + > + + Scroll To Top + + +
+
+
+ + ); +} + +export default App; From 915bf0d3c9ff6688aa32b5cd71bf27613cc7ab2a Mon Sep 17 00:00:00 2001 From: tsertijn Date: Fri, 5 Jul 2024 11:12:17 +0200 Subject: [PATCH 20/57] Add files via upload --- src/ui/json-base64-converter.tsx | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/ui/json-base64-converter.tsx diff --git a/src/ui/json-base64-converter.tsx b/src/ui/json-base64-converter.tsx new file mode 100644 index 00000000..3d3923bc --- /dev/null +++ b/src/ui/json-base64-converter.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import UrlsafeCodec from '../lib/urlsafe-codec'; + +const JsonBase64Converter = () => { + const [jsonText, setJsonText] = useState(''); + const [base64Text, setBase64Text] = useState(''); + const [error, setError] = useState(''); + + const handleEncode = () => { + try { + const jsonObject = JSON.parse(jsonText); + const encoded = UrlsafeCodec.encode(jsonObject); + setBase64Text(encoded); + setError(''); + } catch (error) { + setError('Error parsing JSON. Please enter valid JSON.'); + } + }; + + const handleDecode = () => { + try { + const decoded = UrlsafeCodec.decode(base64Text); + const jsonString = JSON.stringify(decoded, null, 4); + setJsonText(jsonString); + setError(''); + } catch (error) { + setError('Error decoding base64 string. Please enter a valid base64-encoded string.'); + } + }; + + return ( +
+
+

JSON Text

+