From 47bda20e8239a41a92e0275bb0dbad7179490d5d Mon Sep 17 00:00:00 2001 From: oplantalech Date: Tue, 21 Aug 2018 15:46:40 +0200 Subject: [PATCH] Implement gene set option in plots tab --- package-lock.json | 16 +-- src/pages/resultsView/ResultsViewPageStore.ts | 19 ++- src/pages/resultsView/plots/PlotsTab.tsx | 135 +++++++++++++++--- src/pages/resultsView/plots/PlotsTabUtils.tsx | 59 +++++++- src/shared/cache/GenesetCache.ts | 22 +++ 5 files changed, 221 insertions(+), 30 deletions(-) create mode 100644 src/shared/cache/GenesetCache.ts diff --git a/package-lock.json b/package-lock.json index 88bf0ca93b1..66e62b7ae96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6018,9 +6018,9 @@ "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", "optional": true, "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" } }, "fs.realpath": { @@ -6113,8 +6113,8 @@ "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", "optional": true, "requires": { - "ajv": "^4.9.1", - "har-schema": "^1.0.5" + "ajv": "4.11.8", + "har-schema": "1.0.5" } }, "has-unicode": { @@ -6175,7 +6175,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { - "number-is-nan": "^1.0.0" + "number-is-nan": "1.0.1" } }, "is-typedarray": { @@ -6267,7 +6267,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", "requires": { - "mime-db": "~1.27.0" + "mime-db": "1.27.0" } }, "minimatch": { @@ -6275,7 +6275,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "1.1.7" } }, "minimist": { diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index e9221b446de..37fe5e8c339 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -54,10 +54,16 @@ import GeneMolecularDataCache from "../../shared/cache/GeneMolecularDataCache"; import GenesetMolecularDataCache from "../../shared/cache/GenesetMolecularDataCache"; import GenesetCorrelatedGeneCache from "../../shared/cache/GenesetCorrelatedGeneCache"; import GeneCache from "../../shared/cache/GeneCache"; +import GenesetCache from "../../shared/cache/GenesetCache"; import {IHotspotIndex} from "../../shared/model/CancerHotspots"; import {IOncoKbData} from "../../shared/model/OncoKB"; import {generateQueryVariantId} from "../../shared/lib/OncoKbUtils"; -import {CosmicMutation, AlterationEnrichment, ExpressionEnrichment} from "../../shared/api/generated/CBioPortalAPIInternal"; +import { + CosmicMutation, + AlterationEnrichment, + ExpressionEnrichment, + Geneset +} from "../../shared/api/generated/CBioPortalAPIInternal"; import internalClient from "../../shared/api/cbioportalInternalClientInstance"; import {IndicatorQueryResp} from "../../shared/api/generated/OncoKbAPI"; import {getAlterationString} from "../../shared/lib/CopyNumberUtils"; @@ -1674,6 +1680,13 @@ export class ResultsViewPageStore { } }); + readonly genesets = remoteData({ + invoke: async () => internalClient.fetchGenesetsUsingPOST({genesetIds: this.genesetIds.slice()}), + onResult:(genesets:Geneset[])=>{ + this.genesetCache.addData(genesets); + } + }); + readonly entrezGeneIdToGene = remoteData<{[entrezGeneId:number]:Gene}>({ await: ()=>[this.genes], invoke: ()=>Promise.resolve(_.keyBy(this.genes.result!, gene=>gene.entrezGeneId)) @@ -2454,6 +2467,10 @@ export class ResultsViewPageStore { return new GeneCache(); } + @cached get genesetCache() { + return new GenesetCache(); + } + public numericGeneMolecularDataCache = new MobxPromiseCache<{entrezGeneId:number, molecularProfileId:string}, NumericGeneMolecularData[]>( q=>({ await: ()=>[ diff --git a/src/pages/resultsView/plots/PlotsTab.tsx b/src/pages/resultsView/plots/PlotsTab.tsx index df513101363..48d1fabbead 100644 --- a/src/pages/resultsView/plots/PlotsTab.tsx +++ b/src/pages/resultsView/plots/PlotsTab.tsx @@ -16,7 +16,7 @@ import { getCnaQueries, getMutationQueries, getScatterPlotDownloadData, getBoxPlotDownloadData, getTablePlotDownloadData, mutationRenderPriority, mutationSummaryRenderPriority, MutationSummary, mutationSummaryToAppearance, CNA_STROKE_WIDTH, PLOT_SIDELENGTH, CLIN_ATTR_DATA_TYPE, - sortMolecularProfilesForDisplay, scatterPlotZIndexSortBy + sortMolecularProfilesForDisplay, scatterPlotZIndexSortBy, GENESET_DATA_TYPE } from "./PlotsTabUtils"; import { ClinicalAttribute, MolecularProfile, Mutation, @@ -73,7 +73,9 @@ export enum PlotType { export type AxisMenuSelection = { entrezGeneId?:number; + genesetId?:string; selectedGeneOption?:{value:number, label:string}; // value is entrez id, label is hugo symbol + selectedGenesetOption?:{value:string, label:string}; dataType?:string; dataSourceId?:string; logScale: boolean; @@ -91,6 +93,7 @@ class PlotsTabBoxPlot extends BoxScatterPlot {} const SVG_ID = "plots-tab-plot-svg"; export const SAME_GENE_OPTION_VALUE = "same"; +export const SAME_GENESET_OPTION_VALUE = "same"; @observer export default class PlotsTab extends React.Component { @@ -312,7 +315,37 @@ export default class PlotsTab extends React.Component { set logScale(v:boolean) { this._logScale = v; }, + get genesetId() { + if (this.selectedGenesetOption) { + if (this.selectedGenesetOption.value === SAME_GENESET_OPTION_VALUE) { + return self.horzSelection.genesetId; + } else { + return this.selectedGenesetOption.value; + } + } else { + return undefined; + } + }, + get selectedGenesetOption() { + const genesetOptions = (vertical ? self.vertGenesetOptions : self.horzGenesetOptions.result) || []; + if (this._selectedGenesetOption === undefined && genesetOptions.length) { + // select default if _selectedGenesetOption is undefined and theres defaults to choose from + return genesetOptions[0]; + } else if (vertical && this._selectedGenesetOption && this._selectedGenesetOption.value === SAME_GENESET_OPTION_VALUE && + self.horzSelection.dataType === CLIN_ATTR_DATA_TYPE) { + // if vertical gene set option is "same as horizontal", and horizontal is clinical, then use the actual + // gene set option value instead of "Same gene" option value, because that would be slightly weird UX + return self.horzSelection.selectedGenesetOption; + } else { + // otherwise, return stored value for this variable + return this._selectedGenesetOption; + } + }, + set selectedGenesetOption(o:any) { + this._selectedGenesetOption = o; + }, _selectedGeneOption: undefined, + _selectedGenesetOption: undefined, _dataType: undefined, _dataSourceId: undefined, _logScale: true @@ -459,6 +492,16 @@ export default class PlotsTab extends React.Component { this.horzSelection.selectedGeneOption = option; } + @autobind + private onVerticalAxisGenesetSelect(option:any) { + this.vertSelection.selectedGenesetOption = option; + } + + @autobind + private onHorizontalAxisGenesetSelect(option:any) { + this.horzSelection.selectedGenesetOption = option; + } + public test__selectGeneOption(vertical:boolean, optionValue:any) { // for end to end testing // optionValue is either entrez id or the code for same gene @@ -493,13 +536,36 @@ export default class PlotsTab extends React.Component { // option changes. if its remoteData, theres setTimeout(0)'s in the way and it causes unnecessarily an extra // render which leads to a flash of the loading icon on the screen let sameGeneOption = undefined; - if (this.horzSelection.selectedGeneOption && this.horzSelection.dataType !== CLIN_ATTR_DATA_TYPE) { - // show "Same gene" option as long as horzSelection has a selected option, and horz isnt clinical attribute, bc - // in that case theres no selected gene displayed so its confusing UX to have "Same gene" as an option + if (this.horzSelection.selectedGeneOption && this.horzSelection.dataType !== CLIN_ATTR_DATA_TYPE && this.horzSelection.dataType !== GENESET_DATA_TYPE) { + // show "Same gene" option as long as horzSelection has a selected option, and horz isnt clinical attribute or + // a gene set, bc in that case theres no selected gene displayed so its confusing UX to have "Same gene" as an option sameGeneOption = [{ value: SAME_GENE_OPTION_VALUE, label: `Same gene (${this.horzSelection.selectedGeneOption.label})`}]; } return (sameGeneOption || []).concat((this.horzGeneOptions.result || []) as any[]); - }; + } + + //readonly horzGenesetOptions = this.props.store.genesetIds.map(genesetId=>({ value: genesetId, label: genesetId })); + readonly horzGenesetOptions = remoteData({ + await:()=>[this.props.store.genesets], + invoke:()=>{ + return Promise.resolve( + this.props.store.genesets.result!.map(geneset=>({ value: geneset.genesetId, label: geneset.name })) + ); + } + }); + + @computed get vertGenesetOptions() { + // computed instead of remoteData in order to make the rerender synchronous when the 'Same gene set (GENE SET)' + // option changes. if its remoteData, theres setTimeout(0)'s in the way and it causes unnecessarily an extra + // render which leads to a flash of the loading icon on the screen + let sameGenesetOption = undefined; + if (this.horzSelection.selectedGenesetOption && this.horzSelection.dataType === GENESET_DATA_TYPE) { + // show "Same gene set" option as long as horzSelection has a selected option, and horz is gene set attribute, bc + // in that case theres no selected gene displayed so its confusing UX to have "Same gene" as an option + sameGenesetOption = [{ value: SAME_GENESET_OPTION_VALUE, label: `Same gene set (${this.horzSelection.selectedGenesetOption.label})`}]; + } + return (sameGenesetOption || []).concat((this.horzGenesetOptions.result || []) as {value:string, label:string}[]); + } readonly clinicalAttributeIdToClinicalAttribute = remoteData<{[clinicalAttributeId:string]:ClinicalAttribute}>({ await:()=>[ @@ -515,8 +581,8 @@ export default class PlotsTab extends React.Component { readonly clinicalAttributeOptions = remoteData({ await:()=>[this.props.store.clinicalAttributes], invoke:()=>{ - - let _clinicalAttributes = _.sortBy(this.props.store.clinicalAttributes.result!, + + let _clinicalAttributes = _.sortBy(this.props.store.clinicalAttributes.result!, [(o: any)=>-o.priority, (o: any)=>o.label]).map(attribute=>( { value: attribute.clinicalAttributeId, @@ -547,7 +613,8 @@ export default class PlotsTab extends React.Component { readonly dataTypeOptions = remoteData<{value:string, label:string}[]>({ await:()=>[ this.props.store.nonMutationMolecularProfilesWithData, - this.clinicalAttributeOptions + this.clinicalAttributeOptions, + this.props.store.molecularProfilesInStudies ], invoke:()=>{ const profiles = this.props.store.nonMutationMolecularProfilesWithData.result!; @@ -562,6 +629,17 @@ export default class PlotsTab extends React.Component { dataTypeIds.push(CLIN_ATTR_DATA_TYPE); } + if (this.props.store.molecularProfilesInStudies.result!.length && this.horzGenesetOptions.result && this.horzGenesetOptions.result!.length > 0) { + // add geneset profile to list if the study contains it and the query contains gene sets + this.props.store.molecularProfilesInStudies.result.filter(p=>{ + if (p.molecularAlterationType === AlterationTypeConstants[GENESET_DATA_TYPE]) { + if (dataTypeIds.indexOf(GENESET_DATA_TYPE) === -1) { + dataTypeIds.push(GENESET_DATA_TYPE); + } + } + }); + } + return Promise.resolve( _.sortBy(dataTypeIds, // sort them into display order type=>dataTypeDisplayOrder.indexOf(type) @@ -575,11 +653,11 @@ export default class PlotsTab extends React.Component { readonly dataTypeToDataSourceOptions = remoteData<{[dataType:string]:{value:string, label:string}[]}>({ await:()=>[ - this.props.store.nonMutationMolecularProfilesWithData, + this.props.store.molecularProfileIdToMolecularProfile, this.clinicalAttributeOptions ], invoke:()=>{ - const profiles = this.props.store.nonMutationMolecularProfilesWithData.result!; + const profiles = this.props.store.molecularProfileIdToMolecularProfile.result!; const map = _.mapValues( _.groupBy(profiles, profile=>profile.molecularAlterationType), // create a map from profile type to list of profiles of that type profilesOfType=>( @@ -637,6 +715,14 @@ export default class PlotsTab extends React.Component { this.horzSelection.selectedGeneOption = vertOption; this.vertSelection.selectedGeneOption = horzOption; } + + // only swap gene sets if vertSelection is not set to "Same gene set" + if (!this.vertSelection.selectedGenesetOption || (this.vertSelection.selectedGenesetOption.value.toString() !== SAME_GENESET_OPTION_VALUE)) { + const horzOption = this.horzSelection.selectedGenesetOption; + const vertOption = this.vertSelection.selectedGenesetOption; + this.horzSelection.selectedGenesetOption = vertOption; + this.vertSelection.selectedGenesetOption = horzOption; + } } @computed get bothAxesMolecularProfile() { @@ -706,7 +792,8 @@ export default class PlotsTab extends React.Component { this.props.store.entrezGeneIdToGene, this.props.store.clinicalDataCache, this.props.store.numericGeneMolecularDataCache, - this.props.store.studyToMutationMolecularProfile + this.props.store.studyToMutationMolecularProfile, + this.props.store.genesetMolecularDataCache ); } @@ -719,7 +806,8 @@ export default class PlotsTab extends React.Component { this.props.store.entrezGeneIdToGene, this.props.store.clinicalDataCache, this.props.store.numericGeneMolecularDataCache, - this.props.store.studyToMutationMolecularProfile + this.props.store.studyToMutationMolecularProfile, + this.props.store.genesetMolecularDataCache ); } @@ -918,7 +1006,7 @@ export default class PlotsTab extends React.Component { /> Apply Log Scale )} -
+ {(axisSelection.dataType !== GENESET_DATA_TYPE) && (
{ options={this.horzGeneOptions.isComplete ? (vertical ? this.vertGeneOptions : this.horzGeneOptions.result) : []} clearable={false} searchable={false} - disabled={axisSelection.dataType === CLIN_ATTR_DATA_TYPE} + disabled={axisSelection.dataType === CLIN_ATTR_DATA_TYPE || axisSelection.dataType === GENESET_DATA_TYPE} />
-
+
)} + {(axisSelection.dataType === GENESET_DATA_TYPE) && (
+ +
+ +
+
)} ); @@ -1327,4 +1430,4 @@ export default class PlotsTab extends React.Component { ); } -} \ No newline at end of file +} diff --git a/src/pages/resultsView/plots/PlotsTabUtils.tsx b/src/pages/resultsView/plots/PlotsTabUtils.tsx index f8caf41213f..310df9a4059 100644 --- a/src/pages/resultsView/plots/PlotsTabUtils.tsx +++ b/src/pages/resultsView/plots/PlotsTabUtils.tsx @@ -28,17 +28,22 @@ import {AlterationTypeConstants, AnnotatedMutation, AnnotatedNumericGeneMolecula import numeral from "numeral"; import {getUniqueSampleKeyToCategories} from "../../../shared/components/plots/TablePlotUtils"; import {getJitterForCase} from "../../../shared/components/plots/PlotUtils"; +import GenesetMolecularDataCache from "../../../shared/cache/GenesetMolecularDataCache"; +import {GenesetMolecularData} from "../../../shared/api/generated/CBioPortalAPIInternal"; + export const CLIN_ATTR_DATA_TYPE = "clinical_attribute"; +export const GENESET_DATA_TYPE = "GENESET_SCORE"; export const dataTypeToDisplayType:{[s:string]:string} = { "COPY_NUMBER_ALTERATION": "Copy Number", "MRNA_EXPRESSION": "mRNA", + [GENESET_DATA_TYPE]:"Gene Sets", "PROTEIN_LEVEL": "Protein Level", "METHYLATION": "DNA Methylation", [CLIN_ATTR_DATA_TYPE]:"Clinical Attribute" }; -export const dataTypeDisplayOrder = [CLIN_ATTR_DATA_TYPE, "MRNA_EXPRESSION", "COPY_NUMBER_ALTERATION", "PROTEIN_LEVEL", "METHYLATION"]; +export const dataTypeDisplayOrder = [CLIN_ATTR_DATA_TYPE, "MRNA_EXPRESSION", GENESET_DATA_TYPE, "COPY_NUMBER_ALTERATION", "PROTEIN_LEVEL", "METHYLATION"]; export function sortMolecularProfilesForDisplay(profiles:MolecularProfile[]) { if (!profiles.length) { return []; @@ -451,6 +456,37 @@ function makeAxisDataPromise_Molecular( }); } +function makeAxisDataPromise_Geneset( + genesetId:string, + molecularProfileId:string, + genesetMolecularDataCachePromise:MobxPromise, + molecularProfileIdToMolecularProfile:MobxPromise<{[molecularProfileId:string]:MolecularProfile}> +):MobxPromise { + return remoteData({ + await:()=>[genesetMolecularDataCachePromise, molecularProfileIdToMolecularProfile], + invoke: async () => { + const profile = molecularProfileIdToMolecularProfile.result![molecularProfileId]; + // const isDiscreteCna = (profile.molecularAlterationType === AlterationTypeConstants.COPY_NUMBER_ALTERATION + // && profile.datatype === "DISCRETE"); + const makeRequest = true; + await genesetMolecularDataCachePromise.result!.getPromise( + {genesetId, molecularProfileId}, makeRequest); + const data:GenesetMolecularData[] = genesetMolecularDataCachePromise.result!.get({molecularProfileId, genesetId})!.data!; + return Promise.resolve({ + data: data.map(d=>{ + const value = d.value; + return { + uniqueSampleKey: d.uniqueSampleKey, + value: Number(value) + }; + }), + genesetId: genesetId, + datatype: "number" + }); + } + }); +} + export function makeAxisDataPromise( selection:AxisMenuSelection, clinicalAttributeIdToClinicalAttribute:MobxPromise<{[clinicalAttributeId:string]:ClinicalAttribute}>, @@ -459,7 +495,8 @@ export function makeAxisDataPromise( entrezGeneIdToGene:MobxPromise<{[entrezGeneId:number]:Gene}>, clinicalDataCache:MobxPromiseCache, numericGeneMolecularDataCache:MobxPromiseCache<{entrezGeneId:number, molecularProfileId:string}, NumericGeneMolecularData[]>, - studyToMutationMolecularProfile: MobxPromise<{[studyId: string]: MolecularProfile}> + studyToMutationMolecularProfile: MobxPromise<{[studyId: string]: MolecularProfile}>, + genesetMolecularDataCachePromise: MobxPromise ):MobxPromise { let ret:MobxPromise = remoteData(()=>new Promise(()=>0)); // always isPending @@ -470,6 +507,13 @@ export function makeAxisDataPromise( ret = makeAxisDataPromise_Clinical(attribute, clinicalDataCache, patientKeyToSamples, studyToMutationMolecularProfile); } break; + case GENESET_DATA_TYPE: + if (selection.genesetId !== undefined && selection.dataSourceId !== undefined) { + ret = makeAxisDataPromise_Geneset( + selection.genesetId, selection.dataSourceId, genesetMolecularDataCachePromise, + molecularProfileIdToMolecularProfile); + } + break; default: // molecular profile if (selection.entrezGeneId !== undefined && selection.dataSourceId !== undefined) { @@ -498,6 +542,7 @@ export function getAxisLabel( clinicalAttributeIdToClinicalAttribute:{[clinicalAttributeId:string]:ClinicalAttribute} ) { let ret = ""; + const profile = molecularProfileIdToMolecularProfile[selection.dataSourceId!]; switch (selection.dataType) { case CLIN_ATTR_DATA_TYPE: const attribute = clinicalAttributeIdToClinicalAttribute[selection.dataSourceId!]; @@ -505,9 +550,13 @@ export function getAxisLabel( ret = attribute.displayName; } break; + case GENESET_DATA_TYPE: + if (profile && selection.genesetId !== undefined) { + ret = `${selection.genesetId}: ${profile.name}`; + } + break; default: // molecular profile - const profile = molecularProfileIdToMolecularProfile[selection.dataSourceId!]; if (profile && selection.entrezGeneId !== undefined) { ret = `${entrezGeneIdToGene[selection.entrezGeneId].hugoGeneSymbol}: ${profile.name}`; } @@ -960,7 +1009,7 @@ export function makeScatterPlotData( molecularProfileIds:string[], data:AnnotatedNumericGeneMolecularData[] } -):IScatterPlotData[] +):IScatterPlotData[]; export function makeScatterPlotData( horzData: INumberAxisData|IStringAxisData, @@ -1214,4 +1263,4 @@ export function getTablePlotDownloadData( } const header = ["Sample Id", horzLabel, vertLabel]; return header.join("\t")+"\n"+dataRows.join("\n"); -} \ No newline at end of file +} diff --git a/src/shared/cache/GenesetCache.ts b/src/shared/cache/GenesetCache.ts new file mode 100644 index 00000000000..2e1a31547d1 --- /dev/null +++ b/src/shared/cache/GenesetCache.ts @@ -0,0 +1,22 @@ +import LazyMobXCache from "../lib/LazyMobXCache"; +import {Geneset} from "../api/generated/CBioPortalAPIInternal"; +import internalClient from "../api/cbioportalInternalClientInstance"; + +type Query = { + genesetId:string; +}; + +function key(o:{genesetId:string}) { + return o.genesetId.toUpperCase(); +} + +async function fetch(queries:Query[]) { + return internalClient.fetchGenesetsUsingPOST({genesetIds: queries.map(q=>q.genesetId.toUpperCase())}); +} + +export default class GenesetCache extends LazyMobXCache { + + constructor() { + super(key, key, fetch); + } +}