From 48669a15594bd059d35ffebef9cac3371c86dd70 Mon Sep 17 00:00:00 2001 From: Pim van Nierop Date: Mon, 9 Jan 2023 15:23:07 +0100 Subject: [PATCH] Impl. structural variants table in study view --- .../specs/init-columns-in-cna-tables.spec.js | 2 +- .../init-columns-in-struct-var-tables.spec.js | 2 +- .../namespace-columns-in-cna-tables.spec.js | 2 +- ...space-columns-in-struct-var-tables.spec.js | 2 +- ...ils.js => namespace-columns-utils.spec.js} | 0 .../local/specs/struct-var-table.spec.js | 154 ++++ package.json | 10 +- .../src/generated/CBioPortalAPI-docs.json | 87 ++ .../src/generated/CBioPortalAPI.ts | 39 + .../generated/CBioPortalAPIInternal-docs.json | 67 ++ .../src/generated/CBioPortalAPIInternal.ts | 31 + src/pages/studyView/StructVarUtils.spec.tsx | 163 ++++ src/pages/studyView/StructVarUtils.ts | 238 ++++++ src/pages/studyView/StudyViewConfig.ts | 8 + src/pages/studyView/StudyViewPageStore.ts | 406 ++++++++- src/pages/studyView/StudyViewUtils.spec.tsx | 266 +++++- src/pages/studyView/StudyViewUtils.tsx | 120 ++- src/pages/studyView/TableUtils.tsx | 1 + src/pages/studyView/UserSelections.tsx | 143 +++- src/pages/studyView/charts/ChartContainer.tsx | 106 ++- .../studyPageHeader/StudyPageHeader.tsx | 3 + .../studyView/table/FixedHeaderTable.tsx | 1 + .../studyView/table/MultiSelectionTable.tsx | 19 +- src/pages/studyView/table/StructVarCell.tsx | 131 +++ .../StructuralVariantMultiSelectionTable.tsx | 792 ++++++++++++++++++ src/pages/studyView/table/tables.module.scss | 10 +- .../studyView/table/tables.module.scss.d.ts | 1 + src/pages/studyView/tabs/SummaryTab.tsx | 24 + .../GeneSelectionBox/OQLTextArea.tsx | 36 +- .../labeledCheckbox/LabeledCheckbox.tsx | 1 + src/shared/featureFlags.ts | 1 + src/shared/lib/oql/oql-parser.d.ts | 2 +- src/shared/lib/oql/oql-parser.js | 12 +- src/shared/lib/oql/oql-parser.pegjs | 12 +- src/shared/lib/oql/oql-parser.spec.ts | 120 +-- src/shared/lib/oql/oqlfilter.spec.ts | 185 ++++ src/shared/lib/oql/oqlfilter.ts | 96 ++- src/test/check_api_sync.sh | 10 +- 38 files changed, 3052 insertions(+), 251 deletions(-) rename end-to-end-test/local/specs/{namespace-columns-utils.js => namespace-columns-utils.spec.js} (100%) create mode 100644 end-to-end-test/local/specs/struct-var-table.spec.js create mode 100644 src/pages/studyView/StructVarUtils.spec.tsx create mode 100644 src/pages/studyView/StructVarUtils.ts create mode 100644 src/pages/studyView/table/StructVarCell.tsx create mode 100644 src/pages/studyView/table/StructuralVariantMultiSelectionTable.tsx diff --git a/end-to-end-test/local/specs/init-columns-in-cna-tables.spec.js b/end-to-end-test/local/specs/init-columns-in-cna-tables.spec.js index 053ae72c95a..25a69d78d5f 100644 --- a/end-to-end-test/local/specs/init-columns-in-cna-tables.spec.js +++ b/end-to-end-test/local/specs/init-columns-in-cna-tables.spec.js @@ -3,7 +3,7 @@ const { goToUrlAndSetLocalStorageWithProperty, getElementByTestHandle, } = require('../../shared/specUtils'); -const { waitForTable } = require('./namespace-columns-utils'); +const { waitForTable } = require('./namespace-columns-utils.spec'); const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ''); diff --git a/end-to-end-test/local/specs/init-columns-in-struct-var-tables.spec.js b/end-to-end-test/local/specs/init-columns-in-struct-var-tables.spec.js index 85993aabae6..4bdd3e4a5b5 100644 --- a/end-to-end-test/local/specs/init-columns-in-struct-var-tables.spec.js +++ b/end-to-end-test/local/specs/init-columns-in-struct-var-tables.spec.js @@ -3,7 +3,7 @@ const { goToUrlAndSetLocalStorageWithProperty, getElementByTestHandle, } = require('../../shared/specUtils'); -const { waitForTable } = require('./namespace-columns-utils'); +const { waitForTable } = require('./namespace-columns-utils.spec'); const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ''); diff --git a/end-to-end-test/local/specs/namespace-columns-in-cna-tables.spec.js b/end-to-end-test/local/specs/namespace-columns-in-cna-tables.spec.js index 247bb3c57b1..a4ee37029d2 100644 --- a/end-to-end-test/local/specs/namespace-columns-in-cna-tables.spec.js +++ b/end-to-end-test/local/specs/namespace-columns-in-cna-tables.spec.js @@ -9,7 +9,7 @@ const { selectColumn, namespaceColumnsAreDisplayed, getRowByGene, -} = require('./namespace-columns-utils'); +} = require('./namespace-columns-utils.spec'); const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ''); diff --git a/end-to-end-test/local/specs/namespace-columns-in-struct-var-tables.spec.js b/end-to-end-test/local/specs/namespace-columns-in-struct-var-tables.spec.js index f4599925d14..2f119d526ff 100644 --- a/end-to-end-test/local/specs/namespace-columns-in-struct-var-tables.spec.js +++ b/end-to-end-test/local/specs/namespace-columns-in-struct-var-tables.spec.js @@ -9,7 +9,7 @@ const { selectColumn, namespaceColumnsAreDisplayed, getRowByGene, -} = require('./namespace-columns-utils'); +} = require('./namespace-columns-utils.spec'); const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ''); diff --git a/end-to-end-test/local/specs/namespace-columns-utils.js b/end-to-end-test/local/specs/namespace-columns-utils.spec.js similarity index 100% rename from end-to-end-test/local/specs/namespace-columns-utils.js rename to end-to-end-test/local/specs/namespace-columns-utils.spec.js diff --git a/end-to-end-test/local/specs/struct-var-table.spec.js b/end-to-end-test/local/specs/struct-var-table.spec.js new file mode 100644 index 00000000000..aba2de78392 --- /dev/null +++ b/end-to-end-test/local/specs/struct-var-table.spec.js @@ -0,0 +1,154 @@ +var assert = require('assert'); +var goToUrlAndSetLocalStorage = require('../../shared/specUtils') + .goToUrlAndSetLocalStorage; +var waitForStudyView = require('../../shared/specUtils').waitForStudyView; + +const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ''); +// TODO remove feature flag after merge. +const studyViewUrl = `${CBIOPORTAL_URL}/study/summary?id=study_es_0&featureFlags=STUDY_VIEW_STRUCT_VAR_TABLE`; +const structVarTable = '//*[@data-test="structural variant pairs-table"]'; +const filterCheckBox = '[data-test=labeledCheckbox]'; +const structVarFilterPillTag = '[data-test=pill-tag]'; +const uncheckedSvIcon = '[data-test=structVarQueryCheckboxUnchecked]'; +const checkedSvIcon = '[data-test=structVarQueryCheckboxChecked]'; +const structVarNameCell = '[data-test=structVarNameCell]'; +const toast = '.Toastify div[role=alert]'; + +describe('study view structural variant table', function() { + beforeEach(() => { + goToUrlAndSetLocalStorage(studyViewUrl, true); + waitForStudyView(); + }); + + it('adds structural variant to study view filter', () => { + $(structVarTable) + .$(filterCheckBox) + .click(); + $('[data-test=selectSamplesButton]').waitForExist(); + $('[data-test=selectSamplesButton]').click(); + assert($(structVarFilterPillTag).isExisting()); + }); + + it('shows all checkboxes when row is hovered', () => { + $(structVarNameCell).waitForExist(); + const firstSvRowCell = $$(structVarNameCell)[0]; + assert.equal($$(structVarNameCell)[1].getText(), 'SND1'); + assert.equal($$(structVarNameCell)[2].getText(), 'BRAF'); + + movePointerWithRetry(firstSvRowCell, () => + $(uncheckedSvIcon).waitForDisplayed() + ); + assert.equal($$(uncheckedSvIcon).length, 3); + }); + + it('shows only checked checkboxes when row is not hovered', () => { + $(structVarNameCell).waitForExist(); + const gene1Cell = $$(structVarNameCell)[1]; + movePointerWithRetry(gene1Cell, () => + $(uncheckedSvIcon).waitForDisplayed() + ); + assert.equal($$(uncheckedSvIcon).length, 3); + + gene1Cell.waitForClickable(); + gene1Cell.click(); + + // hover somewhere else: + movePointerWithRetry($('span=Gene 2'), $(checkedSvIcon).waitForExist()); + + assert.equal($$(uncheckedSvIcon).length, 0); + assert.equal($$(checkedSvIcon).length, 1); + }); + + it('adds gene1::gene2 to Results View query', () => { + $(structVarNameCell).waitForExist(); + const firstSvRowCell = $$(structVarNameCell)[0]; + + movePointerWithRetry(firstSvRowCell, () => { + $$(uncheckedSvIcon)[0].waitForClickable(); + }); + const gene1And2Checkbox = $$(uncheckedSvIcon)[0]; + gene1And2Checkbox.click(); + $(toast).waitForDisplayed(); + clearToast(); + + const resultsViewQueryBox = openResultViewQueryBox(); + assert.equal('SND1: FUSION::BRAF ;', resultsViewQueryBox.getValue()); + }); + + it('adds gene1::* to Results View query', () => { + $(structVarNameCell).waitForExist(); + const gene1Cell = $$(structVarNameCell)[1]; + movePointerWithRetry(gene1Cell, () => + $(uncheckedSvIcon).waitForDisplayed() + ); + gene1Cell.waitForClickable(); + gene1Cell.click(); + $(toast).waitForDisplayed(); + clearToast(); + + const resultsViewQueryBox = openResultViewQueryBox(); + assert.equal('SND1: FUSION:: ;', resultsViewQueryBox.getValue()); + }); + + it('adds *::gene2 to Results View query', () => { + $(structVarNameCell).waitForExist(); + const gene2Cell = $$(structVarNameCell)[2]; + movePointerWithRetry(gene2Cell, () => + $(uncheckedSvIcon).waitForDisplayed() + ); + gene2Cell.waitForClickable(); + gene2Cell.click(); + $(toast).waitForDisplayed(); + clearToast(); + + const resultsViewQueryBox = openResultViewQueryBox(); + assert.equal('BRAF: ::FUSION ;', resultsViewQueryBox.getValue()); + }); +}); + +function openResultViewQueryBox() { + const resultsViewQueryBox = $('[data-test=geneSet]'); + resultsViewQueryBox.waitForClickable(); + resultsViewQueryBox.click(); + return resultsViewQueryBox; +} + +function clearToast() { + const toastify = $('.Toastify button'); + toastify.waitForClickable(); + toastify.click(); + browser.pause(100); +} + +function movePointerTo(element) { + element.waitForDisplayed(); + element.scrollIntoView(); + const x = element.getLocation('x'); + const y = element.getLocation('y'); + browser.performActions([ + { + type: 'pointer', + parameters: { pointerType: 'mouse' }, + actions: [ + { type: 'pointerMove', duration: 0, x, y }, + { type: 'pointerMove', duration: 0, x, y }, + ], + }, + ]); +} + +/** + * When scrolling to the new location, some tooltips might pop up and interfere. + * A retry solves this problem: the second time the pointer is already near/at the desired location + */ +function movePointerWithRetry(element, isOk) { + movePointerTo(element); + try { + if (isOk()) { + return; + } + } catch (e) { + // retry + } + movePointerTo(element); +} diff --git a/package.json b/package.json index bc096ae83a2..5fdf720bff2 100644 --- a/package.json +++ b/package.json @@ -30,17 +30,17 @@ "buildBootstrap": "sass --style :compressed src/globalStyles/bootstrap-entry.scss src/globalStyles/prefixed-bootstrap.min.css", "heroku-postbuild": "yarn run build && yarn add pushstate-server@3.0.1 -g", "updateAPI": "yarn run fetchAPI && yarn run buildAPI && yarn run updateOncoKbAPI && yarn run updateGenomeNexusAPI", - "fetchAPILocal": "export CBIOPORTAL_URL=http://localhost:8090 && curl -L -k ${CBIOPORTAL_URL}/api/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json && curl -L -k ${CBIOPORTAL_URL}/api/api-docs?group=internal | json | grep -v host | grep -v basePath | grep -v termsOfService > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json", - "fetchAPI": "./scripts/env_vars.sh && eval \"$(./scripts/env_vars.sh)\" && curl -L -k ${CBIOPORTAL_URL}/api/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json && curl -L -k ${CBIOPORTAL_URL}/api/api-docs?group=internal | json | grep -v host | grep -v basePath | grep -v termsOfService > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json", + "fetchAPILocal": "export CBIOPORTAL_URL=http://localhost:8090 && curl -s -L -k ${CBIOPORTAL_URL}/api/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json && curl -s -L -k ${CBIOPORTAL_URL}/api/api-docs?group=internal | json | grep -v host | grep -v basePath | grep -v termsOfService > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json", + "fetchAPI": "./scripts/env_vars.sh && eval \"$(./scripts/env_vars.sh)\" && curl -s -L -k ${CBIOPORTAL_URL}/api/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json && curl -s -L -k ${CBIOPORTAL_URL}/api/api-docs?group=internal | json | grep -v host | grep -v basePath | grep -v termsOfService > packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json", "buildAPI": "node scripts/generate-api.js packages/cbioportal-ts-api-client/src/generated CBioPortalAPI CBioPortalAPIInternal", "updateOncoKbAPI": "yarn run fetchOncoKbAPI && yarn run buildOncoKbAPI", - "fetchOncoKbAPI": "curl -k https://www.oncokb.org/api/v1/v2/api-docs?group=Public%20APIs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/oncokb-ts-api-client/src/generated/OncoKbAPI-docs.json", + "fetchOncoKbAPI": "curl -s -k https://www.oncokb.org/api/v1/v2/api-docs?group=Public%20APIs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/oncokb-ts-api-client/src/generated/OncoKbAPI-docs.json", "buildOncoKbAPI": "node scripts/generate-api.js packages/oncokb-ts-api-client/src/generated OncoKbAPI", "updateG2SAPI": "yarn run fetchG2SAPI && yarn run buildG2SAPI", - "fetchG2SAPI": "curl -k http://g2s.genomenexus.org/v2/api-docs?group=api > packages/genome-nexus-ts-api-client/src/generated/Genome2StructureAPI-docs.json", + "fetchG2SAPI": "curl -s -k http://g2s.genomenexus.org/v2/api-docs?group=api > packages/genome-nexus-ts-api-client/src/generated/Genome2StructureAPI-docs.json", "buildG2SAPI": "node scripts/generate-api.js packages/genome-nexus-ts-api-client/src/generated Genome2StructureAPI", "updateGenomeNexusAPI": "yarn run fetchGenomeNexusAPI && yarn run buildGenomeNexusAPI", - "fetchGenomeNexusAPI": "./scripts/env_vars.sh && eval \"$(./scripts/env_vars.sh)\" && curl -k ${GENOME_NEXUS_URL}/v2/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/genome-nexus-ts-api-client/src/generated/GenomeNexusAPI-docs.json && curl -k ${GENOME_NEXUS_URL}/v2/api-docs?group=internal | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/genome-nexus-ts-api-client/src/generated/GenomeNexusAPIInternal-docs.json", + "fetchGenomeNexusAPI": "./scripts/env_vars.sh && eval \"$(./scripts/env_vars.sh)\" && curl -s -k ${GENOME_NEXUS_URL}/v2/api-docs | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/genome-nexus-ts-api-client/src/generated/GenomeNexusAPI-docs.json && curl -s -k ${GENOME_NEXUS_URL}/v2/api-docs?group=internal | json | grep -v basePath | grep -v termsOfService | grep -v host > packages/genome-nexus-ts-api-client/src/generated/GenomeNexusAPIInternal-docs.json", "buildGenomeNexusAPI": "node scripts/generate-api.js packages/genome-nexus-ts-api-client/src/generated GenomeNexusAPI GenomeNexusAPIInternal", "updateHotspotGenes": "./scripts/get_hotspot_genes.sh > src/shared/static-data/hotspotGenes.json", "compileOqlParser": "cd src/shared/lib/oql && pegjs oql-parser.pegjs", diff --git a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json index 08abf075935..efc300425d8 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json @@ -5874,6 +5874,65 @@ }, "title": "ServerStatusMessage" }, + "StructuralVariantFilterQuery": { + "type": "object", + "properties": { + "gene1Query": { + "$ref": "#/definitions/StructuralVariantGeneSubQuery" + }, + "gene2Query": { + "$ref": "#/definitions/StructuralVariantGeneSubQuery" + }, + "includeDriver": { + "type": "boolean" + }, + "includeGermline": { + "type": "boolean" + }, + "includeSomatic": { + "type": "boolean" + }, + "includeUnknownOncogenicity": { + "type": "boolean" + }, + "includeUnknownStatus": { + "type": "boolean" + }, + "includeUnknownTier": { + "type": "boolean" + }, + "includeVUS": { + "type": "boolean" + }, + "tiersBooleanMap": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + }, + "title": "StructuralVariantFilterQuery" + }, + "StructuralVariantGeneSubQuery": { + "type": "object", + "properties": { + "entrezId": { + "type": "integer", + "format": "int32" + }, + "hugoSymbol": { + "type": "string" + }, + "specialValue": { + "type": "string", + "enum": [ + "ANY_GENE", + "NO_GENE" + ] + } + }, + "title": "StructuralVariantGeneSubQuery" + }, "StudyViewFilter": { "type": "object", "properties": { @@ -5958,6 +6017,12 @@ "sampleTreatmentTargetFilters": { "$ref": "#/definitions/AndedSampleTreatmentFilters" }, + "structuralVariantFilters": { + "type": "array", + "items": { + "$ref": "#/definitions/StudyViewStructuralVariantFilter" + } + }, "studyIds": { "type": "array", "items": { @@ -5967,6 +6032,28 @@ }, "title": "StudyViewFilter" }, + "StudyViewStructuralVariantFilter": { + "type": "object", + "properties": { + "molecularProfileIds": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "structVarQueries": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/StructuralVariantFilterQuery" + } + } + } + }, + "title": "StudyViewStructuralVariantFilter" + }, "TypeOfCancer": { "type": "object", "required": [ diff --git a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI.ts b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI.ts index ea5161ad117..5d68511bab6 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI.ts +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI.ts @@ -703,6 +703,36 @@ export type SampleTreatmentRow = { export type ServerStatusMessage = { 'status': string +}; +export type StructuralVariantFilterQuery = { + 'gene1Query': StructuralVariantGeneSubQuery + + 'gene2Query': StructuralVariantGeneSubQuery + + 'includeDriver': boolean + + 'includeGermline': boolean + + 'includeSomatic': boolean + + 'includeUnknownOncogenicity': boolean + + 'includeUnknownStatus': boolean + + 'includeUnknownTier': boolean + + 'includeVUS': boolean + + 'tiersBooleanMap': {} + +}; +export type StructuralVariantGeneSubQuery = { + 'entrezId': number + + 'hugoSymbol': string + + 'specialValue': "ANY_GENE" | "NO_GENE" + }; export type StudyViewFilter = { 'alterationFilter': AlterationFilter @@ -739,8 +769,17 @@ export type StudyViewFilter = { 'sampleTreatmentTargetFilters': AndedSampleTreatmentFilters + 'structuralVariantFilters': Array < StudyViewStructuralVariantFilter > + 'studyIds': Array < string > +}; +export type StudyViewStructuralVariantFilter = { + 'molecularProfileIds': Array < string > + + 'structVarQueries': Array < Array < StructuralVariantFilterQuery > + > + }; export type TypeOfCancer = { 'cancerTypeId': string diff --git a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json index c8b26a83a20..e6f1ebcca18 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json @@ -5690,6 +5690,45 @@ }, "title": "StructuralVariantFilter" }, + "StructuralVariantFilterQuery": { + "type": "object", + "properties": { + "gene1Query": { + "$ref": "#/definitions/StructuralVariantGeneSubQuery" + }, + "gene2Query": { + "$ref": "#/definitions/StructuralVariantGeneSubQuery" + }, + "includeDriver": { + "type": "boolean" + }, + "includeGermline": { + "type": "boolean" + }, + "includeSomatic": { + "type": "boolean" + }, + "includeUnknownOncogenicity": { + "type": "boolean" + }, + "includeUnknownStatus": { + "type": "boolean" + }, + "includeUnknownTier": { + "type": "boolean" + }, + "includeVUS": { + "type": "boolean" + }, + "tiersBooleanMap": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + }, + "title": "StructuralVariantFilterQuery" + }, "StructuralVariantGeneSubQuery": { "type": "object", "properties": { @@ -5806,6 +5845,12 @@ "sampleTreatmentTargetFilters": { "$ref": "#/definitions/AndedSampleTreatmentFilters" }, + "structuralVariantFilters": { + "type": "array", + "items": { + "$ref": "#/definitions/StudyViewStructuralVariantFilter" + } + }, "studyIds": { "type": "array", "items": { @@ -5815,6 +5860,28 @@ }, "title": "StudyViewFilter" }, + "StudyViewStructuralVariantFilter": { + "type": "object", + "properties": { + "molecularProfileIds": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "structVarQueries": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/StructuralVariantFilterQuery" + } + } + } + }, + "title": "StudyViewStructuralVariantFilter" + }, "VariantCount": { "type": "object", "required": [ diff --git a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts index 1f5ed25ff62..6e1ad94dd2e 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts @@ -1025,6 +1025,28 @@ export type StructuralVariantFilter = { 'structuralVariantQueries': Array < StructuralVariantQuery > +}; +export type StructuralVariantFilterQuery = { + 'gene1Query': StructuralVariantGeneSubQuery + + 'gene2Query': StructuralVariantGeneSubQuery + + 'includeDriver': boolean + + 'includeGermline': boolean + + 'includeSomatic': boolean + + 'includeUnknownOncogenicity': boolean + + 'includeUnknownStatus': boolean + + 'includeUnknownTier': boolean + + 'includeVUS': boolean + + 'tiersBooleanMap': {} + }; export type StructuralVariantGeneSubQuery = { 'entrezId': number @@ -1075,8 +1097,17 @@ export type StudyViewFilter = { 'sampleTreatmentTargetFilters': AndedSampleTreatmentFilters + 'structuralVariantFilters': Array < StudyViewStructuralVariantFilter > + 'studyIds': Array < string > +}; +export type StudyViewStructuralVariantFilter = { + 'molecularProfileIds': Array < string > + + 'structVarQueries': Array < Array < StructuralVariantFilterQuery > + > + }; export type VariantCount = { 'entrezGeneId': number diff --git a/src/pages/studyView/StructVarUtils.spec.tsx b/src/pages/studyView/StructVarUtils.spec.tsx new file mode 100644 index 00000000000..771330e714b --- /dev/null +++ b/src/pages/studyView/StructVarUtils.spec.tsx @@ -0,0 +1,163 @@ +import { assert } from 'chai'; +import * as React from 'react'; +import { + STRUCTVARAnyGeneStr, + STRUCTVARNullGeneStr, + STUCTVARDownstreamFusionStr, + STUCTVARUpstreamFusionStr, +} from 'shared/lib/oql/oqlfilter'; +import { SingleGeneQuery } from 'shared/lib/oql/oql-parser'; +import { + oqlQueryToStructVarGenePair, + StructVarGenePair, +} from 'pages/studyView/StructVarUtils'; + +describe('StructVarUtils', () => { + describe('oqlQueryToStructVarGenePair', () => { + it.each([ + [{ gene: 'A', alterations: [] }, []], + [ + { + gene: 'A', + alterations: [ + { + gene: undefined, + alteration_type: STUCTVARDownstreamFusionStr, + modifiers: [], + }, + ], + }, + [], + ], + [ + { + gene: 'A', + alterations: [ + { + gene: 'B', + alteration_type: STUCTVARDownstreamFusionStr, + modifiers: [], + }, + ], + }, + [{ gene1HugoSymbolOrOql: 'A', gene2HugoSymbolOrOql: 'B' }], + ], + [ + { + gene: 'A', + alterations: [ + { + gene: STRUCTVARAnyGeneStr, + alteration_type: STUCTVARDownstreamFusionStr, + modifiers: [], + }, + ], + }, + [ + { + gene1HugoSymbolOrOql: 'A', + gene2HugoSymbolOrOql: STRUCTVARAnyGeneStr, + }, + ], + ], + [ + { + gene: 'A', + alterations: [ + { + gene: STRUCTVARNullGeneStr, + alteration_type: STUCTVARDownstreamFusionStr, + modifiers: [], + }, + ], + }, + [ + { + gene1HugoSymbolOrOql: 'A', + gene2HugoSymbolOrOql: STRUCTVARNullGeneStr, + }, + ], + ], + [ + { + gene: 'A', + alterations: [ + { + gene: 'B', + alteration_type: STUCTVARUpstreamFusionStr, + modifiers: [], + }, + ], + }, + [{ gene1HugoSymbolOrOql: 'B', gene2HugoSymbolOrOql: 'A' }], + ], + [ + { + gene: 'A', + alterations: [ + { + gene: STRUCTVARAnyGeneStr, + alteration_type: STUCTVARUpstreamFusionStr, + modifiers: [], + }, + ], + }, + [ + { + gene1HugoSymbolOrOql: STRUCTVARAnyGeneStr, + gene2HugoSymbolOrOql: 'A', + }, + ], + ], + [ + { + gene: 'A', + alterations: [ + { + gene: STRUCTVARNullGeneStr, + alteration_type: STUCTVARUpstreamFusionStr, + modifiers: [], + }, + ], + }, + [ + { + gene1HugoSymbolOrOql: STRUCTVARNullGeneStr, + gene2HugoSymbolOrOql: 'A', + }, + ], + ], + [ + { + gene: 'A', + alterations: [ + { + gene: 'B', + alteration_type: STUCTVARDownstreamFusionStr, + modifiers: [], + }, + { + gene: 'C', + alteration_type: STUCTVARDownstreamFusionStr, + modifiers: [], + }, + ], + }, + [ + { gene1HugoSymbolOrOql: 'A', gene2HugoSymbolOrOql: 'B' }, + { gene1HugoSymbolOrOql: 'A', gene2HugoSymbolOrOql: 'C' }, + ], + ], + ])( + 'converts %p into %p', + (singleGeneQuery, expected: StructVarGenePair[]) => { + assert.deepEqual( + oqlQueryToStructVarGenePair( + singleGeneQuery as SingleGeneQuery + ), + expected + ); + } + ); + }); +}); diff --git a/src/pages/studyView/StructVarUtils.ts b/src/pages/studyView/StructVarUtils.ts new file mode 100644 index 00000000000..2506a76bce6 --- /dev/null +++ b/src/pages/studyView/StructVarUtils.ts @@ -0,0 +1,238 @@ +import { + FUSIONCommandDownstream, + FUSIONCommandUpstream, + SingleGeneQuery, +} from 'shared/lib/oql/oql-parser'; +import _ from 'lodash'; +import { + alterationIsStructVar, + queryContainsStructVarAlteration, + STRUCTVARAnyGeneStr, + STRUCTVARNullGeneStr, + STUCTVARDownstreamFusionStr, + STUCTVARUpstreamFusionStr, + unparseOQLQueryLine, +} from 'shared/lib/oql/oqlfilter'; +import { + StructuralVariantGeneSubQuery, + StructuralVariantFilterQuery, +} from 'cbioportal-ts-api-client'; + +export type StructVarGenePair = { + gene1HugoSymbolOrOql: string; + gene2HugoSymbolOrOql: string; +}; + +// This function acts as a toggle. If present in 'geneQueries', the query +// is removed. If absent, a gene1/gene2 struct var query is added. +export function updateStructuralVariantQuery( + geneQueries: SingleGeneQuery[], + structvarGene1: string, + structvarGene2: string +): SingleGeneQuery[] { + // At this point any gene must be: + // 1) HUGO gene symbol + // 2) '*' indicating any gene + // 3) '-' indicating no gene. + if (!structvarGene1 || !structvarGene2) { + return geneQueries; + } + + // Remove any SV alteration with the same genes (both upstream and downstream fusions are evaluated). + const updatedQueries = _.filter( + geneQueries, + query => + !doesStructVarMatchSingleGeneQuery( + query, + structvarGene1, + structvarGene2 + ) + ); + + const representativeGene = ![ + STRUCTVARNullGeneStr, + STRUCTVARAnyGeneStr, + ].includes(structvarGene1) + ? structvarGene1 + : structvarGene2; + const otherGene = + representativeGene === structvarGene1 ? structvarGene2 : structvarGene1; + const alterationType = + representativeGene === structvarGene1 + ? STUCTVARDownstreamFusionStr + : STUCTVARUpstreamFusionStr; + + if (updatedQueries.length === geneQueries.length) { + updatedQueries.push({ + gene: representativeGene!, + alterations: [ + { + alteration_type: alterationType, + gene: otherGene, + modifiers: [], + }, + ], + }); + } + return updatedQueries; +} + +export function doesStructVarMatchSingleGeneQuery( + query: SingleGeneQuery, + structvarGene1: string, + structvarGene2: string +) { + const isStructVar = queryContainsStructVarAlteration(query); + const isUpstreamMatch = + query.gene === structvarGene1 && + query.alterations && + _.some( + query.alterations, + (alt: FUSIONCommandUpstream | FUSIONCommandDownstream) => + (alt.alteration_type === STUCTVARDownstreamFusionStr && + alt.gene) || + STRUCTVARNullGeneStr === structvarGene2 + ); + const isDownstreamMatch = + query.gene === structvarGene2 && + query.alterations && + _.some( + query.alterations, + (alt: FUSIONCommandUpstream | FUSIONCommandDownstream) => + (alt.alteration_type === STUCTVARUpstreamFusionStr && + alt.gene) || + STRUCTVARNullGeneStr === structvarGene1 + ); + return isStructVar && (isDownstreamMatch || isUpstreamMatch); +} + +export function structVarFilterQueryToOql( + query: StructuralVariantFilterQuery +): string { + const gene1 = query.gene1Query.hugoSymbol || ''; + const gene2 = query.gene2Query.hugoSymbol || ''; + // Translate to OQL SingleGeneQuery object. + const parsed_oql_line: SingleGeneQuery = gene1 + ? { + gene: gene1, + alterations: [ + { + gene: gene2, + alteration_type: STUCTVARDownstreamFusionStr, + modifiers: [], + }, + ], + } + : { + gene: gene2, + alterations: [ + { + gene: gene1, + alteration_type: STUCTVARUpstreamFusionStr, + modifiers: [], + }, + ], + }; + return unparseOQLQueryLine(parsed_oql_line); +} + +export function StructuralVariantFilterQueryFromOql( + gene1Gene2Representation: string, + includeDriver?: boolean, + includeVUS?: boolean, + includeUnknownOncogenicity?: boolean, + selectedDriverTiers?: { [tier: string]: boolean }, + includeUnknownDriverTier?: boolean, + includeGermline?: boolean, + includeSomatic?: boolean, + includeUnknownStatus?: boolean +): StructuralVariantFilterQuery { + if (!gene1Gene2Representation.match('::')) { + throw new Error( + "Stuct var representation is not of format 'GeneA::GeneB'. Passed value: " + + gene1Gene2Representation + ); + } + const [ + gene1HugoSymbol, + gene2HugoSymbol, + ]: string[] = gene1Gene2Representation.split('::'); + if (!gene1HugoSymbol && !gene2HugoSymbol) { + throw new Error( + 'Both Gene1 and Gene2 are falsy. Passed value: ' + + gene1Gene2Representation + ); + } + + return { + gene1Query: createStructVarGeneSubQuery(gene1HugoSymbol), + gene2Query: createStructVarGeneSubQuery(gene2HugoSymbol), + includeDriver: includeDriver === undefined ? true : includeDriver, + includeVUS: includeVUS === undefined ? true : includeVUS, + includeUnknownOncogenicity: + includeUnknownOncogenicity === undefined + ? true + : includeUnknownOncogenicity, + tiersBooleanMap: + selectedDriverTiers || ({} as { [tier: string]: boolean }), + includeUnknownTier: + includeUnknownDriverTier === undefined + ? true + : includeUnknownDriverTier, + includeGermline: includeGermline === undefined ? true : includeGermline, + includeSomatic: includeSomatic === undefined ? true : includeSomatic, + includeUnknownStatus: + includeUnknownStatus === undefined ? true : includeUnknownStatus, + }; +} + +function createStructVarGeneSubQuery( + hugoGeneSybol: string | undefined +): StructuralVariantGeneSubQuery { + let stringStructuralVariantGeneSubQuery; + if (hugoGeneSybol === undefined) { + stringStructuralVariantGeneSubQuery = { + specialValue: 'NO_GENE', + }; + } else if (hugoGeneSybol === STRUCTVARAnyGeneStr) { + stringStructuralVariantGeneSubQuery = { + specialValue: 'ANY_GENE', + }; + } else { + stringStructuralVariantGeneSubQuery = { + hugoSymbol: hugoGeneSybol, + }; + } + return (stringStructuralVariantGeneSubQuery as unknown) as StructuralVariantGeneSubQuery; +} + +export function generateStructVarTableCellKey( + gene1HugoSymbol: string | undefined, + gene2HugoSymbol: string | undefined +): string { + return `${gene1HugoSymbol || STRUCTVARNullGeneStr}::${gene2HugoSymbol || + STRUCTVARNullGeneStr}`; +} +export function oqlQueryToStructVarGenePair( + query: SingleGeneQuery +): StructVarGenePair[] | undefined { + if (!queryContainsStructVarAlteration(query)) { + return []; + } + const representativeGene = query.gene; + return _(query.alterations || []) + .filter(alteration => alterationIsStructVar(alteration)) + .map((alteration: FUSIONCommandDownstream | FUSIONCommandUpstream) => { + const otherGene = alteration.gene; + return alteration.alteration_type === STUCTVARUpstreamFusionStr + ? { + gene1HugoSymbolOrOql: otherGene, + gene2HugoSymbolOrOql: representativeGene, + } + : { + gene1HugoSymbolOrOql: representativeGene, + gene2HugoSymbolOrOql: otherGene, + }; + }) + .value(); +} diff --git a/src/pages/studyView/StudyViewConfig.ts b/src/pages/studyView/StudyViewConfig.ts index b73e88eb0c7..36d4251d653 100644 --- a/src/pages/studyView/StudyViewConfig.ts +++ b/src/pages/studyView/StudyViewConfig.ts @@ -69,6 +69,7 @@ export enum ChartTypeEnum { VIOLIN_PLOT_TABLE = 'VIOLIN_PLOT_TABLE', MUTATED_GENES_TABLE = 'MUTATED_GENES_TABLE', STRUCTURAL_VARIANT_GENES_TABLE = 'STRUCTURAL_VARIANT_GENES_TABLE', + STRUCTURAL_VARIANTS_TABLE = 'STRUCTURAL_VARIANTS_TABLE', CNA_GENES_TABLE = 'CNA_GENES_TABLE', GENOMIC_PROFILES_TABLE = 'GENOMIC_PROFILES_TABLE', CASE_LIST_TABLE = 'CASE_LIST_TABLE', @@ -91,6 +92,7 @@ export enum ChartTypeNameEnum { VIOLIN_PLOT_TABLE = 'table', MUTATED_GENES_TABLE = 'table', STRUCTURAL_VARIANT_GENES_TABLE = 'table', + STRUCTURAL_VARIANTS_TABLE = 'table', CNA_GENES_TABLE = 'table', GENOMIC_PROFILES_TABLE = 'table', CASE_LIST_TABLE = 'table', @@ -126,6 +128,7 @@ const studyViewFrontEnd = { PFS_SURVIVAL: 250, MUTATED_GENES_TABLE: 90, STRUCTURAL_VARIANT_GENES_TABLE: 85, + STRUCTURAL_VARIANTS_TABLE: 85, CNA_GENES_TABLE: 80, PATIENT_TREATMENTS_TABLE: 75, PATIENT_TREATMENT_GROUPS_TABLE: 75, @@ -208,6 +211,11 @@ const studyViewFrontEnd = { h: 2, minW: 2, }, + [ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE]: { + w: 2, + h: 2, + minW: 2, + }, [ChartTypeEnum.CNA_GENES_TABLE]: { w: 2, h: 2, diff --git a/src/pages/studyView/StudyViewPageStore.ts b/src/pages/studyView/StudyViewPageStore.ts index b513cfbf946..a0a48f56694 100644 --- a/src/pages/studyView/StudyViewPageStore.ts +++ b/src/pages/studyView/StudyViewPageStore.ts @@ -61,7 +61,9 @@ import { SampleIdentifier, SampleMolecularIdentifier, SampleTreatmentRow, + StructuralVariantFilterQuery, StudyViewFilter, + StudyViewStructuralVariantFilter, } from 'cbioportal-ts-api-client'; import { fetchCopyNumberSegmentsForSamples, @@ -130,6 +132,7 @@ import { getSampleToClinicalData, getStructuralVariantSamplesCount, getUniqueKey, + getUniqueKeyFromGeneFilterMolecularProfileIds, getUniqueKeyFromMolecularProfileIds, getUserGroupColor, isFiltered, @@ -143,6 +146,7 @@ import { RectangleBounds, shouldShowChart, showOriginStudiesInSummaryDescription, + showQueryUpdatedToast, SPECIAL_CHARTS, SpecialChartsUniqueKeyEnum, statusFilterActive, @@ -159,7 +163,13 @@ import { updateGeneQuery, } from 'pages/studyView/StudyViewUtils'; import { generateDownloadFilenamePrefixByStudies } from 'shared/lib/FilenameUtils'; -import { unparseOQLQueryLine } from 'shared/lib/oql/oqlfilter'; +import { + convertToGene1Gene2String, + parseOQLQuery, + queryContainsStructVarAlteration, + STRUCTVARNullGeneStr, + unparseOQLQueryLine, +} from 'shared/lib/oql/oqlfilter'; import sessionServiceClient from 'shared/api//sessionServiceInstance'; import windowStore from 'shared/components/window/WindowStore'; import { getHeatmapMeta } from '../../shared/lib/MDACCUtils'; @@ -267,12 +277,24 @@ import { import { PageType } from 'shared/userSession/PageType'; import { FeatureFlagEnum } from 'shared/featureFlags'; import intersect from 'fast_array_intersect'; +import { Simulate } from 'react-dom/test-utils'; +import select = Simulate.select; +import { StructVarMultiSelectionTableRow } from 'pages/studyView/table/StructuralVariantMultiSelectionTable'; import { PillStore } from 'shared/components/PillTag/PillTag'; import { toast, cssTransition } from 'react-toastify'; import { PatientIdentifier, PatientIdentifierFilter, } from 'shared/model/PatientIdentifierFilter'; +import { + doesStructVarMatchSingleGeneQuery, + generateStructVarTableCellKey, + oqlQueryToStructVarGenePair, + StructuralVariantFilterQueryFromOql, + structVarFilterQueryToOql, + StructVarGenePair, + updateStructuralVariantQuery, +} from 'pages/studyView/StructVarUtils'; import { ClinicalAttributeQueryExtractor, SharedGroupsAndCustomDataQueryExtractor, @@ -395,6 +417,17 @@ export type OncokbCancerGene = { isCancerGene: boolean; }; +export type OncokbCancerStructVar = { + gene1OncokbAnnotated: boolean; + gene1IsOncokbOncogene: boolean; + gene1IsOncokbTumorSuppressorGene: boolean; + gene1IsCancerGene: boolean; + gene2OncokbAnnotated: boolean; + gene2IsOncokbOncogene: boolean; + gene2IsOncokbTumorSuppressorGene: boolean; + gene2IsCancerGene: boolean; +}; + export enum BinMethodOption { QUARTILE = 'QUARTILE', MEDIAN = 'MEDIAN', @@ -1990,6 +2023,11 @@ export class StudyViewPageStore GeneFilterQuery[][] >(); + @observable private _structVarFilterSet = observable.map< + string, + StructuralVariantFilterQuery[][] + >(); + // TODO: make it computed // Currently the study view store does not have the full control of the promise. // ChartContainer should be modified, instead of accepting a promise, it should accept data and loading state. @@ -2086,12 +2124,23 @@ export class StudyViewPageStore if (!_.isEmpty(filters.geneFilters)) { filters.geneFilters!.forEach(geneFilter => { - const key = getUniqueKeyFromMolecularProfileIds( + const key = getUniqueKeyFromGeneFilterMolecularProfileIds( geneFilter.molecularProfileIds ); this._geneFilterSet.set(key, _.clone(geneFilter.geneQueries)); }); } + if (!_.isEmpty(filters.structuralVariantFilters)) { + filters.structuralVariantFilters!.forEach(structVarFilter => { + const key = getUniqueKeyFromMolecularProfileIds( + structVarFilter.molecularProfileIds + ); + this._structVarFilterSet.set( + key, + _.clone(structVarFilter.structVarQueries) + ); + }); + } if (!_.isEmpty(filters.sampleIdentifiers)) { this.numberOfSelectedSamplesInCustomSelection = filters.sampleIdentifiers!.length; this.updateChartSampleIdentifierFilter( @@ -2344,6 +2393,7 @@ export class StudyViewPageStore @observable private _filterMutatedGenesTableByCancerGenes: boolean = false; @observable private _filterSVGenesTableByCancerGenes: boolean = false; + @observable private _filterStructVarsTableByCancerGenes: boolean = false; @observable private _filterCNAGenesTableByCancerGenes: boolean = false; @action.bound @@ -2354,6 +2404,10 @@ export class StudyViewPageStore updateSVGenesTableByCancerGenesFilter(filtered: boolean): void { this._filterSVGenesTableByCancerGenes = filtered; } + @action.bound + updateStructVarsTableByCancerGenesFilter(filtered: boolean): void { + this._filterStructVarsTableByCancerGenes = filtered; + } @action.bound updateCNAGenesTableByCancerGenesFilter(filtered: boolean): void { @@ -2374,6 +2428,13 @@ export class StudyViewPageStore ); } + @computed get filterStructVarsTableByCancerGenes(): boolean { + return ( + this.oncokbCancerGeneFilterEnabled && + this._filterStructVarsTableByCancerGenes + ); + } + @computed get filterCNAGenesTableByCancerGenes(): boolean { return ( this.oncokbCancerGeneFilterEnabled && @@ -2522,25 +2583,8 @@ export class StudyViewPageStore message = `${hugoGeneSymbol} queued for query (see top right)`; } - const Zoom = cssTransition({ - enter: 'zoomIn', - exit: 'zoomOut', - appendPosition: false, - collapse: true, - collapseDuration: 300, - }); + showQueryUpdatedToast(message); - toast.success(message, { - delay: 0, - position: 'top-right', - autoClose: 1500, - hideProgressBar: true, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: 'light', - }); //only update geneQueryStr whenever a table gene is clicked. this.geneQueries = updateGeneQuery(this.geneQueries, hugoGeneSymbol); this.geneQueryStr = this.geneQueries @@ -2548,6 +2592,38 @@ export class StudyViewPageStore .join(' '); } + @action.bound + onCheckStructuralVariant( + gene1SymbolOrOql: string, + gene2SymbolOrOql: string + ): void { + let message = ''; + if ( + this.geneQueries.find(q => + doesStructVarMatchSingleGeneQuery( + q, + gene1SymbolOrOql, + gene1SymbolOrOql + ) + ) + ) { + message = `${gene1SymbolOrOql}::${gene2SymbolOrOql} removed from query queue`; + } else { + message = `${gene1SymbolOrOql}::${gene2SymbolOrOql} queued for query (see top right)`; + } + + showQueryUpdatedToast(message); + + this.geneQueries = updateStructuralVariantQuery( + this.geneQueries, + gene1SymbolOrOql, + gene2SymbolOrOql + ); + this.geneQueryStr = this.geneQueries + .map(query => unparseOQLQueryLine(query)) + .join(' '); + } + @action.bound isSharedCustomData(chartId: string): boolean { return this.customChartSet.has(chartId) @@ -2556,7 +2632,24 @@ export class StudyViewPageStore } @computed get selectedGenes(): string[] { - return this.geneQueries.map(singleGeneQuery => singleGeneQuery.gene); + return _(this.geneQueries) + .filter(query => !queryContainsStructVarAlteration(query)) + .map(query => query.gene) + .value(); + } + + @computed get selectedStructuralVariants(): StructVarGenePair[] { + return _(this.geneQueries) + .filter(query => queryContainsStructVarAlteration(query)) + .map(query => oqlQueryToStructVarGenePair(query)) + .flatten() + .compact() // Remove falsy elements. + .uniqWith( + (repr1, repr2) => + repr1.gene1HugoSymbolOrOql === repr2.gene1HugoSymbolOrOql && + repr1.gene2HugoSymbolOrOql === repr2.gene2HugoSymbolOrOql + ) // Remove duplicate elements. + .value(); } @action.bound @@ -2631,6 +2724,7 @@ export class StudyViewPageStore this._clinicalDataFilterSet.clear(); this._customDataFilterSet.clear(); this._geneFilterSet.clear(); + this._structVarFilterSet.clear(); this._genomicDataIntervalFilterSet.clear(); this._genericAssayDataFilterSet.clear(); this._chartSampleIdentifiersFilterSet.clear(); @@ -3021,6 +3115,39 @@ export class StudyViewPageStore this._geneFilterSet.set(chartMeta.uniqueKey, geneFilter); } + @action.bound + addStructVarFilters( + chartMeta: ChartMeta, + seletedStructVarRows: string[][] + ): void { + trackStudyViewFilterEvent('structVarFilter', this); + let structVarFilter = + toJS(this._structVarFilterSet.get(chartMeta.uniqueKey)) || []; + // convert structVarRowKeys to GeneFilterObjects accepted by the backend. + const queries: StructuralVariantFilterQuery[][] = _.map( + seletedStructVarRows, + structVarRowKeys => + _.map(structVarRowKeys, structVarRowKey => + StructuralVariantFilterQueryFromOql( + structVarRowKey, + this.driverAnnotationSettings.includeDriver, + this.driverAnnotationSettings.includeVUS, + this.driverAnnotationSettings + .includeUnknownOncogenicity, + this.selectedDriverTiersMap.isComplete + ? this.selectedDriverTiersMap.result! + : {}, + this.driverAnnotationSettings.includeUnknownTier, + this.includeGermlineMutations, + this.includeSomaticMutations, + this.includeUnknownStatusMutations + ) + ) + ); + structVarFilter = structVarFilter.concat(queries); + this._structVarFilterSet.set(chartMeta.uniqueKey, structVarFilter); + } + @action.bound updateGenomicDataIntervalFiltersByValues( uniqueKey: string, @@ -3104,11 +3231,50 @@ export class StudyViewPageStore } } + @action.bound + removeStructVarFilter(chartUniqueKey: string, toBeRemoved: string): void { + const oqlQuery = parseOQLQuery(toBeRemoved + ';')[0]; + const gene1Gene2Str = convertToGene1Gene2String(oqlQuery)[0]; + const [ + gene1HugoSymbol, + gene2HugoSymbol, + ]: string[] = gene1Gene2Str.split('::'); + let structVarFilters: StructuralVariantFilterQuery[][] = + toJS(this._structVarFilterSet.get(chartUniqueKey)) || []; + structVarFilters = _.reduce( + structVarFilters, + (acc, next) => { + const newGroup = next.filter( + StructuralVariantFilterQuery => + StructuralVariantFilterQuery.gene1Query.hugoSymbol !== + gene1HugoSymbol && + StructuralVariantFilterQuery.gene2Query.hugoSymbol !== + gene2HugoSymbol + ); + if (newGroup.length > 0) { + acc.push(newGroup); + } + return acc; + }, + [] as StructuralVariantFilterQuery[][] + ); + if (structVarFilters.length === 0) { + this._structVarFilterSet.delete(chartUniqueKey); + } else { + this._structVarFilterSet.set(chartUniqueKey, structVarFilters); + } + } + @action.bound resetGeneFilter(chartUniqueKey: string): void { this._geneFilterSet.delete(chartUniqueKey); } + @action.bound + resetStructVarFilter(chartUniqueKey: string): void { + this._structVarFilterSet.delete(chartUniqueKey); + } + @action.bound removeGenomicProfileFilter(toBeRemoved: string): void { let genomicProfilesFilter = toJS(this.genomicProfilesFilter) || []; @@ -3279,6 +3445,7 @@ export class StudyViewPageStore break; case ChartTypeEnum.MUTATED_GENES_TABLE: case ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE: + case ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE: case ChartTypeEnum.CNA_GENES_TABLE: this.resetGeneFilter(chartUniqueKey); break; @@ -3362,6 +3529,8 @@ export class StudyViewPageStore case ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE: case ChartTypeEnum.CNA_GENES_TABLE: return this._geneFilterSet.has(chartUniqueKey); + case ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE: + return this._structVarFilterSet.has(chartUniqueKey); case ChartTypeEnum.GENOMIC_PROFILES_TABLE: return !_.isEmpty(this._genomicProfilesFilter); case ChartTypeEnum.CASE_LIST_TABLE: @@ -3645,6 +3814,15 @@ export class StudyViewPageStore }); } + @computed get structVarFilters(): StudyViewStructuralVariantFilter[] { + return _.map(this._structVarFilterSet.toJSON(), ([key, value]) => { + return { + molecularProfileIds: getMolecularProfileIdsFromUniqueKey(key), + structVarQueries: value, + }; + }); + } + @computed get genomicDataIntervalFilters(): GenomicDataFilter[] { return Array.from(this._genomicDataIntervalFilterSet.values()); } @@ -3684,6 +3862,10 @@ export class StudyViewPageStore filters.geneFilters = this.geneFilters; } + if (this.structVarFilters.length > 0) { + filters.structuralVariantFilters = this.structVarFilters; + } + if (this.genomicProfilesFilter.length > 0) { filters.genomicProfiles = toJS(this.genomicProfilesFilter); } @@ -3812,6 +3994,14 @@ export class StudyViewPageStore return toJS(filters); } + public getStructVarFiltersByUniqueKey(uniqueKey: string): string[][] { + const filters = _.map( + this._structVarFilterSet.get(uniqueKey), + filterSet => _.map(filterSet, structVarFilterQueryToOql) + ); + return toJS(filters); + } + @autobind public getClinicalDataFiltersByUniqueKey( uniqueKey: string @@ -6173,11 +6363,12 @@ export class StudyViewPageStore if (!_.isEmpty(this.structuralVariantProfiles.result)) { const uniqueKey = getUniqueKeyFromMolecularProfileIds( this.structuralVariantProfiles.result.map( - profile => profile.molecularProfileId - ) + p => p.molecularProfileId + ), + ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE ); _chartMetaSet[uniqueKey] = { - uniqueKey: uniqueKey, + uniqueKey, dataType: ChartMetaDataTypeEnum.GENOMIC, patientAttribute: false, displayName: 'Structural Variant Genes', @@ -6187,6 +6378,25 @@ export class StudyViewPageStore renderWhenDataChange: true, description: '', }; + if (this.isStructVarFeatureFlagEnabled) { + const structVarGenesUniqueKey = getUniqueKeyFromMolecularProfileIds( + this.structuralVariantProfiles.result.map( + p => p.molecularProfileId + ), + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ); + _chartMetaSet[structVarGenesUniqueKey] = { + uniqueKey: structVarGenesUniqueKey, + dataType: ChartMetaDataTypeEnum.GENOMIC, + patientAttribute: false, + displayName: 'Structural Variants', + priority: getDefaultPriorityByUniqueKey( + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ), + renderWhenDataChange: true, + description: '', + }; + } } if (!_.isEmpty(this.cnaProfiles.result)) { @@ -6196,7 +6406,7 @@ export class StudyViewPageStore ) ); _chartMetaSet[uniqueKey] = { - uniqueKey: uniqueKey, + uniqueKey, dataType: ChartMetaDataTypeEnum.GENOMIC, patientAttribute: false, displayName: 'CNA Genes', @@ -6243,6 +6453,13 @@ export class StudyViewPageStore ); } + // TODO Remove feature flag after acceptance by product team. + get isStructVarFeatureFlagEnabled() { + return this.appStore.featureFlagStore.has( + FeatureFlagEnum.STUDY_VIEW_STRUCT_VAR_TABLE + ); + } + @computed get showSettingRestoreMsg(): boolean { return !!( @@ -6717,6 +6934,12 @@ export class StudyViewPageStore ? true : chartUserSettings.filterByCancerGenes; break; + case ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE: + this._filterStructVarsTableByCancerGenes = + chartUserSettings.filterByCancerGenes === undefined + ? true + : chartUserSettings.filterByCancerGenes; + break; case ChartTypeEnum.CNA_GENES_TABLE: this._filterCNAGenesTableByCancerGenes = chartUserSettings.filterByCancerGenes === undefined @@ -6870,8 +7093,9 @@ export class StudyViewPageStore if (!_.isEmpty(this.structuralVariantProfiles.result)) { const uniqueKey = getUniqueKeyFromMolecularProfileIds( this.structuralVariantProfiles.result.map( - profile => profile.molecularProfileId - ) + p => p.molecularProfileId + ), + ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE ); const structuralVariantGeneMeta = _.find( this.chartMetaSet, @@ -6896,6 +7120,37 @@ export class StudyViewPageStore ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE ] ); + if (this.isStructVarFeatureFlagEnabled) { + const structVarUniqueKey = getUniqueKeyFromMolecularProfileIds( + this.structuralVariantProfiles.result.map( + p => p.molecularProfileId + ), + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ); + const structuralVariantsMeta = _.find( + this.chartMetaSet, + chartMeta => chartMeta.uniqueKey === uniqueKey + ); + if ( + structuralVariantsMeta && + structuralVariantsMeta.priority !== 0 + ) { + this.changeChartVisibility( + structuralVariantsMeta.uniqueKey, + true + ); + } + this.chartsType.set( + structVarUniqueKey, + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ); + this.chartsDimension.set( + structVarUniqueKey, + STUDY_VIEW_CONFIG.layout.dimensions[ + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ] + ); + } } if (!_.isEmpty(this.cnaProfiles.result)) { const uniqueKey = getUniqueKeyFromMolecularProfileIds( @@ -7903,6 +8158,85 @@ export class StudyViewPageStore default: [], }); + readonly structuralVariantTableRowData = remoteData< + StructVarMultiSelectionTableRow[] + >({ + await: () => + this.oncokbCancerGeneFilterEnabled + ? [ + this.structuralVariantProfiles, + this.oncokbAnnotatedGeneEntrezGeneIds, + this.oncokbOncogeneEntrezGeneIds, + this.oncokbTumorSuppressorGeneEntrezGeneIds, + this.oncokbCancerGeneEntrezGeneIds, + ] + : [this.structuralVariantProfiles], + invoke: async () => { + if (!_.isEmpty(this.structuralVariantProfiles.result)) { + const structuralVariantCounts = await internalClient.fetchStructuralVariantCountsUsingPOST( + { + studyViewFilter: this.filters, + } + ); + return structuralVariantCounts.map(item => { + return { + ...item, + label1: item.gene1HugoGeneSymbol, + label2: item.gene2HugoGeneSymbol, + uniqueKey: generateStructVarTableCellKey( + item.gene1HugoGeneSymbol, + item.gene2HugoGeneSymbol + ), + gene1OncokbAnnotated: + this.oncokbCancerGeneFilterEnabled && + this.oncokbAnnotatedGeneEntrezGeneIds.result.includes( + item.gene1EntrezGeneId + ), + gene2OncokbAnnotated: + this.oncokbCancerGeneFilterEnabled && + this.oncokbAnnotatedGeneEntrezGeneIds.result.includes( + item.gene2EntrezGeneId + ), + gene1IsOncokbOncogene: + this.oncokbCancerGeneFilterEnabled && + this.oncokbOncogeneEntrezGeneIds.result.includes( + item.gene1EntrezGeneId + ), + gene2IsOncokbOncogene: + this.oncokbCancerGeneFilterEnabled && + this.oncokbOncogeneEntrezGeneIds.result.includes( + item.gene2EntrezGeneId + ), + gene1IsOncokbTumorSuppressorGene: + this.oncokbCancerGeneFilterEnabled && + this.oncokbTumorSuppressorGeneEntrezGeneIds.result.includes( + item.gene1EntrezGeneId + ), + gene2IsOncokbTumorSuppressorGene: + this.oncokbCancerGeneFilterEnabled && + this.oncokbTumorSuppressorGeneEntrezGeneIds.result.includes( + item.gene2EntrezGeneId + ), + gene1IsCancerGene: + this.oncokbCancerGeneFilterEnabled && + this.oncokbCancerGeneEntrezGeneIds.result.includes( + item.gene1EntrezGeneId + ), + gene2IsCancerGene: + this.oncokbCancerGeneFilterEnabled && + this.oncokbCancerGeneEntrezGeneIds.result.includes( + item.gene2EntrezGeneId + ), + }; + }); + } else { + return []; + } + }, + onError: () => {}, + default: [], + }); + readonly cnaGeneTableRowData = remoteData({ await: () => this.oncokbCancerGeneFilterEnabled @@ -9131,11 +9465,19 @@ export class StudyViewPageStore ); } if (!_.isEmpty(this.structuralVariantProfiles.result)) { - const uniqueKey = getUniqueKeyFromMolecularProfileIds( + const structVarGenesUniqueKey = getUniqueKeyFromMolecularProfileIds( this.structuralVariantProfiles.result.map( profile => profile.molecularProfileId - ) + ), + ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE + ); + const structVarUniqueKey = getUniqueKeyFromMolecularProfileIds( + this.structuralVariantProfiles.result.map( + profile => profile.molecularProfileId + ), + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE ); + // samples countaing this data would be the samples profiled for these molecular profiles let count = _.sumBy( this.structuralVariantProfiles.result, @@ -9153,7 +9495,8 @@ export class StudyViewPageStore ); count = ret[key]; } - ret[uniqueKey] = count; + ret[structVarGenesUniqueKey] = count; + ret[structVarUniqueKey] = count; } if (!_.isEmpty(this.cnaProfiles.result)) { @@ -9614,7 +9957,8 @@ export class StudyViewPageStore : 0; break; } - case ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE: { + case ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE: + case ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE: { count = getStructuralVariantSamplesCount( this.molecularProfileSampleCountSet.result ); diff --git a/src/pages/studyView/StudyViewUtils.spec.tsx b/src/pages/studyView/StudyViewUtils.spec.tsx index 06b8d8a07aa..8f181dffbc9 100644 --- a/src/pages/studyView/StudyViewUtils.spec.tsx +++ b/src/pages/studyView/StudyViewUtils.spec.tsx @@ -99,6 +99,17 @@ import { remoteData, toPromise } from 'cbioportal-frontend-commons'; import { autorun, observable, runInAction } from 'mobx'; import { AlterationTypeConstants, DataTypeConstants } from 'shared/constants'; +import { SingleGeneQuery } from 'shared/lib/oql/oql-parser'; +import { + oqlQueryToStructVarGenePair, + updateStructuralVariantQuery, +} from 'pages/studyView/StructVarUtils'; +import { + STRUCTVARAnyGeneStr, + STRUCTVARNullGeneStr, + STUCTVARDownstreamFusionStr, + STUCTVARUpstreamFusionStr, +} from 'shared/lib/oql/oqlfilter'; describe('StudyViewUtils', () => { const emptyStudyViewFilter: StudyViewFilter = { @@ -180,6 +191,90 @@ describe('StudyViewUtils', () => { }); }); + describe('updateStructuralVariantQuery', () => { + it.each([ + [ + [ + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARDownstreamFusionStr, + gene: 'B', + }, + ], + }, + ] as SingleGeneQuery[], + 'A', + 'B', + [] as SingleGeneQuery[], + ], + [ + [ + { + gene: 'B', + alterations: [ + { + alteration_type: STUCTVARUpstreamFusionStr, + gene: 'A', + }, + ], + }, + ] as SingleGeneQuery[], + 'A', + 'B', + [] as SingleGeneQuery[], + ], + [ + [] as SingleGeneQuery[], + 'A', + 'B', + [ + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARDownstreamFusionStr, + gene: 'B', + modifiers: [], + }, + ], + }, + ] as SingleGeneQuery[], + ], + [ + [{ gene: 'X' }] as SingleGeneQuery[], + 'A', + 'B', + [ + { gene: 'X' }, + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARDownstreamFusionStr, + gene: 'B', + modifiers: [], + }, + ], + }, + ] as SingleGeneQuery[], + ], + ])( + 'updates queries', + (input, selectedGene1, selectedGene2, expected) => { + assert.deepEqual( + updateStructuralVariantQuery( + input, + selectedGene1, + selectedGene2 + ), + expected + ); + } + ); + }); + describe('getVirtualStudyDescription', () => { let studies = [ { @@ -3461,16 +3556,17 @@ describe('StudyViewUtils', () => { ['study1'] ) .then(() => { - assert.isTrue( - fetchStub.calledWith({ - studyViewFilter: { - ...emptyStudyViewFilter, - sampleIdentifiers: [ - { sampleId: 'sample1', studyId: 'study1' }, - ], - }, - }) - ); + const expectedFilters = { + studyViewFilter: { + ...emptyStudyViewFilter, + sampleIdentifiers: [ + { sampleId: 'sample1', studyId: 'study1' }, + ], + structuralVariantFilters: undefined, + }, + }; + const actualFilters = fetchStub.getCall(0).args[0]; + expect(actualFilters).toStrictEqual(expectedFilters); done(); }) .catch(done); @@ -3492,16 +3588,17 @@ describe('StudyViewUtils', () => { ['study1'] ) .then(() => { - assert.isTrue( - fetchStub.calledWith({ - studyViewFilter: { - ...emptyStudyViewFilter, - sampleIdentifiers: [ - { sampleId: 'sample1', studyId: 'study1' }, - ], - }, - }) - ); + const expectedFilters = { + studyViewFilter: { + ...emptyStudyViewFilter, + sampleIdentifiers: [ + { sampleId: 'sample1', studyId: 'study1' }, + ], + structuralVariantFilters: undefined, + }, + }; + const actualFilters = fetchStub.getCall(0).args[0]; + expect(actualFilters).toStrictEqual(expectedFilters); done(); }) .catch(done); @@ -3516,14 +3613,15 @@ describe('StudyViewUtils', () => { ['study1'] ) .then(() => { - assert.isTrue( - fetchStub.calledWith({ - studyViewFilter: { - ...emptyStudyViewFilter, - studyIds: ['study1'], - }, - }) - ); + const expectedFilters = { + studyViewFilter: { + ...emptyStudyViewFilter, + studyIds: ['study1'], + structuralVariantFilters: undefined, + }, + }; + const actualFilters = fetchStub.getCall(0).args[0]; + expect(actualFilters).toStrictEqual(expectedFilters); done(); }) .catch(done); @@ -3542,16 +3640,17 @@ describe('StudyViewUtils', () => { ['study1'] ) .then(() => { - assert.isTrue( - fetchStub.calledWith({ - studyViewFilter: { - ...emptyStudyViewFilter, - sampleIdentifiers: [ - { sampleId: 'sample1', studyId: 'study1' }, - ], - }, - }) - ); + const expectedFilters = { + studyViewFilter: { + ...emptyStudyViewFilter, + sampleIdentifiers: [ + { sampleId: 'sample1', studyId: 'study1' }, + ], + structuralVariantFilters: undefined, + }, + }; + const actualFilters = fetchStub.getCall(0).args[0]; + expect(actualFilters).toStrictEqual(expectedFilters); done(); }) .catch(done); @@ -4993,6 +5092,7 @@ describe('StudyViewUtils', () => { ); }); }); + describe('Create object for group comparison custom numerical data', () => { it('transform sample data to clinical data ', function() { const sampleData = [ @@ -5081,4 +5181,94 @@ describe('StudyViewUtils', () => { assert.deepEqual(result, outputObject); }); }); + + describe('oqlQueryToGene1Gene2Representation', () => { + it.each([ + [ + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARDownstreamFusionStr, + gene: 'B', + }, + ], + } as SingleGeneQuery, + [{ gene1HugoSymbolOrOql: 'A', gene2HugoSymbolOrOql: 'B' }], + ], + [ + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARUpstreamFusionStr, + gene: 'B', + }, + ], + } as SingleGeneQuery, + [{ gene1HugoSymbolOrOql: 'B', gene2HugoSymbolOrOql: 'A' }], + ], + [ + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARDownstreamFusionStr, + gene: 'B', + }, + { + alteration_type: STUCTVARUpstreamFusionStr, + gene: 'B', + }, + ], + } as SingleGeneQuery, + [ + { gene1HugoSymbolOrOql: 'A', gene2HugoSymbolOrOql: 'B' }, + { gene1HugoSymbolOrOql: 'B', gene2HugoSymbolOrOql: 'A' }, + ], + ], + [ + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARDownstreamFusionStr, + gene: STRUCTVARAnyGeneStr, + }, + ], + } as SingleGeneQuery, + [ + { + gene1HugoSymbolOrOql: 'A', + gene2HugoSymbolOrOql: STRUCTVARAnyGeneStr, + }, + ], + ], + [ + { + gene: 'A', + alterations: [ + { + alteration_type: STUCTVARDownstreamFusionStr, + gene: STRUCTVARNullGeneStr, + }, + ], + } as SingleGeneQuery, + [ + { + gene1HugoSymbolOrOql: 'A', + gene2HugoSymbolOrOql: STRUCTVARNullGeneStr, + }, + ], + ], + ])( + 'should convert oql query to gene1/gene2 representation', + (oqlQuery, expected) => { + assert.deepEqual( + expected, + oqlQueryToStructVarGenePair(oqlQuery) + ); + } + ); + }); }); diff --git a/src/pages/studyView/StudyViewUtils.tsx b/src/pages/studyView/StudyViewUtils.tsx index 1f3009782e3..42bc99cbcb0 100644 --- a/src/pages/studyView/StudyViewUtils.tsx +++ b/src/pages/studyView/StudyViewUtils.tsx @@ -26,18 +26,19 @@ import { PatientIdentifier, Sample, SampleIdentifier, + StructuralVariantFilterQuery, StudyViewFilter, } from 'cbioportal-ts-api-client'; import * as React from 'react'; import { buildCBioPortalPageUrl } from '../../shared/api/urls'; import { BarDatum } from './charts/barChart/BarChart'; import { + BinMethodOption, GenericAssayChart, GenomicChart, - XvsYScatterChart, XvsYChartSettings, + XvsYScatterChart, XvsYViolinChart, - BinMethodOption, } from './StudyViewPageStore'; import { StudyViewPageTabKeyEnum } from 'pages/studyView/StudyViewPageTabs'; import { Layout } from 'react-grid-layout'; @@ -84,6 +85,8 @@ import { getServerConfig } from 'config/config'; import joinJsx from 'shared/lib/joinJsx'; import { BoundType, NumberRange } from 'range-ts'; import { ClinicalEventTypeCount } from 'cbioportal-ts-api-client/dist/generated/CBioPortalAPIInternal'; +import { queryContainsStructVarAlteration } from 'shared/lib/oql/oqlfilter'; +import { toast } from 'react-toastify'; // Cannot use ClinicalDataTypeEnum here for the strong type. The model in the type is not strongly typed export enum ClinicalDataTypeEnum { @@ -524,13 +527,18 @@ export function getDescriptionOverlay( ); } +// This function acts as a toggle. If present in 'geneQueries', +// the query is removed. If absent a query is added. export function updateGeneQuery( geneQueries: SingleGeneQuery[], selectedGene: string ): SingleGeneQuery[] { + // Remove any query that is already known for this gene. let updatedQueries = _.filter( geneQueries, - query => query.gene !== selectedGene + query => + query.gene !== selectedGene || + queryContainsStructVarAlteration(query) ); if (updatedQueries.length === geneQueries.length) { updatedQueries.push({ @@ -786,11 +794,30 @@ export function getGenericAssayChartUniqueKey( } const UNIQUE_KEY_SEPARATOR = ':'; +const CHART_TYPE_SEPARATOR = ';'; -export function getUniqueKeyFromMolecularProfileIds( +export function getUniqueKeyFromGeneFilterMolecularProfileIds( molecularProfileIds: string[] ) { - return _.sortBy(molecularProfileIds).join(UNIQUE_KEY_SEPARATOR); + const svChartType = molecularProfileIds[0]?.includes('structural_variants') + ? ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE + : undefined; + return getUniqueKeyFromMolecularProfileIds( + molecularProfileIds, + svChartType + ); +} + +export function getUniqueKeyFromMolecularProfileIds( + molecularProfileIds: string[], + chartType?: ChartTypeEnum +) { + const returnValue = _(molecularProfileIds) + .sortBy(molecularProfileIds) + .join(UNIQUE_KEY_SEPARATOR); + return chartType + ? chartType + CHART_TYPE_SEPARATOR + returnValue + : returnValue; } export function calculateSampleCountForClinicalEventTypeCountTable( @@ -811,8 +838,27 @@ export function calculateSampleCountForClinicalEventTypeCountTable( return sampleCount; } +function startsWithSvChartType(chartType?: string) { + return ( + chartType && + [ + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE, + ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE, + ].some(ct => chartType.startsWith(ct)) + ); +} + export function getMolecularProfileIdsFromUniqueKey(uniqueKey: string) { - return uniqueKey.split(UNIQUE_KEY_SEPARATOR); + const parts = uniqueKey.split(UNIQUE_KEY_SEPARATOR); + if (!startsWithSvChartType(parts[0])) { + return parts; + } else { + return _.map( + parts, + molecularProfileId => + molecularProfileId.split(CHART_TYPE_SEPARATOR)[1] + ); + } } export function getCurrentDate() { @@ -988,6 +1034,7 @@ export function isFiltered( _.isEmpty(filter) || (_.isEmpty(filter.clinicalDataFilters) && _.isEmpty(filter.geneFilters) && + _.isEmpty(filter.structuralVariantFilters) && _.isEmpty(filter.genomicProfiles) && _.isEmpty(filter.genomicDataFilters) && _.isEmpty(filter.genericAssayDataFilters) && @@ -2364,6 +2411,7 @@ export function getSamplesByExcludingFiltersOnChart( let updatedFilter: StudyViewFilter = { clinicalDataFilters: filter.clinicalDataFilters, geneFilters: filter.geneFilters, + structuralVariantFilters: filter.structuralVariantFilters, } as any; let _sampleIdentifiers = _.reduce( @@ -3411,25 +3459,26 @@ export function buildSelectedDriverTiersMap( export const FilterIconMessage: React.FunctionComponent<{ chartType: ChartType; - geneFilterQuery: GeneFilterQuery; -}> = observer(({ chartType, geneFilterQuery }) => { + annotatedFilterQuery: GeneFilterQuery | StructuralVariantFilterQuery; +}> = observer(({ chartType, annotatedFilterQuery }) => { const annotationFilterIsActive = annotationFilterActive( - geneFilterQuery.includeDriver, - geneFilterQuery.includeVUS, - geneFilterQuery.includeUnknownOncogenicity + annotatedFilterQuery.includeDriver, + annotatedFilterQuery.includeVUS, + annotatedFilterQuery.includeUnknownOncogenicity ); const tierFilterIsActive = driverTierFilterActive( - geneFilterQuery.tiersBooleanMap, - geneFilterQuery.includeUnknownTier + annotatedFilterQuery.tiersBooleanMap, + annotatedFilterQuery.includeUnknownTier ); const statusFilterIsActive = statusFilterActive( - geneFilterQuery.includeGermline, - geneFilterQuery.includeSomatic, - geneFilterQuery.includeUnknownStatus + annotatedFilterQuery.includeGermline, + annotatedFilterQuery.includeSomatic, + annotatedFilterQuery.includeUnknownStatus ); const isMutationType = chartType === ChartTypeEnum.MUTATED_GENES_TABLE || - chartType === ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE; + chartType === ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE || + chartType === ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE; if ( !annotationFilterIsActive && !tierFilterIsActive && @@ -3439,31 +3488,31 @@ export const FilterIconMessage: React.FunctionComponent<{ const driverFilterTextElements: string[] = []; if (annotationFilterIsActive) { - geneFilterQuery.includeDriver && + annotatedFilterQuery.includeDriver && driverFilterTextElements.push('driver'); - geneFilterQuery.includeVUS && + annotatedFilterQuery.includeVUS && driverFilterTextElements.push('passenger'); - geneFilterQuery.includeUnknownOncogenicity && + annotatedFilterQuery.includeUnknownOncogenicity && driverFilterTextElements.push('unknown'); } const statusFilterTextElements: string[] = []; if (statusFilterIsActive && isMutationType) { - geneFilterQuery.includeGermline && + annotatedFilterQuery.includeGermline && statusFilterTextElements.push('germline'); - geneFilterQuery.includeSomatic && + annotatedFilterQuery.includeSomatic && statusFilterTextElements.push('somatic'); - geneFilterQuery.includeUnknownStatus && + annotatedFilterQuery.includeUnknownStatus && statusFilterTextElements.push('unknown'); } const tierNames = tierFilterIsActive - ? _(geneFilterQuery.tiersBooleanMap) + ? _(annotatedFilterQuery.tiersBooleanMap) .pickBy() .keys() .value() : []; - if (tierFilterIsActive && geneFilterQuery.includeUnknownTier) + if (tierFilterIsActive && annotatedFilterQuery.includeUnknownTier) tierNames.push('unknown'); let driverFilterText = ''; @@ -3570,17 +3619,19 @@ export function findInvalidMolecularProfileIds( filters: StudyViewFilter, molecularProfiles: MolecularProfile[] ): string[] { + let geneFilters = filters.geneFilters; const molecularProfilesInFilters = _( - filters.geneFilters?.map(f => f.molecularProfileIds) + geneFilters?.map(f => f.molecularProfileIds) ) .flatten() .uniq() .value(); - - return _.difference( + let result = _.difference( molecularProfilesInFilters, molecularProfiles.map(p => p.molecularProfileId) ); + console.log({ geneFilters, result }); + return result; } export function getFilteredMolecularProfilesByAlterationType( @@ -3888,3 +3939,16 @@ export function transformSampleDataToSelectedSampleClinicalData( .filter(item => item.uniqueSampleKey !== undefined); return clinicalDataSamples; } +export function showQueryUpdatedToast(message: string) { + toast.success(message, { + delay: 0, + position: 'top-right', + autoClose: 1500, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: 'light', + } as any); +} diff --git a/src/pages/studyView/TableUtils.tsx b/src/pages/studyView/TableUtils.tsx index e8c291dfa8f..3f5b7ea613a 100644 --- a/src/pages/studyView/TableUtils.tsx +++ b/src/pages/studyView/TableUtils.tsx @@ -96,6 +96,7 @@ export function getCancerGeneFilterToggleIcon( export enum FreqColumnTypeEnum { MUTATION = 'mutations', STRUCTURAL_VARIANT = 'structural variants', + STRUCTURAL_VARIANT_PAIR = 'structural variant pairs', CNA = 'copy number alterations', DATA = 'data', } diff --git a/src/pages/studyView/UserSelections.tsx b/src/pages/studyView/UserSelections.tsx index 6f22ae2797c..38097016328 100644 --- a/src/pages/studyView/UserSelections.tsx +++ b/src/pages/studyView/UserSelections.tsx @@ -4,6 +4,9 @@ import { observer } from 'mobx-react'; import { computed, makeObservable, runInAction } from 'mobx'; import styles from './styles.module.scss'; import { + OredPatientTreatmentFilters, + OredSampleTreatmentFilters, + DataFilter, DataFilterValue, AndedPatientTreatmentFilters, AndedSampleTreatmentFilters, @@ -11,7 +14,7 @@ import { PatientTreatmentFilter, SampleTreatmentFilter, ClinicalDataFilter, - DataFilter, + StructuralVariantFilterQuery, } from 'cbioportal-ts-api-client'; import { DataType, @@ -21,9 +24,6 @@ import { getGenericAssayChartUniqueKey, updateCustomIntervalFilter, SpecialChartsUniqueKeyEnum, -} from 'pages/studyView/StudyViewUtils'; -import { - ChartMeta, geneFilterQueryToOql, getCNAColorByAlteration, getPatientIdentifiers, @@ -31,6 +31,8 @@ import { getUniqueKeyFromMolecularProfileIds, intervalFiltersDisplayValue, StudyViewFilterWithSampleIdentifierFilters, + ChartMeta, + getUniqueKeyFromGeneFilterMolecularProfileIds, } from 'pages/studyView/StudyViewUtils'; import { PillTag } from '../../shared/components/PillTag/PillTag'; import { GroupLogic } from './filters/groupLogic/GroupLogic'; @@ -42,16 +44,13 @@ import { getSampleIdentifiers, StudyViewComparisonGroup, } from '../groupComparison/GroupComparisonUtils'; -import { DefaultTooltip, getBrowserWindow } from 'cbioportal-frontend-commons'; -import { - OredPatientTreatmentFilters, - OredSampleTreatmentFilters, -} from 'cbioportal-ts-api-client'; import { - STRUCTURAL_VARIANT_COLOR, + DefaultTooltip, MUT_COLOR_MISSENSE, + STRUCTURAL_VARIANT_COLOR, } from 'cbioportal-frontend-commons'; import { StudyViewPageStore } from 'pages/studyView/StudyViewPageStore'; +import { structVarFilterQueryToOql } from 'pages/studyView/StructVarUtils'; import classNames from 'classnames'; import { StudyViewPageTabKeyEnum } from 'pages/studyView/StudyViewPageTabs'; import { @@ -72,6 +71,7 @@ export interface IUserSelectionsProps { ) => void; updateCustomChartFilter: (uniqueKey: string, values: string[]) => void; removeGeneFilter: (uniqueKey: string, oql: string) => void; + removeStructVarFilter: (uniqueKey: string, oql: string) => void; updateGenomicDataIntervalFilter: ( uniqueKey: string, values: DataFilterValue[] @@ -364,7 +364,7 @@ export default class UserSelections extends React.Component< _.reduce( this.props.filter.geneFilters || [], (acc, geneFilter) => { - const uniqueKey = getUniqueKeyFromMolecularProfileIds( + const uniqueKey = getUniqueKeyFromGeneFilterMolecularProfileIds( geneFilter.molecularProfileIds ); const chartMeta = this.props.attributesMetaSet[uniqueKey]; @@ -397,6 +397,43 @@ export default class UserSelections extends React.Component< components ); + _.reduce( + this.props.filter.structuralVariantFilters || [], + (acc, structuralVariantFilter) => { + const uniqueKey = getUniqueKeyFromMolecularProfileIds( + structuralVariantFilter.molecularProfileIds, + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ); + const chartMeta = this.props.attributesMetaSet[uniqueKey]; + if (chartMeta) { + acc.push( +
+ { + return ( + 1} + /> + ); + } + )} + operation={'and'} + group={false} + /> +
+ ); + } + return acc; + }, + components + ); + if (!_.isEmpty(this.props.filter.genomicProfiles)) { components.push(
@@ -802,35 +839,18 @@ export default class UserSelections extends React.Component< chartMeta: ChartMeta & { chartType: ChartType } ): JSX.Element[] { return geneQueries.map(geneQuery => { - let color = DEFAULT_NA_COLOR; - let displayGeneSymbol = geneQuery.hugoGeneSymbol; - switch (chartMeta.chartType) { - case ChartTypeEnum.MUTATED_GENES_TABLE: - color = MUT_COLOR_MISSENSE; - break; - case ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE: - color = STRUCTURAL_VARIANT_COLOR; - break; - case ChartTypeEnum.CNA_GENES_TABLE: { - if (geneQuery.alterations.length === 1) { - let tagColor = getCNAColorByAlteration( - geneQuery.alterations[0] - ); - if (tagColor) { - color = tagColor; - } - } - break; - } - } + const color = this.getQueryFilterPillTagColor( + chartMeta, + geneQuery.alterations + ); return ( } onDelete={() => @@ -845,6 +865,61 @@ export default class UserSelections extends React.Component< }); } + private groupedStructVarQueries( + structVarFilterQueries: StructuralVariantFilterQuery[], + chartMeta: ChartMeta & { chartType: ChartType } + ): JSX.Element[] { + return structVarFilterQueries.map(structVarQuery => { + let color = this.getQueryFilterPillTagColor(chartMeta); + const gene1 = structVarQuery.gene1Query.hugoSymbol || ''; + const gene2 = structVarQuery.gene2Query.hugoSymbol || ''; + let displayLabel = `${gene1}::${gene2}`; + return ( + + } + onDelete={() => + this.props.removeStructVarFilter( + chartMeta.uniqueKey, + structVarFilterQueryToOql(structVarQuery) + ) + } + store={this.props.store} + /> + ); + }); + } + + private getQueryFilterPillTagColor( + chartMeta: ChartMeta & { chartType: ChartType }, + cnaAlterations?: string[] + ): string { + switch (chartMeta.chartType) { + case ChartTypeEnum.MUTATED_GENES_TABLE: + return MUT_COLOR_MISSENSE; + case ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE: + case ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE: + return STRUCTURAL_VARIANT_COLOR; + case ChartTypeEnum.CNA_GENES_TABLE: { + if (cnaAlterations && cnaAlterations.length === 1) { + const tagGColor = getCNAColorByAlteration( + cnaAlterations[0] + ); + return tagGColor || DEFAULT_NA_COLOR; + } + return DEFAULT_NA_COLOR; + } + default: + return DEFAULT_NA_COLOR; + } + } + private groupedGenomicProfiles(genomicProfiles: string[]): JSX.Element[] { return genomicProfiles.map(profile => { return ( diff --git a/src/pages/studyView/charts/ChartContainer.tsx b/src/pages/studyView/charts/ChartContainer.tsx index dee3530c596..b8556a31044 100644 --- a/src/pages/studyView/charts/ChartContainer.tsx +++ b/src/pages/studyView/charts/ChartContainer.tsx @@ -23,7 +23,6 @@ import SurvivalChart, { import BarChart from './barChart/BarChart'; import { ChartMeta, - ChartMetaDataTypeEnum, ChartType, ClinicalDataCountSummary, DataBin, @@ -31,7 +30,6 @@ import { getRangeFromDataBins, getTableHeightByDimension, getWidthByDimension, - logScalePossible, MutationCountVsCnaYBinsMin, NumericalGroupComparisonType, } from '../StudyViewUtils'; @@ -75,6 +73,12 @@ import { PatientSurvival } from 'shared/model/PatientSurvival'; import ClinicalEventTypeCountTable, { ClinicalEventTypeCountColumnKey, } from 'pages/studyView/table/ClinicalEventTypeCountTable'; +import { + StructuralVariantMultiSelectionTable, + StructVarMultiSelectionTableColumn, + StructVarMultiSelectionTableColumnKey, +} from 'pages/studyView/table/StructuralVariantMultiSelectionTable'; +import { StructVarGenePair } from 'pages/studyView/StructVarUtils'; export interface AbstractChart { toSVGDOMNode: () => Element; @@ -100,6 +104,7 @@ const COMPARISON_CHART_TYPES: ChartType[] = [ ChartTypeEnum.PATIENT_TREATMENT_GROUPS_TABLE, ChartTypeEnum.PATIENT_TREATMENT_TARGET_TABLE, ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE, + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE, ]; export interface IChartContainerProps { @@ -152,6 +157,8 @@ export interface IChartContainerProps { selectedGenes?: any; cancerGenes: number[]; onGeneSelect?: any; + selectedStructuralVariants?: StructVarGenePair[]; + onStructuralVariantSelect?: any; isNewlyAdded: (uniqueKey: string) => boolean; cancerGeneFilterEnabled: boolean; filterByCancerGenes?: boolean; @@ -707,6 +714,101 @@ export class ChartContainer extends React.Component { ); }; } + case ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE: { + return () => { + const numColumn: StructVarMultiSelectionTableColumn = { + columnKey: StructVarMultiSelectionTableColumnKey.NUMBER, + }; + if (this.props.store.isGlobalMutationFilterActive) { + numColumn.columnTooltip = ( + + Total number of fusions +
+ This table is filtered based on selections in + the Alteration Filter menu. +
+ ); + } + return ( + + ); + }; + } case ChartTypeEnum.CNA_GENES_TABLE: { return () => { const numColumn: MultiSelectionTableColumn = { diff --git a/src/pages/studyView/studyPageHeader/StudyPageHeader.tsx b/src/pages/studyView/studyPageHeader/StudyPageHeader.tsx index 16184280c09..db22373effb 100644 --- a/src/pages/studyView/studyPageHeader/StudyPageHeader.tsx +++ b/src/pages/studyView/studyPageHeader/StudyPageHeader.tsx @@ -104,6 +104,9 @@ export default class StudyPageHeader extends React.Component< this.props.store.setCustomChartCategoricalFilters } removeGeneFilter={this.props.store.removeGeneFilter} + removeStructVarFilter={ + this.props.store.removeStructVarFilter + } removeCustomSelectionFilter={ this.props.store.removeCustomSelectFilter } diff --git a/src/pages/studyView/table/FixedHeaderTable.tsx b/src/pages/studyView/table/FixedHeaderTable.tsx index 4c1cb4eb4c8..1d9de955bd0 100644 --- a/src/pages/studyView/table/FixedHeaderTable.tsx +++ b/src/pages/studyView/table/FixedHeaderTable.tsx @@ -423,6 +423,7 @@ export default class FixedHeaderTable extends React.Component<