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 0000645dbbe..7d97a62d758 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI-docs.json @@ -5901,6 +5901,65 @@ }, "title": "ServerStatusMessage" }, + "StructVarFilterQuery": { + "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": "StructVarFilterQuery" + }, + "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": { @@ -5985,6 +6044,12 @@ "sampleTreatmentTargetFilters": { "$ref": "#/definitions/AndedSampleTreatmentFilters" }, + "structuralVariantFilters": { + "type": "array", + "items": { + "$ref": "#/definitions/StudyViewStructuralVariantFilter" + } + }, "studyIds": { "type": "array", "items": { @@ -5994,6 +6059,28 @@ }, "title": "StudyViewFilter" }, + "StudyViewStructuralVariantFilter": { + "type": "object", + "properties": { + "molecularProfileIds": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "structVarQueries": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/StructVarFilterQuery" + } + } + } + }, + "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 977145a6f9d..feb83f9b30f 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI.ts +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPI.ts @@ -701,6 +701,36 @@ export type SampleTreatmentRow = { export type ServerStatusMessage = { 'status': string +}; +export type StructVarFilterQuery = { + '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 @@ -737,8 +767,17 @@ export type StudyViewFilter = { 'sampleTreatmentTargetFilters': AndedSampleTreatmentFilters + 'structuralVariantFilters': Array < StudyViewStructuralVariantFilter > + 'studyIds': Array < string > +}; +export type StudyViewStructuralVariantFilter = { + 'molecularProfileIds': Array < string > + + 'structVarQueries': Array < Array < StructVarFilterQuery > + > + }; 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 411de1b1332..a2a909e322b 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal-docs.json @@ -2454,6 +2454,43 @@ } } }, + "/structuralvariant-counts/fetch": { + "post": { + "tags": [ + "Study View" + ], + "summary": "Fetch structural variant genes by study view filter", + "operationId": "fetchStructuralVariantCountsUsingPOST", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "studyViewFilter", + "description": "Study view filter", + "required": true, + "schema": { + "$ref": "#/definitions/StudyViewFilter" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/AlterationCountByStructuralVariant" + } + } + } + } + } + }, "/structuralvariant-genes/fetch": { "post": { "tags": [ @@ -3360,9 +3397,22 @@ "type": "integer", "format": "int32" }, + "entrezGeneIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, "hugoGeneSymbol": { "type": "string" }, + "hugoGeneSymbols": { + "type": "array", + "items": { + "type": "string" + } + }, "matchingGenePanelIds": { "type": "array", "uniqueItems": true, @@ -3384,10 +3434,68 @@ "totalCount": { "type": "integer", "format": "int32" + }, + "uniqueEventKey": { + "type": "string" } }, "title": "AlterationCountByGene" }, + "AlterationCountByStructuralVariant": { + "type": "object", + "properties": { + "entrezGeneIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "gene1EntrezGeneId": { + "type": "integer", + "format": "int32" + }, + "gene1HugoGeneSymbol": { + "type": "string" + }, + "gene2EntrezGeneId": { + "type": "integer", + "format": "int32" + }, + "gene2HugoGeneSymbol": { + "type": "string" + }, + "hugoGeneSymbols": { + "type": "array", + "items": { + "type": "string" + } + }, + "matchingGenePanelIds": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "numberOfAlteredCases": { + "type": "integer", + "format": "int32" + }, + "numberOfProfiledCases": { + "type": "integer", + "format": "int32" + }, + "totalCount": { + "type": "integer", + "format": "int32" + }, + "uniqueEventKey": { + "type": "string" + } + }, + "title": "AlterationCountByStructuralVariant" + }, "AlterationEnrichment": { "type": "object", "required": [ @@ -4048,9 +4156,22 @@ "type": "integer", "format": "int32" }, + "entrezGeneIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, "hugoGeneSymbol": { "type": "string" }, + "hugoGeneSymbols": { + "type": "array", + "items": { + "type": "string" + } + }, "matchingGenePanelIds": { "type": "array", "uniqueItems": true, @@ -4072,6 +4193,9 @@ "totalCount": { "type": "integer", "format": "int32" + }, + "uniqueEventKey": { + "type": "string" } }, "title": "CopyNumberCountByGene" @@ -5368,6 +5492,45 @@ }, "title": "SampleTreatmentFilter" }, + "StructVarFilterQuery": { + "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": "StructVarFilterQuery" + }, "StructuralVariant": { "type": "object", "properties": { @@ -5686,6 +5849,12 @@ "sampleTreatmentTargetFilters": { "$ref": "#/definitions/AndedSampleTreatmentFilters" }, + "structuralVariantFilters": { + "type": "array", + "items": { + "$ref": "#/definitions/StudyViewStructuralVariantFilter" + } + }, "studyIds": { "type": "array", "items": { @@ -5695,6 +5864,28 @@ }, "title": "StudyViewFilter" }, + "StudyViewStructuralVariantFilter": { + "type": "object", + "properties": { + "molecularProfileIds": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "structVarQueries": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/StructVarFilterQuery" + } + } + } + }, + "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 88891df0938..f9526a93546 100644 --- a/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts +++ b/packages/cbioportal-ts-api-client/src/generated/CBioPortalAPIInternal.ts @@ -4,8 +4,12 @@ type CallbackHandler = (err: any, res ? : request.Response) => void; export type AlterationCountByGene = { 'entrezGeneId': number + 'entrezGeneIds': Array < number > + 'hugoGeneSymbol': string + 'hugoGeneSymbols': Array < string > + 'matchingGenePanelIds': Array < string > 'numberOfAlteredCases': number @@ -16,6 +20,32 @@ export type AlterationCountByGene = { 'totalCount': number + 'uniqueEventKey': string + +}; +export type AlterationCountByStructuralVariant = { + 'entrezGeneIds': Array < number > + + 'gene1EntrezGeneId': number + + 'gene1HugoGeneSymbol': string + + 'gene2EntrezGeneId': number + + 'gene2HugoGeneSymbol': string + + 'hugoGeneSymbols': Array < string > + + 'matchingGenePanelIds': Array < string > + + 'numberOfAlteredCases': number + + 'numberOfProfiledCases': number + + 'totalCount': number + + 'uniqueEventKey': string + }; export type AlterationEnrichment = { 'counts': Array < CountSummary > @@ -306,8 +336,12 @@ export type CopyNumberCountByGene = { 'entrezGeneId': number + 'entrezGeneIds': Array < number > + 'hugoGeneSymbol': string + 'hugoGeneSymbols': Array < string > + 'matchingGenePanelIds': Array < string > 'numberOfAlteredCases': number @@ -318,6 +352,8 @@ export type CopyNumberCountByGene = { 'totalCount': number + 'uniqueEventKey': string + }; export type CopyNumberCountIdentifier = { 'alteration': number @@ -879,6 +915,28 @@ export type SampleTreatmentFilter = { 'treatment': string +}; +export type StructVarFilterQuery = { + 'gene1Query': StructuralVariantGeneSubQuery + + 'gene2Query': StructuralVariantGeneSubQuery + + 'includeDriver': boolean + + 'includeGermline': boolean + + 'includeSomatic': boolean + + 'includeUnknownOncogenicity': boolean + + 'includeUnknownStatus': boolean + + 'includeUnknownTier': boolean + + 'includeVUS': boolean + + 'tiersBooleanMap': {} + }; export type StructuralVariant = { 'annotation': string @@ -1039,8 +1097,17 @@ export type StudyViewFilter = { 'sampleTreatmentTargetFilters': AndedSampleTreatmentFilters + 'structuralVariantFilters': Array < StudyViewStructuralVariantFilter > + 'studyIds': Array < string > +}; +export type StudyViewStructuralVariantFilter = { + 'molecularProfileIds': Array < string > + + 'structVarQueries': Array < Array < StructVarFilterQuery > + > + }; export type VariantCount = { 'entrezGeneId': number @@ -5805,6 +5872,83 @@ export default class CBioPortalAPIInternal { return response.body; }); }; + fetchStructuralVariantCountsUsingPOSTURL(parameters: { + 'studyViewFilter': StudyViewFilter, + $queryParameters ? : any + }): string { + let queryParameters: any = {}; + let path = '/structuralvariant-counts/fetch'; + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + let keys = Object.keys(queryParameters); + return this.domain + path + (keys.length > 0 ? '?' + (keys.map(key => key + '=' + encodeURIComponent(queryParameters[key])).join('&')) : ''); + }; + + /** + * Fetch structural variant genes by study view filter + * @method + * @name CBioPortalAPIInternal#fetchStructuralVariantCountsUsingPOST + * @param {} studyViewFilter - Study view filter + */ + fetchStructuralVariantCountsUsingPOSTWithHttpInfo(parameters: { + 'studyViewFilter': StudyViewFilter, + $queryParameters ? : any, + $domain ? : string + }): Promise < request.Response > { + const domain = parameters.$domain ? parameters.$domain : this.domain; + const errorHandlers = this.errorHandlers; + const request = this.request; + let path = '/structuralvariant-counts/fetch'; + let body: any; + let queryParameters: any = {}; + let headers: any = {}; + let form: any = {}; + return new Promise(function(resolve, reject) { + headers['Accept'] = 'application/json'; + headers['Content-Type'] = 'application/json'; + + if (parameters['studyViewFilter'] !== undefined) { + body = parameters['studyViewFilter']; + } + + if (parameters['studyViewFilter'] === undefined) { + reject(new Error('Missing required parameter: studyViewFilter')); + return; + } + + if (parameters.$queryParameters) { + Object.keys(parameters.$queryParameters).forEach(function(parameterName) { + var parameter = parameters.$queryParameters[parameterName]; + queryParameters[parameterName] = parameter; + }); + } + + request('POST', domain + path, body, headers, queryParameters, form, reject, resolve, errorHandlers); + + }); + }; + + /** + * Fetch structural variant genes by study view filter + * @method + * @name CBioPortalAPIInternal#fetchStructuralVariantCountsUsingPOST + * @param {} studyViewFilter - Study view filter + */ + fetchStructuralVariantCountsUsingPOST(parameters: { + 'studyViewFilter': StudyViewFilter, + $queryParameters ? : any, + $domain ? : string + }): Promise < Array < AlterationCountByStructuralVariant > + > { + return this.fetchStructuralVariantCountsUsingPOSTWithHttpInfo(parameters).then(function(response: request.Response) { + return response.body; + }); + }; fetchStructuralVariantGenesUsingPOSTURL(parameters: { 'studyViewFilter': StudyViewFilter, $queryParameters ? : any diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index b779eb12a0f..63ea305b29b 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -54,8 +54,6 @@ import { } from 'mobx'; import { getProteinPositionFromProteinChange, - IHotspotIndex, - indexHotspotsData, IOncoKbData, } from 'cbioportal-utils'; import { 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 e7eb538bc98..a767168e240 100644 --- a/src/pages/studyView/StudyViewPageStore.ts +++ b/src/pages/studyView/StudyViewPageStore.ts @@ -59,7 +59,9 @@ import { SampleIdentifier, SampleMolecularIdentifier, SampleTreatmentRow, + StructVarFilterQuery, StudyViewFilter, + StudyViewStructuralVariantFilter, } from 'cbioportal-ts-api-client'; import { fetchCopyNumberSegmentsForSamples, @@ -137,6 +139,7 @@ import { MolecularProfileOption, MUTATION_COUNT_PLOT_DOMAIN, NumericalGroupComparisonType, + oqlQueryToGene1Gene2Representation, pickNewColorForClinicData, RectangleBounds, shouldShowChart, @@ -144,10 +147,14 @@ import { SPECIAL_CHARTS, SpecialChartsUniqueKeyEnum, statusFilterActive, + structVarFilterQueryFromOql, + structVarFilterQueryToOql, + StructVarGene1Gene2, StudyWithSamples, submitToPage, updateCustomIntervalFilter, transformSampleDataToSelectedSampleClinicalData, + updateStructuralVariantQuery, } from './StudyViewUtils'; import MobxPromise from 'mobxpromise'; import { SingleGeneQuery } from 'shared/lib/oql/oql-parser'; @@ -157,7 +164,12 @@ import { updateGeneQuery, } from 'pages/studyView/StudyViewUtils'; import { generateDownloadFilenamePrefixByStudies } from 'shared/lib/FilenameUtils'; -import { unparseOQLQueryLine } from 'shared/lib/oql/oqlfilter'; +import { + convertToGeneAGeneBRepresentation, + parseOQLQuery, + queryContainsStructVarAlteration, + 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,6 +279,7 @@ 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'; type ChartUniqueKey = string; type ResourceId = string; @@ -375,6 +388,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', @@ -1916,6 +1940,11 @@ export class StudyViewPageStore GeneFilterQuery[][] >(); + @observable.ref private _structVarFilterSet = observable.map< + string, + StructVarFilterQuery[][] + >(); + // 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. @@ -2018,6 +2047,17 @@ export class StudyViewPageStore 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( @@ -2332,6 +2372,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 @@ -2342,6 +2383,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 { @@ -2362,6 +2407,13 @@ export class StudyViewPageStore ); } + @computed get filterStructVarsTableByCancerGenes(): boolean { + return ( + this.oncokbCancerGeneFilterEnabled && + this._filterStructVarsTableByCancerGenes + ); + } + @computed get filterCNAGenesTableByCancerGenes(): boolean { return ( this.oncokbCancerGeneFilterEnabled && @@ -2510,6 +2562,26 @@ export class StudyViewPageStore .join(' '); } + @action.bound + onCheckStructuralVariant( + gene1hugoGeneSymbol: string | undefined, + gene2HugoGeneSymbol: string | undefined + ): void { + if ( + gene1hugoGeneSymbol !== undefined || + gene2HugoGeneSymbol !== undefined + ) { + this.geneQueries = updateStructuralVariantQuery( + this.geneQueries, + gene1hugoGeneSymbol, + gene2HugoGeneSymbol + ); + this.geneQueryStr = this.geneQueries + .map(query => unparseOQLQueryLine(query)) + .join(' '); + } + } + @action.bound isSharedCustomData(chartId: string): boolean { return this.customChartSet.has(chartId) @@ -2518,7 +2590,23 @@ 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(): StructVarGene1Gene2[] { + return _(this.geneQueries) + .filter(query => queryContainsStructVarAlteration(query)) + .map(query => oqlQueryToGene1Gene2Representation(query)) + .flatten() + .compact() // Remove falsy elements. + .uniqWith( + (repr1, repr2) => + repr1.gene1 === repr2.gene2 && repr1.gene2 === repr2.gene2 + ) // Remove duplicate elements. + .value(); } @action.bound @@ -2593,6 +2681,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(); @@ -2974,6 +3063,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: StructVarFilterQuery[][] = _.map( + seletedStructVarRows, + structVarRowKeys => + _.map(structVarRowKeys, structVarRowKey => + structVarFilterQueryFromOql( + 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, @@ -3057,11 +3179,50 @@ export class StudyViewPageStore } } + @action.bound + removeStructVarFilter(chartUniqueKey: string, toBeRemoved: string): void { + const oqlQuery = parseOQLQuery(toBeRemoved + ';')[0]; + const gene1Gene2Str = convertToGeneAGeneBRepresentation(oqlQuery)[0]; + const [ + gene1HugoSymbol, + gene2HugoSymbol, + ]: string[] = gene1Gene2Str.split('::'); + let structVarFilters: StructVarFilterQuery[][] = + toJS(this._structVarFilterSet.get(chartUniqueKey)) || []; + structVarFilters = _.reduce( + structVarFilters, + (acc, next) => { + const newGroup = next.filter( + structVarFilterQuery => + structVarFilterQuery.gene1Query.hugoSymbol !== + gene1HugoSymbol && + structVarFilterQuery.gene2Query.hugoSymbol !== + gene2HugoSymbol + ); + if (newGroup.length > 0) { + acc.push(newGroup); + } + return acc; + }, + [] as StructVarFilterQuery[][] + ); + 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) || []; @@ -3232,6 +3393,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; @@ -3315,6 +3477,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: @@ -3598,6 +3762,18 @@ export class StudyViewPageStore }); } + @computed get structVarFilters(): StudyViewStructuralVariantFilter[] { + return _.map(this._structVarFilterSet.toJSON(), ([key, value]) => { + return { + molecularProfileIds: getMolecularProfileIdsFromUniqueKey( + key, + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ), + structVarQueries: value, + }; + }); + } + @computed get genomicDataIntervalFilters(): GenomicDataFilter[] { return Array.from(this._genomicDataIntervalFilterSet.values()); } @@ -3634,6 +3810,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); } @@ -3762,6 +3942,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 @@ -6115,13 +6303,14 @@ export class StudyViewPageStore } if (!_.isEmpty(this.structuralVariantProfiles.result)) { - const uniqueKey = getUniqueKeyFromMolecularProfileIds( + let 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', @@ -6131,6 +6320,23 @@ export class StudyViewPageStore renderWhenDataChange: true, description: '', }; + uniqueKey = getUniqueKeyFromMolecularProfileIds( + this.structuralVariantProfiles.result.map( + p => p.molecularProfileId + ), + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ); + _chartMetaSet[uniqueKey] = { + uniqueKey, + dataType: ChartMetaDataTypeEnum.GENOMIC, + patientAttribute: false, + displayName: 'Structural Variants', + priority: getDefaultPriorityByUniqueKey( + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ), + renderWhenDataChange: true, + description: '', + }; } if (!_.isEmpty(this.cnaProfiles.result)) { @@ -6140,7 +6346,7 @@ export class StudyViewPageStore ) ); _chartMetaSet[uniqueKey] = { - uniqueKey: uniqueKey, + uniqueKey, dataType: ChartMetaDataTypeEnum.GENOMIC, patientAttribute: false, displayName: 'CNA Genes', @@ -6187,6 +6393,15 @@ export class StudyViewPageStore ); } + // TODO activate feature flag, + @computed + get isStructVarFeatureFlagEnabled() { + return true; + // return this.appStore.featureFlagStore.has( + // FeatureFlagEnum.STUDY_VIEW_STRUCT_VAR_TABLE + // ); + } + @computed get showSettingRestoreMsg(): boolean { return !!( @@ -6661,6 +6876,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 @@ -6812,10 +7033,11 @@ export class StudyViewPageStore } } if (!_.isEmpty(this.structuralVariantProfiles.result)) { - const uniqueKey = getUniqueKeyFromMolecularProfileIds( + let uniqueKey = getUniqueKeyFromMolecularProfileIds( this.structuralVariantProfiles.result.map( - profile => profile.molecularProfileId - ) + p => p.molecularProfileId + ), + ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE ); const structuralVariantGeneMeta = _.find( this.chartMetaSet, @@ -6840,6 +7062,37 @@ export class StudyViewPageStore ChartTypeEnum.STRUCTURAL_VARIANT_GENES_TABLE ] ); + if (this.isStructVarFeatureFlagEnabled) { + uniqueKey = 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( + uniqueKey, + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ); + this.chartsDimension.set( + uniqueKey, + STUDY_VIEW_CONFIG.layout.dimensions[ + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE + ] + ); + } } if (!_.isEmpty(this.cnaProfiles.result)) { const uniqueKey = getUniqueKeyFromMolecularProfileIds( @@ -7847,6 +8100,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: + 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 @@ -9048,11 +9380,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, @@ -9070,7 +9410,8 @@ export class StudyViewPageStore ); count = ret[key]; } - ret[uniqueKey] = count; + ret[structVarGenesUniqueKey] = count; + ret[structVarUniqueKey] = count; } if (!_.isEmpty(this.cnaProfiles.result)) { @@ -9531,7 +9872,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..9e9b51cfe68 100644 --- a/src/pages/studyView/StudyViewUtils.spec.tsx +++ b/src/pages/studyView/StudyViewUtils.spec.tsx @@ -57,6 +57,7 @@ import { makePatientToClinicalAnalysisGroup, mergeClinicalDataCollection, needAdditionShiftForLogScaleBarChart, + oqlQueryToGene1Gene2Representation, pickClinicalDataColors, shouldShowChart, showOriginStudiesInSummaryDescription, @@ -67,6 +68,7 @@ import { updateCustomIntervalFilter, updateGeneQuery, updateSavedUserPreferenceChartIds, + updateStructuralVariantQuery, } from 'pages/studyView/StudyViewUtils'; import { CancerStudy, @@ -99,6 +101,7 @@ 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'; describe('StudyViewUtils', () => { const emptyStudyViewFilter: StudyViewFilter = { @@ -180,6 +183,76 @@ describe('StudyViewUtils', () => { }); }); + describe('updateStructuralVariantQuery', () => { + it.each([ + [ + [ + { + gene: 'A', + alterations: [ + { alteration_type: 'downstream_fusion', gene: 'B' }, + ], + }, + ] as SingleGeneQuery[], + 'A', + 'B', + [] as SingleGeneQuery[], + ], + [ + [ + { + gene: 'B', + alterations: [ + { alteration_type: 'upstream_fusion', gene: 'A' }, + ], + }, + ] as SingleGeneQuery[], + 'A', + 'B', + [] as SingleGeneQuery[], + ], + [ + [] as SingleGeneQuery[], + 'A', + 'B', + [ + { + gene: 'A', + alterations: [ + { alteration_type: 'downstream_fusion', gene: 'B' }, + ], + }, + ] as SingleGeneQuery[], + ], + [ + [{ gene: 'X' }] as SingleGeneQuery[], + 'A', + 'B', + [ + { gene: 'X' }, + { + gene: 'A', + alterations: [ + { alteration_type: 'downstream_fusion', gene: 'B' }, + ], + }, + ] as SingleGeneQuery[], + ], + ])( + 'updates queries', + (input, selectedGene1, selectedGene2, expected) => { + assert.deepEqual( + updateStructuralVariantQuery( + input, + selectedGene1, + selectedGene2 + ), + expected + ); + } + ); + }); + describe('getVirtualStudyDescription', () => { let studies = [ { @@ -4993,6 +5066,7 @@ describe('StudyViewUtils', () => { ); }); }); + describe('Create object for group comparison custom numerical data', () => { it('transform sample data to clinical data ', function() { const sampleData = [ @@ -5081,4 +5155,72 @@ describe('StudyViewUtils', () => { assert.deepEqual(result, outputObject); }); }); + + describe('oqlQueryToGene1Gene2Representation', () => { + it.each([ + [ + { + gene: 'A', + alterations: [ + { alteration_type: 'downstream_fusion', gene: 'B' }, + ], + } as SingleGeneQuery, + [{ gene1: 'A', gene2: 'B' }], + ], + [ + { + gene: 'A', + alterations: [ + { alteration_type: 'upstream_fusion', gene: 'B' }, + ], + } as SingleGeneQuery, + [{ gene1: 'B', gene2: 'A' }], + ], + [ + { + gene: 'A', + alterations: [ + { alteration_type: 'downstream_fusion', gene: 'B' }, + { + alteration_type: 'upstream_fusion', + gene: 'B', + }, + ], + } as SingleGeneQuery, + [ + { gene1: 'A', gene2: 'B' }, + { gene1: 'B', gene2: 'A' }, + ], + ], + [ + { + gene: 'A', + alterations: [ + { alteration_type: 'downstream_fusion', gene: '*' }, + ], + } as SingleGeneQuery, + [{ gene1: 'A', gene2: undefined }], + ], + [ + { + gene: 'A', + alterations: [ + { + alteration_type: 'downstream_fusion', + gene: undefined, + }, + ], + } as SingleGeneQuery, + [{ gene1: 'A', gene2: 'null' }], + ], + ])( + 'should convert oql query to gene1/gene2 representation', + (oqlQuery, expected) => { + assert.deepEqual( + expected, + oqlQueryToGene1Gene2Representation(oqlQuery) + ); + } + ); + }); }); diff --git a/src/pages/studyView/StudyViewUtils.tsx b/src/pages/studyView/StudyViewUtils.tsx index d7f7f2b7395..0c6dc105fd8 100644 --- a/src/pages/studyView/StudyViewUtils.tsx +++ b/src/pages/studyView/StudyViewUtils.tsx @@ -1,5 +1,9 @@ import _ from 'lodash'; -import { SingleGeneQuery } from 'shared/lib/oql/oql-parser'; +import { + FUSIONCommandDownstream, + FUSIONCommandUpstream, + SingleGeneQuery, +} from 'shared/lib/oql/oql-parser'; import { BinsGeneratorConfig, CancerStudy, @@ -26,18 +30,20 @@ import { PatientIdentifier, Sample, SampleIdentifier, + StructuralVariantGeneSubQuery, + StructVarFilterQuery, 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 +90,7 @@ 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'; // Cannot use ClinicalDataTypeEnum here for the strong type. The model in the type is not strongly typed export enum ClinicalDataTypeEnum { @@ -106,6 +113,8 @@ export type ClinicalDataType = 'SAMPLE' | 'PATIENT'; export type ChartType = keyof typeof ChartTypeEnum; +const STRUCT_VAR_TABLE_KEY_SUFFIX = '_STRUCT_VARS'; + export enum SpecialChartsUniqueKeyEnum { CUSTOM_SELECT = 'CUSTOM_SELECT', SELECTED_COMPARISON_GROUPS = 'SELECTED_COMPARISON_GROUPS', @@ -514,13 +523,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({ @@ -531,6 +545,68 @@ export function updateGeneQuery( return updatedQueries; } +// This function acts as a toggle. If present in 'geneQueries', the query +// is removed. If absent, a gene1/gene2 query is added. +export function updateStructuralVariantQuery( + geneQueries: SingleGeneQuery[], + selectedGene1: string | undefined, + selectedGene2: string | undefined +): SingleGeneQuery[] { + if (!selectedGene1 && !selectedGene2) { + return geneQueries; + } + + // TODO replace with STRUCTVARAnyGeneStr (does not work somehow...) + const gene1 = selectedGene1 || '*'; + const gene2 = selectedGene2 || '*'; + + // Remove any SV alteration with the same genes + // (both upstream and downstream fusions are evaluated). + const updatedQueries = _.filter(geneQueries, (query: SingleGeneQuery) => { + const isStructVar = queryContainsStructVarAlteration(query); + const isUpstreamMatch = + query.gene === gene1 && + query.alterations && + !!_.find( + query.alterations, + (alt: FUSIONCommandUpstream | FUSIONCommandDownstream) => + alt.alteration_type === 'downstream_fusion' && + alt.gene === gene2 + ); + const isDownstreamMatch = + query.gene === gene2 && + query.alterations && + !!_.find( + query.alterations, + (alt: FUSIONCommandUpstream | FUSIONCommandDownstream) => + alt.alteration_type === 'upstream_fusion' && + alt.gene === gene1 + ); + return !isStructVar || (!isDownstreamMatch && !isUpstreamMatch); + }); + + const representativeGene = selectedGene1 ? selectedGene1 : selectedGene2; + const otherGene = representativeGene === selectedGene1 ? gene2 : gene1; + const alterationType = + representativeGene === selectedGene1 + ? 'downstream_fusion' + : 'upstream_fusion'; + + if (updatedQueries.length === geneQueries.length) { + updatedQueries.push({ + gene: representativeGene!, + alterations: [ + { + alteration_type: alterationType, + gene: otherGene, + modifiers: [], + }, + ], + }); + } + return updatedQueries; +} + function translateSpecialText(text: string | undefined): string { if (!text) { return ''; @@ -778,9 +854,14 @@ export function getGenericAssayChartUniqueKey( const UNIQUE_KEY_SEPARATOR = ':'; export function getUniqueKeyFromMolecularProfileIds( - molecularProfileIds: string[] + molecularProfileIds: string[], + chartType?: ChartTypeEnum ) { - return _.sortBy(molecularProfileIds).join(UNIQUE_KEY_SEPARATOR); + let ids = _.sortBy(molecularProfileIds); + if (chartType === ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE) { + ids = _.map(ids, id => id + STRUCT_VAR_TABLE_KEY_SUFFIX); + } + return ids.join(UNIQUE_KEY_SEPARATOR); } export function calculateSampleCountForClinicalEventTypeCountTable( @@ -801,8 +882,17 @@ export function calculateSampleCountForClinicalEventTypeCountTable( return sampleCount; } -export function getMolecularProfileIdsFromUniqueKey(uniqueKey: string) { - return uniqueKey.split(UNIQUE_KEY_SEPARATOR); +export function getMolecularProfileIdsFromUniqueKey( + uniqueKey: string, + chartType?: ChartTypeEnum +) { + let molecularProfileIds = uniqueKey.split(UNIQUE_KEY_SEPARATOR); + if (chartType === ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE) { + molecularProfileIds = _.map(molecularProfileIds, molecularProfileId => + molecularProfileId.replace(STRUCT_VAR_TABLE_KEY_SUFFIX, '') + ); + } + return molecularProfileIds; } export function getCurrentDate() { @@ -978,6 +1068,7 @@ export function isFiltered( _.isEmpty(filter) || (_.isEmpty(filter.clinicalDataFilters) && _.isEmpty(filter.geneFilters) && + _.isEmpty(filter.structuralVariantFilters) && _.isEmpty(filter.genomicProfiles) && _.isEmpty(filter.genomicDataFilters) && _.isEmpty(filter.genericAssayDataFilters) && @@ -2354,6 +2445,7 @@ export function getSamplesByExcludingFiltersOnChart( let updatedFilter: StudyViewFilter = { clinicalDataFilters: filter.clinicalDataFilters, geneFilters: filter.geneFilters, + structuralVariantFilters: filter.structuralVariantFilters, } as any; let _sampleIdentifiers = _.reduce( @@ -3305,6 +3397,12 @@ export function geneFilterQueryToOql(query: GeneFilterQuery): string { : query.hugoGeneSymbol; } +export function structVarFilterQueryToOql(query: StructVarFilterQuery): string { + const gene1 = query.gene1Query.hugoSymbol || ''; + const gene2 = query.gene2Query.hugoSymbol || ''; + return gene1 ? `${gene1}: FUSION::${gene2}` : `${gene2}: ${gene1}::FUSION`; +} + export function geneFilterQueryFromOql( oql: string, includeDriver?: boolean, @@ -3348,6 +3446,76 @@ export function geneFilterQueryFromOql( }; } +export function structVarFilterQueryFromOql( + gene1Gene2Representation: string, + includeDriver?: boolean, + includeVUS?: boolean, + includeUnknownOncogenicity?: boolean, + selectedDriverTiers?: { [tier: string]: boolean }, + includeUnknownDriverTier?: boolean, + includeGermline?: boolean, + includeSomatic?: boolean, + includeUnknownStatus?: boolean +): StructVarFilterQuery { + 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 === '*') { + stringStructuralVariantGeneSubQuery = { + specialValue: 'ANY_GENE', + }; + } else { + stringStructuralVariantGeneSubQuery = { + hugoSymbol: hugoGeneSybol, + }; + } + return (stringStructuralVariantGeneSubQuery as unknown) as StructuralVariantGeneSubQuery; +} + export function ensureBackwardCompatibilityOfFilters( filters: Partial ) { @@ -3401,25 +3569,26 @@ export function buildSelectedDriverTiersMap( export const FilterIconMessage: React.FunctionComponent<{ chartType: ChartType; - geneFilterQuery: GeneFilterQuery; -}> = observer(({ chartType, geneFilterQuery }) => { + annotatedFilterQuery: GeneFilterQuery | StructVarFilterQuery; +}> = 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 && @@ -3429,31 +3598,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 = ''; @@ -3878,3 +4047,32 @@ export function transformSampleDataToSelectedSampleClinicalData( .filter(item => item.uniqueSampleKey !== undefined); return clinicalDataSamples; } + +export type StructVarGene1Gene2 = { + gene1: string | undefined; + gene2: string | undefined; +}; + +export function oqlQueryToGene1Gene2Representation( + query: SingleGeneQuery +): StructVarGene1Gene2[] | undefined { + if (!queryContainsStructVarAlteration) { + return undefined; + } + const representativeGene = query.gene; + const alterations = query.alterations as ( + | FUSIONCommandUpstream + | FUSIONCommandDownstream + )[]; + return _.map(alterations, alt => { + const otherGene = + alt.gene == undefined + ? 'null' + : alt.gene === '*' + ? undefined + : alt.gene; + return alt.alteration_type === 'downstream_fusion' + ? { gene1: representativeGene, gene2: otherGene } + : { gene1: otherGene, gene2: representativeGene }; + }); +} diff --git a/src/pages/studyView/UserSelections.tsx b/src/pages/studyView/UserSelections.tsx index 64414b8e1a5..57fbfd4617a 100644 --- a/src/pages/studyView/UserSelections.tsx +++ b/src/pages/studyView/UserSelections.tsx @@ -4,15 +4,7 @@ import { observer } from 'mobx-react'; import { computed, makeObservable } from 'mobx'; import styles from './styles.module.scss'; import { - DataFilterValue, - AndedPatientTreatmentFilters, - AndedSampleTreatmentFilters, - GeneFilterQuery, - PatientTreatmentFilter, - SampleTreatmentFilter, - ClinicalDataFilter, -} from 'cbioportal-ts-api-client'; -import { + ChartMeta, DataType, FilterIconMessage, ChartType, @@ -20,9 +12,6 @@ import { getGenericAssayChartUniqueKey, updateCustomIntervalFilter, SpecialChartsUniqueKeyEnum, -} from 'pages/studyView/StudyViewUtils'; -import { - ChartMeta, geneFilterQueryToOql, getCNAColorByAlteration, getPatientIdentifiers, @@ -30,6 +19,7 @@ import { getUniqueKeyFromMolecularProfileIds, intervalFiltersDisplayValue, StudyViewFilterWithSampleIdentifierFilters, + structVarFilterQueryToOql, } from 'pages/studyView/StudyViewUtils'; import { PillTag } from '../../shared/components/PillTag/PillTag'; import { GroupLogic } from './filters/groupLogic/GroupLogic'; @@ -41,14 +31,22 @@ import { getSampleIdentifiers, StudyViewComparisonGroup, } from '../groupComparison/GroupComparisonUtils'; -import { DefaultTooltip } from 'cbioportal-frontend-commons'; import { OredPatientTreatmentFilters, OredSampleTreatmentFilters, + DataFilterValue, + AndedPatientTreatmentFilters, + AndedSampleTreatmentFilters, + GeneFilterQuery, + PatientTreatmentFilter, + SampleTreatmentFilter, + ClinicalDataFilter, + StructVarFilterQuery, } 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 { DataFilter } from 'cbioportal-ts-api-client'; @@ -66,6 +64,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[] @@ -372,6 +371,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(
@@ -767,35 +803,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={() => @@ -809,6 +828,60 @@ export default class UserSelections extends React.Component< }); } + private groupedStructVarQueries( + structVarFilterQueries: StructVarFilterQuery[], + 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) + ) + } + /> + ); + }); + } + + 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 68f14d29435..ef82ed62c2d 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'; @@ -74,6 +72,11 @@ import { PatientSurvival } from 'shared/model/PatientSurvival'; import ClinicalEventTypeCountTable, { ClinicalEventTypeCountColumnKey, } from 'pages/studyView/table/ClinicalEventTypeCountTable'; +import { + StructuralVariantMultiSelectionTable, + StructVarMultiSelectionTableColumn, + StructVarMultiSelectionTableColumnKey, +} from 'pages/studyView/table/StructuralVariantMultiSelectionTable'; export interface AbstractChart { toSVGDOMNode: () => Element; @@ -99,6 +102,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 { @@ -151,6 +155,8 @@ export interface IChartContainerProps { selectedGenes?: any; cancerGenes: number[]; onGeneSelect?: any; + selectedStructuralVariants?: any; + onStructuralVariantSelect?: any; isNewlyAdded: (uniqueKey: string) => boolean; cancerGeneFilterEnabled: boolean; filterByCancerGenes?: boolean; @@ -680,6 +686,94 @@ 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 630efc8d30f..eb49bcb1ebf 100644 --- a/src/pages/studyView/studyPageHeader/StudyPageHeader.tsx +++ b/src/pages/studyView/studyPageHeader/StudyPageHeader.tsx @@ -90,6 +90,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/MultiSelectionTable.tsx b/src/pages/studyView/table/MultiSelectionTable.tsx index f74c37ab0ee..9221378b307 100644 --- a/src/pages/studyView/table/MultiSelectionTable.tsx +++ b/src/pages/studyView/table/MultiSelectionTable.tsx @@ -70,28 +70,31 @@ export type MultiSelectionTableColumn = { columnTooltip?: JSX.Element; }; -export type MultiSelectionTableProps = { +export type BaseMultiSelectionTableProps = { tableType: FreqColumnTypeEnum; - promise: MobxPromise; width: number; height: number; filters: string[][]; onSubmitSelection: (value: string[][]) => void; onChangeSelectedRows: (rowsKeys: string[]) => void; - extraButtons?: IFixedHeaderTableProps< - MultiSelectionTableRow - >['extraButtons']; selectedRowsKeys: string[]; - onGeneSelect: (hugoGeneSymbol: string) => void; - selectedGenes: string[]; cancerGeneFilterEnabled?: boolean; genePanelCache: MobxPromiseCache<{ genePanelId: string }, GenePanel>; filterByCancerGenes: boolean; onChangeCancerGeneFilter: (filtered: boolean) => void; alterationFilterEnabled?: boolean; filterAlterations?: boolean; +}; + +export type MultiSelectionTableProps = BaseMultiSelectionTableProps & { defaultSortBy: MultiSelectionTableColumnKey; + extraButtons?: IFixedHeaderTableProps< + MultiSelectionTableRow + >['extraButtons']; + selectedGenes: string[]; + onGeneSelect: (hugoGeneSymbol: string) => void; columns: MultiSelectionTableColumn[]; + promise: MobxPromise; }; const DEFAULT_COLUMN_WIDTH_RATIO: { diff --git a/src/pages/studyView/table/StructVarCell.tsx b/src/pages/studyView/table/StructVarCell.tsx new file mode 100644 index 00000000000..2621b55dffa --- /dev/null +++ b/src/pages/studyView/table/StructVarCell.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import styles from './tables.module.scss'; +import classnames from 'classnames'; +import { EllipsisTextTooltip } from 'cbioportal-frontend-commons'; +import { FreqColumnTypeEnum } from '../TableUtils'; +import { action, computed, makeObservable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Else, If, Then } from 'react-if'; +import _ from 'lodash'; +import { StructVarGene1Gene2 } from 'pages/studyView/StudyViewUtils'; + +export type IStructVarCellProps = { + tableType: FreqColumnTypeEnum; + uniqueRowId: string; + selectedStructVars: StructVarGene1Gene2[]; + label?: string; + gene1HugoSymbol?: string; + gene2HugoSymbol?: string; + isCancerGene: boolean; + oncokbAnnotated: boolean; + isOncogene: boolean; + isTumorSuppressorGene: boolean; + hoveredStructVarRowIds: string[]; + onStructVarSelect?: ( + gene1HugoSymbol: string | undefined, + gene2HugoSymbol: string | undefined + ) => void; + onGeneHovered?: (uniqueRowId: string, isHovered: boolean) => void; +}; + +@observer +export class StructVarCell extends React.Component { + constructor(props: IStructVarCellProps) { + super(props); + makeObservable(this); + } + + @action.bound + onHover(isVisible: boolean) { + if (this.props.onGeneHovered) { + this.props.onGeneHovered(this.props.uniqueRowId, isVisible); + } + } + + @action.bound + private onStructVarSelect() { + if (this.props.onStructVarSelect) { + this.props.onStructVarSelect!( + this.props.gene1HugoSymbol, + this.props.gene2HugoSymbol + ); + } + } + + @computed + get showCheckbox() { + return this.props.hoveredStructVarRowIds.includes( + this.props.uniqueRowId + ); + } + + @computed + get isCheckBoxChecked() { + const gene1Str = this.props.gene1HugoSymbol; + const gene2Str = this.props.gene2HugoSymbol; + return !!_.find( + this.props.selectedStructVars, + sv => sv.gene1 === gene1Str && sv.gene2 === gene2Str + ); + } + + render() { + return ( +
this.onHover(true)} + onMouseLeave={() => this.onHover(false)} + onClick={this.onStructVarSelect} + > + + + + + + + + + + + + + + + + + + {/*If there is no label defined, add some whitespace so that the*/} + {/*the user can trigger a hover event more.*/} + +   + + + +
+ ); + } +} diff --git a/src/pages/studyView/table/StructuralVariantMultiSelectionTable.tsx b/src/pages/studyView/table/StructuralVariantMultiSelectionTable.tsx new file mode 100644 index 00000000000..11d0f586ff2 --- /dev/null +++ b/src/pages/studyView/table/StructuralVariantMultiSelectionTable.tsx @@ -0,0 +1,782 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import _ from 'lodash'; +import FixedHeaderTable, { IFixedHeaderTableProps } from './FixedHeaderTable'; +import { action, computed, makeObservable, observable } from 'mobx'; +import autobind from 'autobind-decorator'; +import { + Column, + SortDirection, +} from '../../../shared/components/lazyMobXTable/LazyMobXTable'; +import { StudyViewGenePanelModal } from './StudyViewGenePanelModal'; +import { + correctColumnWidth, + correctMargin, + getFixedHeaderNumberCellMargin, + getFixedHeaderTableMaxLengthStringPixel, + getFrequencyStr, + StructVarGene1Gene2, +} from 'pages/studyView/StudyViewUtils'; +import { OncokbCancerStructVar } from 'pages/studyView/StudyViewPageStore'; +import { + FreqColumnTypeEnum, + getCancerGeneToggledOverlay, + getFreqColumnRender, + getTooltip, + SelectionOperatorEnum, +} from 'pages/studyView/TableUtils'; +import LabeledCheckbox from 'shared/components/labeledCheckbox/LabeledCheckbox'; +import styles from 'pages/studyView/table/tables.module.scss'; +import { + stringListToIndexSet, + stringListToSet, +} from 'cbioportal-frontend-commons'; +import ifNotDefined from 'shared/lib/ifNotDefined'; +import { TableHeaderCellFilterIcon } from 'pages/studyView/table/TableHeaderCellFilterIcon'; +import { StructVarCell } from 'pages/studyView/table/StructVarCell'; +import { BaseMultiSelectionTableProps } from 'pages/studyView/table/MultiSelectionTable'; +import MobxPromise from 'mobxpromise'; + +export type StructVarMultiSelectionTableRow = OncokbCancerStructVar & { + label1: string; + label2: string; + matchingGenePanelIds: Array; + numberOfAlteredCases: number; + numberOfProfiledCases: number; + totalCount: number; + alteration?: number; + cytoband?: string; + uniqueKey: string; +}; + +export enum StructVarMultiSelectionTableColumnKey { + STRUCTVAR_SELECT = 'StructVarSelect', + GENE1 = 'Gene 1', + GENE2 = 'Gene 2', + NUMBER_STRUCTURAL_VARIANTS = '# SV', + NUMBER = '#', + FREQ = 'Freq', +} + +export type StructVarMultiSelectionTableColumn = { + columnKey: StructVarMultiSelectionTableColumnKey; + columnWidthRatio?: number; + columnTooltip?: JSX.Element; +}; + +export type StructVarMultiSelectionTableProps = BaseMultiSelectionTableProps & { + onStructuralVariantSelect: ( + gene1HugoGeneSymbol: string | undefined, + gene2HugoGeneSymbol: string | undefined + ) => void; + selectedStructVars: StructVarGene1Gene2[]; + defaultSortBy: StructVarMultiSelectionTableColumnKey; + extraButtons?: IFixedHeaderTableProps< + StructVarMultiSelectionTableRow + >['extraButtons']; + columns: StructVarMultiSelectionTableColumn[]; + promise: MobxPromise; +}; + +const DEFAULT_COLUMN_WIDTH_RATIO: { + [key in StructVarMultiSelectionTableColumnKey]: number; +} = { + [StructVarMultiSelectionTableColumnKey.STRUCTVAR_SELECT]: 0.07, + [StructVarMultiSelectionTableColumnKey.GENE1]: 0.3, + [StructVarMultiSelectionTableColumnKey.GENE2]: 0.3, + [StructVarMultiSelectionTableColumnKey.NUMBER_STRUCTURAL_VARIANTS]: 0.08, + [StructVarMultiSelectionTableColumnKey.NUMBER]: 0.2, + [StructVarMultiSelectionTableColumnKey.FREQ]: 0.3, +}; + +class MultiSelectionTableComponent extends FixedHeaderTable< + StructVarMultiSelectionTableRow +> {} + +@observer +export class StructuralVariantMultiSelectionTable extends React.Component< + StructVarMultiSelectionTableProps, + {} +> { + @observable protected sortBy: StructVarMultiSelectionTableColumnKey; + @observable private sortDirection: SortDirection; + @observable private modalSettings: { + modalOpen: boolean; + modalPanelName: string; + } = { + modalOpen: false, + modalPanelName: '', + }; + + @observable private hoveredStructVarTableRowIds: string[] = []; + + public static defaultProps = { + cancerGeneFilterEnabled: false, + }; + + constructor(props: StructVarMultiSelectionTableProps, context: any) { + super(props, context); + makeObservable(this); + this.sortBy = this.props.defaultSortBy; + } + + getDefaultColumnDefinition = ( + columnKey: StructVarMultiSelectionTableColumnKey, + columnWidth: number, + cellMargin: number + ) => { + const defaults: { + [key in StructVarMultiSelectionTableColumnKey]: Column< + StructVarMultiSelectionTableRow + >; + } = { + [StructVarMultiSelectionTableColumnKey.STRUCTVAR_SELECT]: { + name: columnKey, + headerRender: () => { + return ( + + + + ); + }, + render: (data: StructVarMultiSelectionTableRow) => { + return ( + + ); + }, + sortBy: (data: StructVarMultiSelectionTableRow) => data.label1, + defaultSortDirection: 'asc' as 'asc', + filter: ( + data: StructVarMultiSelectionTableRow, + filterString: string, + filterStringUpper: string + ) => { + return data.uniqueKey + .toUpperCase() + .includes(filterStringUpper); + }, + width: columnWidth, + }, + [StructVarMultiSelectionTableColumnKey.GENE1]: { + name: columnKey, + headerRender: () => { + return {columnKey}; + }, + render: (data: StructVarMultiSelectionTableRow) => { + return ( + + ); + }, + sortBy: (data: StructVarMultiSelectionTableRow) => data.label1, + defaultSortDirection: 'asc' as 'asc', + filter: ( + data: StructVarMultiSelectionTableRow, + filterString: string, + filterStringUpper: string + ) => { + return data.label1 + .toUpperCase() + .includes(filterStringUpper); + }, + width: columnWidth, + }, + [StructVarMultiSelectionTableColumnKey.GENE2]: { + name: columnKey, + headerRender: () => { + return {columnKey}; + }, + render: (data: StructVarMultiSelectionTableRow) => { + return ( + + ); + }, + sortBy: (data: StructVarMultiSelectionTableRow) => data.label2, + defaultSortDirection: 'asc' as 'asc', + filter: ( + data: StructVarMultiSelectionTableRow, + filterString: string, + filterStringUpper: string + ) => { + return data.label2 + .toUpperCase() + .includes(filterStringUpper); + }, + width: columnWidth, + }, + [StructVarMultiSelectionTableColumnKey.NUMBER]: { + name: columnKey, + tooltip: {getTooltip(this.props.tableType, false)}, + headerRender: () => { + return ( + + {columnKey} + + ); + }, + render: (data: StructVarMultiSelectionTableRow) => ( + this.toggleSelectRow(data.uniqueKey)} + labelProps={{ + style: { + display: 'flex', + justifyContent: 'space-between', + marginLeft: cellMargin, + marginRight: cellMargin, + }, + }} + inputProps={{ + className: styles.autoMarginCheckbox, + }} + > + + {data.numberOfAlteredCases.toLocaleString()} + + + ), + sortBy: (data: StructVarMultiSelectionTableRow) => + data.numberOfAlteredCases, + defaultSortDirection: 'desc' as 'desc', + filter: ( + data: StructVarMultiSelectionTableRow, + filterString: string + ) => { + return _.toString(data.numberOfAlteredCases).includes( + filterString + ); + }, + width: columnWidth, + }, + [StructVarMultiSelectionTableColumnKey.FREQ]: { + name: columnKey, + tooltip: {getTooltip(this.props.tableType, true)}, + headerRender: () => { + return
Freq
; + }, + render: (data: StructVarMultiSelectionTableRow) => { + return getFreqColumnRender( + this.props.tableType, + data.numberOfProfiledCases, + data.numberOfAlteredCases, + data.matchingGenePanelIds || [], + this.toggleModal, + { marginLeft: cellMargin } + ); + }, + sortBy: (data: StructVarMultiSelectionTableRow) => + (data.numberOfAlteredCases / data.numberOfProfiledCases) * + 100, + defaultSortDirection: 'desc' as 'desc', + filter: ( + data: StructVarMultiSelectionTableRow, + filterString: string + ) => { + return _.toString( + getFrequencyStr( + data.numberOfAlteredCases / + data.numberOfProfiledCases + ) + ).includes(filterString); + }, + width: columnWidth, + }, + [StructVarMultiSelectionTableColumnKey.NUMBER_STRUCTURAL_VARIANTS]: { + name: columnKey, + tooltip: Total number of mutations, + headerRender: () => { + return ( +
+ { + StructVarMultiSelectionTableColumnKey.NUMBER_STRUCTURAL_VARIANTS + } +
+ ); + }, + render: (data: StructVarMultiSelectionTableRow) => ( + + {data.totalCount.toLocaleString()} + + ), + sortBy: (data: StructVarMultiSelectionTableRow) => + data.totalCount, + defaultSortDirection: 'desc' as 'desc', + filter: ( + data: StructVarMultiSelectionTableRow, + filterString: string + ) => { + return _.toString(data.totalCount).includes(filterString); + }, + width: columnWidth, + }, + [StructVarMultiSelectionTableColumnKey.NUMBER_STRUCTURAL_VARIANTS]: { + name: columnKey, + tooltip: Total number of structural variants, + headerRender: () => { + return ( +
+ { + StructVarMultiSelectionTableColumnKey.NUMBER_STRUCTURAL_VARIANTS + } +
+ ); + }, + render: (data: StructVarMultiSelectionTableRow) => ( + + {data.totalCount.toLocaleString()} + + ), + sortBy: (data: StructVarMultiSelectionTableRow) => + data.totalCount, + defaultSortDirection: 'desc' as 'desc', + filter: ( + data: StructVarMultiSelectionTableRow, + filterString: string + ) => { + return _.toString(data.totalCount).includes(filterString); + }, + width: columnWidth, + }, + }; + return defaults[columnKey]; + }; + + getDefaultCellMargin = ( + columnKey: StructVarMultiSelectionTableColumnKey, + columnWidth: number + ) => { + const defaults: { + [key in StructVarMultiSelectionTableColumnKey]: number; + } = { + [StructVarMultiSelectionTableColumnKey.GENE1]: 0, + [StructVarMultiSelectionTableColumnKey.GENE2]: 0, + [StructVarMultiSelectionTableColumnKey.STRUCTVAR_SELECT]: 0, + [StructVarMultiSelectionTableColumnKey.NUMBER_STRUCTURAL_VARIANTS]: correctMargin( + getFixedHeaderNumberCellMargin( + columnWidth, + this.totalCountLocaleString + ) + ), + [StructVarMultiSelectionTableColumnKey.NUMBER]: correctMargin( + (columnWidth - + 10 - + (getFixedHeaderTableMaxLengthStringPixel( + this.alteredCasesLocaleString + ) + + 30)) / + 2 + ), + [StructVarMultiSelectionTableColumnKey.FREQ]: correctMargin( + getFixedHeaderNumberCellMargin( + columnWidth, + getFrequencyStr( + _.max( + this.tableData.map( + item => + (item.numberOfAlteredCases! / + item.numberOfProfiledCases!) * + 100 + ) + )! + ) + ) + ), + }; + return defaults[columnKey]; + }; + + @computed + get maxNumberTotalCount() { + return _.max(this.tableData.map(item => item.totalCount)); + } + + @computed + get maxNumberAlteredCasesColumn() { + return _.max(this.tableData!.map(item => item.numberOfAlteredCases)); + } + + @computed + get totalCountLocaleString() { + return this.maxNumberTotalCount === undefined + ? '' + : this.maxNumberTotalCount.toLocaleString(); + } + + @computed + get alteredCasesLocaleString() { + return this.maxNumberAlteredCasesColumn === undefined + ? '' + : this.maxNumberAlteredCasesColumn.toLocaleString(); + } + + @computed + get columnsWidth() { + return _.reduce( + this.props.columns, + (acc, column) => { + acc[column.columnKey] = correctColumnWidth( + (column.columnWidthRatio + ? column.columnWidthRatio + : DEFAULT_COLUMN_WIDTH_RATIO[column.columnKey]) * + this.props.width + ); + return acc; + }, + {} as { [key in StructVarMultiSelectionTableColumnKey]: number } + ); + } + + @computed + get cellMargin() { + return _.reduce( + this.props.columns, + (acc, column) => { + acc[column.columnKey] = this.getDefaultCellMargin( + column.columnKey, + this.columnsWidth[column.columnKey] + ); + return acc; + }, + {} as { [key in StructVarMultiSelectionTableColumnKey]: number } + ); + } + + @computed get tableData() { + return this.isFilteredByCancerGeneList + ? _.filter( + this.props.promise.result, + data => data.gene1IsCancerGene || data.gene2IsCancerGene + ) + : this.props.promise.result || []; + } + + @computed get flattenedFilters() { + return _.flatMap(this.props.filters); + } + + @computed get selectableTableData() { + if (this.flattenedFilters.length === 0) { + return this.tableData; + } + return _.filter( + this.tableData, + data => !this.flattenedFilters.includes(data.uniqueKey) + ); + } + + @computed + get preSelectedRows() { + if (this.flattenedFilters.length === 0) { + return []; + } + const order = stringListToIndexSet(this.flattenedFilters); + return _.chain(this.tableData) + .filter(data => this.flattenedFilters.includes(data.uniqueKey)) + .sortBy(data => + ifNotDefined(order[data.uniqueKey], Number.POSITIVE_INFINITY) + ) + .value(); + } + + @computed + get preSelectedRowsKeys() { + return this.preSelectedRows.map(row => row.uniqueKey); + } + + @computed + get tableColumns() { + return this.props.columns.map(column => { + const columnDefinition = this.getDefaultColumnDefinition( + column.columnKey, + this.columnsWidth[column.columnKey], + this.cellMargin[column.columnKey] + ); + if (column.columnTooltip) { + columnDefinition.tooltip = column.columnTooltip; + } + return columnDefinition; + }); + } + + @action.bound + toggleModal(panelName: string) { + this.modalSettings.modalOpen = !this.modalSettings.modalOpen; + if (!this.modalSettings.modalOpen) { + return; + } + this.modalSettings.modalPanelName = panelName; + } + + @action.bound + closeModal() { + this.modalSettings.modalOpen = !this.modalSettings.modalOpen; + } + + @autobind + toggleCancerGeneFilter(event: any) { + event.stopPropagation(); + this.props.onChangeCancerGeneFilter(!this.props.filterByCancerGenes); + } + + @computed get isFilteredByCancerGeneList() { + return ( + !!this.props.cancerGeneFilterEnabled && + this.props.filterByCancerGenes + ); + } + + @computed get allSelectedRowsKeysSet() { + return stringListToSet([ + ...this.props.selectedRowsKeys, + ...this.preSelectedRowsKeys, + ]); + } + + @autobind + isChecked(uniqueKey: string) { + return !!this.allSelectedRowsKeysSet[uniqueKey]; + } + + @autobind + isDisabled(uniqueKey: string) { + return _.some(this.preSelectedRowsKeys, key => key === uniqueKey); + } + + @action.bound + onStructVarHover(rowId: string, isHovered: boolean) { + if (isHovered) { + this.hoveredStructVarTableRowIds.push(rowId); + } else { + _.pull(this.hoveredStructVarTableRowIds, rowId); + } + } + + @action.bound + toggleSelectRow(uniqueKey: string) { + const record = _.find( + this.props.selectedRowsKeys, + key => key === uniqueKey + ); + if (_.isUndefined(record)) { + this.props.onChangeSelectedRows( + this.props.selectedRowsKeys.concat([uniqueKey]) + ); + } else { + this.props.onChangeSelectedRows( + _.xorBy(this.props.selectedRowsKeys, [record]) + ); + } + } + @observable private _selectionType: SelectionOperatorEnum; + + @action.bound + afterSelectingRows() { + if (this.selectionType === SelectionOperatorEnum.UNION) { + this.props.onSubmitSelection([this.props.selectedRowsKeys]); + } else { + this.props.onSubmitSelection( + this.props.selectedRowsKeys.map(selectedRowsKey => [ + selectedRowsKey, + ]) + ); + } + this.props.onChangeSelectedRows([]); + } + + @computed get selectionType() { + if (this._selectionType) { + return this._selectionType; + } + switch ( + (localStorage.getItem(this.props.tableType) || '').toUpperCase() + ) { + case SelectionOperatorEnum.INTERSECTION: + return SelectionOperatorEnum.INTERSECTION; + case SelectionOperatorEnum.UNION: + return SelectionOperatorEnum.UNION; + default: + return this.props.tableType === FreqColumnTypeEnum.DATA + ? SelectionOperatorEnum.INTERSECTION + : SelectionOperatorEnum.UNION; + } + } + + @action.bound + toggleSelectionOperator() { + const selectionType = this._selectionType || this.selectionType; + if (selectionType === SelectionOperatorEnum.INTERSECTION) { + this._selectionType = SelectionOperatorEnum.UNION; + } else { + this._selectionType = SelectionOperatorEnum.INTERSECTION; + } + localStorage.setItem(this.props.tableType, this.selectionType); + } + + @autobind + isSelectedRow(data: StructVarMultiSelectionTableRow) { + return this.isChecked(data.uniqueKey); + } + + @computed get filterKeyToIndexSet() { + return _.reduce( + this.props.filters, + (acc, next, index) => { + next.forEach(key => { + acc[key] = index; + }); + return acc; + }, + {} as { [id: string]: number } + ); + } + + @autobind + selectedRowClassName(data: StructVarMultiSelectionTableRow) { + const index = this.filterKeyToIndexSet[data.uniqueKey]; + if (index === undefined) { + return this.props.filters.length % 2 === 0 + ? styles.highlightedEvenRow + : styles.highlightedOddRow; + } + return index % 2 === 0 + ? styles.highlightedEvenRow + : styles.highlightedOddRow; + } + + @action.bound + afterSorting( + sortBy: StructVarMultiSelectionTableColumnKey, + sortDirection: SortDirection + ) { + this.sortBy = sortBy; + this.sortDirection = sortDirection; + } + + public render() { + const tableId = `${this.props.tableType}-table`; + return ( +
+ {this.props.promise.isComplete && ( + + )} + {this.props.genePanelCache ? ( + + ) : null} +
+ ); + } +} diff --git a/src/pages/studyView/tabs/SummaryTab.tsx b/src/pages/studyView/tabs/SummaryTab.tsx index 16a3ba93043..e4e46d5ee93 100644 --- a/src/pages/studyView/tabs/SummaryTab.tsx +++ b/src/pages/studyView/tabs/SummaryTab.tsx @@ -389,6 +389,30 @@ export class StudySummaryTab extends React.Component< props.filterAlterations = this.store.isGlobalMutationFilterActive; break; } + case ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE: { + props.filters = this.store.getGeneFiltersByUniqueKey( + chartMeta.uniqueKey + ); + props.promise = this.store.structuralVariantTableRowData; + props.onValueSelection = this.store.addStructVarFilters; + props.onResetSelection = () => + this.store.resetGeneFilter(chartMeta.uniqueKey); + props.selectedStructuralVariants = this.store.selectedStructuralVariants; + props.onStructuralVariantSelect = this.store.onCheckStructuralVariant; + props.title = this.store.getChartTitle( + ChartTypeEnum.STRUCTURAL_VARIANTS_TABLE, + props.title + ); + props.getData = () => + this.store.getStructuralVariantGenesDownloadData(); + props.genePanelCache = this.store.genePanelCache; + props.downloadTypes = ['Data']; + props.filterByCancerGenes = this.store.filterStructVarsTableByCancerGenes; + props.onChangeCancerGeneFilter = this.store.updateStructVarsTableByCancerGenesFilter; + props.alterationFilterEnabled = getServerConfig().skin_show_settings_menu; + props.filterAlterations = this.store.isGlobalMutationFilterActive; + break; + } case ChartTypeEnum.CNA_GENES_TABLE: { props.filters = this.store.getGeneFiltersByUniqueKey( chartMeta.uniqueKey @@ -696,6 +720,7 @@ export class StudySummaryTab extends React.Component< // Then across the study page, there should be only one place to include ChartContainer component. // 2. The maintainer of RGL repo currently not actively accepts pull requests. So we don't know when the // issue will be solved. + return (
diff --git a/src/shared/components/GeneSelectionBox/OQLTextArea.tsx b/src/shared/components/GeneSelectionBox/OQLTextArea.tsx index 9600f6a6c44..b2b8a167481 100644 --- a/src/shared/components/GeneSelectionBox/OQLTextArea.tsx +++ b/src/shared/components/GeneSelectionBox/OQLTextArea.tsx @@ -78,19 +78,19 @@ export default class OQLTextArea extends React.Component< // Need to record the textarea value due to SyntheticEvent restriction due to debounce private currentTextAreaValue = ''; - @observable private _geneQuery = ''; - @computed get geneQuery() { + @observable private _geneQueryStr = ''; + @computed get geneQueryStr() { if (this.queryStore) { return this.queryStore.geneQuery; } else { - return this._geneQuery; + return this._geneQueryStr; } } - set geneQuery(q: string) { + set geneQueryStr(q: string) { if (this.queryStore) { this.queryStore.geneQuery = q; } else { - this._geneQuery = q; + this._geneQueryStr = q; } } @observable private geneQueryIsValid = true; @@ -107,12 +107,12 @@ export default class OQLTextArea extends React.Component< // When the text is empty, it will be skipped from oql and further no validation will be done. // Need to set the geneQuery here if (this.currentTextAreaValue === '') { - this.geneQuery = ''; + this.geneQueryStr = ''; if (this.props.callback) { this.props.callback( getOQL(''), getEmptyGeneValidationResult(), - this.geneQuery + this.geneQueryStr ); } } @@ -125,8 +125,8 @@ export default class OQLTextArea extends React.Component< constructor(props: IGeneSelectionBoxProps) { super(props); makeObservable(this); - this.geneQuery = this.props.inputGeneQuery || ''; - this.queryToBeValidated = this.geneQuery; + this.geneQueryStr = this.props.inputGeneQuery || ''; + this.queryToBeValidated = this.geneQueryStr; if (!this.props.validateInputGeneQuery) { this.skipGenesValidation = true; } @@ -140,13 +140,13 @@ export default class OQLTextArea extends React.Component< inputGeneQuery => { if ( (inputGeneQuery || '').toUpperCase() !== - this.geneQuery.toUpperCase() + this.geneQueryStr.toUpperCase() ) { if (!this.props.validateInputGeneQuery) { this.skipGenesValidation = true; } - this.geneQuery = (inputGeneQuery || '').trim(); - this.queryToBeValidated = this.geneQuery; + this.geneQueryStr = (inputGeneQuery || '').trim(); + this.queryToBeValidated = this.geneQueryStr; } this.updateTextAreaRefValue(); } @@ -168,7 +168,7 @@ export default class OQLTextArea extends React.Component< @action.bound private updateGeneQuery(value: string) { - this.geneQuery = value; + this.geneQueryStr = value; // at the time gene query is updated, the queryToBeValidated should be set to the same this.queryToBeValidated = value; @@ -181,7 +181,7 @@ export default class OQLTextArea extends React.Component< private getTextAreaValue() { if (this.showFullText) { - return this.geneQuery; + return this.geneQueryStr; } else { return this.getFocusOutValue(); } @@ -194,7 +194,7 @@ export default class OQLTextArea extends React.Component< private getFocusOutValue() { return getFocusOutText( - getOQL(this.geneQuery).query.map(query => query.gene) + getOQL(this.geneQueryStr).query.map(query => query.gene) ); } @@ -215,7 +215,7 @@ export default class OQLTextArea extends React.Component< classNames.push(styles.default); break; } - if (!this.geneQuery) { + if (!this.geneQueryStr) { classNames.push(styles.empty); } return classNames; @@ -238,7 +238,7 @@ export default class OQLTextArea extends React.Component< this.geneQueryIsValid = validQuery; if (this.props.callback) { - this.props.callback(oql, validationResult, this.geneQuery); + this.props.callback(oql, validationResult, this.geneQueryStr); } } @@ -260,7 +260,7 @@ export default class OQLTextArea extends React.Component< @bind onChange(event: any) { this.currentTextAreaValue = event.currentTarget.value; - this.geneQuery = this.currentTextAreaValue; + this.geneQueryStr = this.currentTextAreaValue; this.updateQueryToBeValidateDebounce(); } diff --git a/src/shared/featureFlags.ts b/src/shared/featureFlags.ts index 79e68fc6d7d..1e87701342b 100644 --- a/src/shared/featureFlags.ts +++ b/src/shared/featureFlags.ts @@ -1,3 +1,4 @@ export enum FeatureFlagEnum { + STUDY_VIEW_STRUCT_VAR_TABLE = 'STUDY_VIEW_STRUCT_VAR_TABLE', LEFT_TRUNCATION_ADJUSTMENT = 'LEFT_TRUNCATION_ADJUSTMENT', } diff --git a/src/shared/lib/oql/oqlfilter.ts b/src/shared/lib/oql/oqlfilter.ts index ddfb1b4dff9..005cfc6a1f5 100644 --- a/src/shared/lib/oql/oqlfilter.ts +++ b/src/shared/lib/oql/oqlfilter.ts @@ -411,6 +411,88 @@ export function unparseOQLQueryLine(parsed_oql_line: SingleGeneQuery): string { return ret; } +// TODO improve implementation +// TODO add tests !!!!!!!!!!!! +export function convertToGeneAGeneBRepresentation( + parsed_oql_line: SingleGeneQuery +): string[] { + const representativeGene = parsed_oql_line.gene; + if (!queryContainsStructVarAlteration(parsed_oql_line)) { + return [representativeGene]; + } + return _(parsed_oql_line.alterations || []) + .filter(alteration => alterationIsStructVar(alteration)) + .map((alteration: FUSIONCommandDownstream | FUSIONCommandUpstream) => { + const otherGene = + alteration.gene == undefined + ? '-' + : alteration.gene === '*' + ? '' + : alteration.gene; + return alteration.alteration_type === 'upstream_fusion' + ? otherGene + '::' + representativeGene + : representativeGene + '::' + otherGene; + }) + .value(); +} + +// Convert 'GeneA::GeneB' notation to OQL SingleGeneQuery. +export function convertGene1Gene2RepresentationToOQL( + gene1Gene2Representation: string +): SingleGeneQuery { + 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 + ); + } + const representativeGene = gene1HugoSymbol || gene2HugoSymbol; + if (representativeGene === gene1HugoSymbol) { + return { + gene: gene1HugoSymbol, + alterations: [ + { + alteration_type: 'downstream_fusion', + gene: gene2HugoSymbol, + }, + ], + } as SingleGeneQuery; + } else { + return { + gene: gene2HugoSymbol, + alterations: [ + { + alteration_type: 'upstream_fusion', + gene: gene1HugoSymbol, + }, + ], + } as SingleGeneQuery; + } +} + +// TODO add tests !!!!!!!!!!!! +export function queryContainsStructVarAlteration( + parsed_oql_line: SingleGeneQuery +): boolean { + if (!parsed_oql_line.alterations) { + return false; + } + return !!_.find(parsed_oql_line.alterations, alteration => + alterationIsStructVar(alteration) + ); +} + +// TODO add tests !!!!!!!!!!!! export function alterationIsStructVar(alteration: Alteration): boolean { return ( alteration.alteration_type === 'upstream_fusion' ||