diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index 72d1b7be72f..48340be206a 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -4460,16 +4460,19 @@ export class ResultsViewPageStore extends AnalysisStore this.molecularProfilesInStudies, this.selectedMolecularProfiles, this.genesetMolecularProfile, + this.hasVAFData, ], invoke: () => { const MRNA_EXPRESSION = AlterationTypeConstants.MRNA_EXPRESSION; const PROTEIN_LEVEL = AlterationTypeConstants.PROTEIN_LEVEL; const METHYLATION = AlterationTypeConstants.METHYLATION; + const MUTATION_EXTENDED = AlterationTypeConstants.MUTATION_EXTENDED; const selectedMolecularProfileIds = stringListToSet( this.selectedMolecularProfiles.result!.map( profile => profile.molecularProfileId ) ); + const hasVAF = this.hasVAFData.result; const expressionHeatmaps = _.sortBy( _.filter(this.molecularProfilesInStudies.result!, profile => { @@ -4480,7 +4483,10 @@ export class ResultsViewPageStore extends AnalysisStore profile.molecularAlterationType === PROTEIN_LEVEL) && profile.showProfileInAnalysisTab) || - profile.molecularAlterationType === METHYLATION + profile.molecularAlterationType === METHYLATION || + (profile.molecularAlterationType === + MUTATION_EXTENDED && + hasVAF) ); }), profile => { @@ -4496,15 +4502,19 @@ export class ResultsViewPageStore extends AnalysisStore return 1; case METHYLATION: return 2; + case MUTATION_EXTENDED: + return 3; } } else { switch (profile.molecularAlterationType) { case MRNA_EXPRESSION: - return 3; - case PROTEIN_LEVEL: return 4; - case METHYLATION: + case PROTEIN_LEVEL: return 5; + case METHYLATION: + return 6; + case MUTATION_EXTENDED: + return 7; } } } @@ -4518,6 +4528,18 @@ export class ResultsViewPageStore extends AnalysisStore }, }); + readonly hasVAFData = remoteData({ + await: () => [this.mutations], + invoke: () => { + const hasVAF = this.mutations.result!.some( + m => _.isFinite(m.tumorAltCount) && _.isFinite(m.tumorRefCount) + ); + + return Promise.resolve(hasVAF); + }, + default: false, + }); + readonly genesetMolecularProfile = remoteData>({ await: () => [this.selectedMolecularProfiles], invoke: () => { diff --git a/src/shared/components/oncoprint/DataUtils.spec.ts b/src/shared/components/oncoprint/DataUtils.spec.ts index 6ae57beba95..b3b7400a63e 100644 --- a/src/shared/components/oncoprint/DataUtils.spec.ts +++ b/src/shared/components/oncoprint/DataUtils.spec.ts @@ -4,6 +4,7 @@ import { fillGeneticTrackDatum, fillHeatmapTrackDatum, getOncoprintMutationType, + HeatmapCaseDatum, makeGeneticTrackData, selectDisplayValue, } from './DataUtils'; @@ -1554,7 +1555,12 @@ describe('DataUtils', () => { { sampleId: 'sample', studyId: 'study' } as Sample, data ), - { hugo_gene_symbol: 'gene', study_id: 'study', profile_data: 3 } + { + hugo_gene_symbol: 'gene', + study_id: 'study', + na: false, + profile_data: 3, + } ); }); it('removes data points with NaN value', () => { @@ -1570,7 +1576,12 @@ describe('DataUtils', () => { { sampleId: 'sample', studyId: 'study' } as Sample, data ), - { hugo_gene_symbol: 'gene', study_id: 'study', profile_data: 3 } + { + hugo_gene_symbol: 'gene', + study_id: 'study', + na: false, + profile_data: 3, + } ); }); it('throws exception if more than one data given for sample', () => { @@ -1604,7 +1615,12 @@ describe('DataUtils', () => { { patientId: 'patient', studyId: 'study' } as Sample, data ), - { hugo_gene_symbol: 'gene', study_id: 'study', profile_data: 3 } + { + hugo_gene_symbol: 'gene', + study_id: 'study', + na: false, + profile_data: 3, + } ); data = [{ value: 2 }]; @@ -1619,7 +1635,12 @@ describe('DataUtils', () => { { patientId: 'patient', studyId: 'study' } as Sample, data ), - { hugo_gene_symbol: 'gene', study_id: 'study', profile_data: 2 } + { + hugo_gene_symbol: 'gene', + study_id: 'study', + na: false, + profile_data: 2, + } ); data = [{ value: 2 }, { value: 3 }, { value: 4 }]; @@ -1634,7 +1655,12 @@ describe('DataUtils', () => { { patientId: 'patient', studyId: 'study' } as Sample, data ), - { hugo_gene_symbol: 'gene', study_id: 'study', profile_data: 4 } + { + hugo_gene_symbol: 'gene', + study_id: 'study', + na: false, + profile_data: 4, + } ); data = [{ value: -10 }, { value: 3 }, { value: 4 }]; @@ -1652,6 +1678,7 @@ describe('DataUtils', () => { { hugo_gene_symbol: 'gene', study_id: 'study', + na: false, profile_data: -10, } ); @@ -1663,17 +1690,31 @@ describe('DataUtils', () => { 'geneset_id', 'MY_FAVORITE_GENE_SET-3', { sampleId: 'sample', studyId: 'study' } as Sample, - [{ value: 7 }] + [ + { + value: 7, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key', + }, + ] ); assert.deepEqual(partialTrackDatum, { geneset_id: 'MY_FAVORITE_GENE_SET-3', study_id: 'study', + na: false, profile_data: 7, }); }); it('adds thresholdType and category to trackDatum', () => { - let data = [{ value: 8, thresholdType: '>' as '>' }]; + let data: HeatmapCaseDatum[] = [ + { + value: 8, + thresholdType: '>' as '>', + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key', + }, + ]; const partialTrackDatum = {}; fillHeatmapTrackDatum( partialTrackDatum, @@ -1685,6 +1726,7 @@ describe('DataUtils', () => { assert.deepEqual(partialTrackDatum, { entityId: 'GENERIC_ASSAY_ID_1', study_id: 'study', + na: false, profile_data: 8, thresholdType: '>', category: '>8.00', @@ -1692,7 +1734,23 @@ describe('DataUtils', () => { }); it('returns smallest value with ASC sort order', () => { - let data = [{ value: 1 }, { value: 2 }, { value: 3 }]; + let data: HeatmapCaseDatum[] = [ + { + value: 1, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_1', + }, + { + value: 2, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_2', + }, + { + value: 3, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_3', + }, + ]; const partialTrackDatum = {}; fillHeatmapTrackDatum( partialTrackDatum, @@ -1705,12 +1763,29 @@ describe('DataUtils', () => { assert.deepEqual(partialTrackDatum, { entityId: 'GENERIC_ASSAY_ID_1', study_id: 'study', + na: false, profile_data: 1, }); }); it('returns largest value with DESC sort order', () => { - let data = [{ value: 1 }, { value: 2 }, { value: 3 }]; + let data: HeatmapCaseDatum[] = [ + { + value: 1, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_1', + }, + { + value: 2, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_2', + }, + { + value: 3, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_3', + }, + ]; const partialTrackDatum = {}; fillHeatmapTrackDatum( partialTrackDatum, @@ -1723,12 +1798,29 @@ describe('DataUtils', () => { assert.deepEqual(partialTrackDatum, { entityId: 'GENERIC_ASSAY_ID_1', study_id: 'study', + na: false, profile_data: 3, }); }); it('selects non-threshold over threshold data point when values are equal', () => { - let data = [{ value: 1, thresholdType: '>' as '>' }, { value: 1 }]; + let data: HeatmapCaseDatum[] = [ + { + value: 1, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_1', + }, + { + value: 2, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_2', + }, + { + value: 3, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_3', + }, + ]; const partialTrackDatum = {}; fillHeatmapTrackDatum( partialTrackDatum, @@ -1740,12 +1832,24 @@ describe('DataUtils', () => { assert.deepEqual(partialTrackDatum, { entityId: 'GENERIC_ASSAY_ID_1', study_id: 'study', - profile_data: 1, + na: false, + profile_data: 3, }); }); it('handles all NaN-value data points', () => { - let data = [{ value: NaN }, { value: NaN }]; + let data: HeatmapCaseDatum[] = [ + { + value: NaN, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_1', + }, + { + value: NaN, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_2', + }, + ]; const partialTrackDatum = {} as IGenericAssayHeatmapTrackDatum; fillHeatmapTrackDatum( partialTrackDatum, @@ -1759,9 +1863,18 @@ describe('DataUtils', () => { }); it('Prefers largest non-threshold absolute value when no sort order provided', () => { - let data = [ - { value: -10 }, - { value: 10, thresholdType: '>' as '>' }, + let data: HeatmapCaseDatum[] = [ + { + value: -10, + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_1', + }, + { + value: 10, + thresholdType: '>' as '>', + uniquePatientKey: 'patient_key', + uniqueSampleKey: 'sample_key_2', + }, ]; const partialTrackDatum = {}; fillHeatmapTrackDatum( @@ -1774,6 +1887,7 @@ describe('DataUtils', () => { assert.deepEqual(partialTrackDatum, { entityId: 'GENERIC_ASSAY_ID_1', study_id: 'study', + na: false, profile_data: -10, }); }); diff --git a/src/shared/components/oncoprint/DataUtils.ts b/src/shared/components/oncoprint/DataUtils.ts index 10d11c00982..90c31804cac 100644 --- a/src/shared/components/oncoprint/DataUtils.ts +++ b/src/shared/components/oncoprint/DataUtils.ts @@ -30,6 +30,7 @@ import { stringListToIndexSet } from 'cbioportal-frontend-commons'; import { CaseAggregatedData } from 'shared/model/CaseAggregatedData'; import { AnnotatedExtendedAlteration } from 'shared/model/AnnotatedExtendedAlteration'; import { CustomDriverNumericGeneMolecularData } from 'shared/model/CustomDriverNumericGeneMolecularData'; +import { isSampleProfiled } from 'shared/lib/isSampleProfiled'; const cnaDataToString: { [integerCNA: string]: string | undefined } = { '-2': 'homdel', @@ -72,8 +73,10 @@ const protRenderPriority = { low: 0, }; -type HeatmapCaseDatum = { - value: number; +export type HeatmapCaseDatum = { + value: number | null; + uniquePatientKey: string; + uniqueSampleKey: string; thresholdType?: '<' | '>'; }; @@ -393,22 +396,31 @@ export function fillHeatmapTrackDatum< trackDatum[featureKey] = featureId; trackDatum.study_id = case_.studyId; - // remove data points of which `value` is NaN - const dataWithValue = _.filter(data, d => !isNaN(d.value)); + const dataWithValue = _.filter( + data, + d => d && (d.value === null || !isNaN(d.value)) + ); if (!dataWithValue || !dataWithValue.length) { trackDatum.profile_data = null; trackDatum.na = true; } else if (dataWithValue.length === 1) { - trackDatum.profile_data = dataWithValue[0].value; - if (dataWithValue[0].thresholdType) { - trackDatum.thresholdType = dataWithValue[0].thresholdType; - trackDatum.category = - trackDatum.profile_data && trackDatum.thresholdType - ? `${ - trackDatum.thresholdType - }${trackDatum.profile_data.toFixed(2)}` - : undefined; + const singleData = dataWithValue[0]; + if (singleData.value === null) { + trackDatum.profile_data = null; + trackDatum.na = false; + } else { + trackDatum.profile_data = singleData.value; + trackDatum.na = false; + if (singleData.thresholdType) { + trackDatum.thresholdType = singleData.thresholdType; + trackDatum.category = + trackDatum.profile_data && trackDatum.thresholdType + ? `${ + trackDatum.thresholdType + }${trackDatum.profile_data.toFixed(2)}` + : undefined; + } } } else { if (isSample(case_)) { @@ -416,60 +428,73 @@ export function fillHeatmapTrackDatum< 'Unexpectedly received multiple heatmap profile data for one sample' ); } else { - // aggregate samples for this patient by selecting the highest absolute (Z-)score - // default: the most extreme value (pos. or neg.) is shown for data - // sortOrder=ASC: the smallest value is shown for data - // sortOrder=DESC: the largest value is shown for data - let representingDatum; - let bestValue; - switch (sortOrder) { - case 'ASC': - bestValue = _(dataWithValue) - .map((d: HeatmapCaseDatum) => d.value) - .min(); - representingDatum = selectRepresentingDataPoint( - bestValue!, - dataWithValue, - false - ); - break; - case 'DESC': - bestValue = _(dataWithValue) - .map((d: HeatmapCaseDatum) => d.value) - .max(); - representingDatum = selectRepresentingDataPoint( - bestValue!, - dataWithValue, - false - ); - break; - default: - bestValue = _.maxBy(dataWithValue, (d: HeatmapCaseDatum) => - Math.abs(d.value) - )!.value; - representingDatum = selectRepresentingDataPoint( - bestValue, - dataWithValue, - true - ); - break; - } + // Handle multiple data points for patient mode + const dataWithNonNullValues = dataWithValue.filter( + d => d.value !== null + ); - // `data` can contain data points with only NaN values - // this is detected by `representingDatum` to be undefined - // in that case select the first element as representing datum - if (representingDatum === undefined) { - representingDatum = dataWithValue[0]; - } + if (dataWithNonNullValues.length === 0) { + trackDatum.profile_data = null; + trackDatum.na = false; + } else { + // aggregate samples for this patient by selecting the highest absolute (Z-)score + // default: the most extreme value (pos. or neg.) is shown for data + // sortOrder=ASC: the smallest value is shown for data + // sortOrder=DESC: the largest value is shown for data + let representingDatum; + let bestValue; + + switch (sortOrder) { + case 'ASC': + bestValue = _(dataWithNonNullValues) + .map((d: HeatmapCaseDatum) => d.value as number) + .min(); + representingDatum = selectRepresentingDataPoint( + bestValue!, + dataWithNonNullValues, + false + ); + break; + case 'DESC': + bestValue = _(dataWithNonNullValues) + .map((d: HeatmapCaseDatum) => d.value as number) + .max(); + representingDatum = selectRepresentingDataPoint( + bestValue!, + dataWithNonNullValues, + false + ); + break; + default: + bestValue = _.maxBy( + dataWithNonNullValues, + (d: HeatmapCaseDatum) => Math.abs(d.value as number) + )!.value as number; + representingDatum = selectRepresentingDataPoint( + bestValue, + dataWithNonNullValues, + true + ); + break; + } - trackDatum.profile_data = representingDatum!.value; - if (representingDatum!.thresholdType) { - trackDatum.thresholdType = representingDatum!.thresholdType; - trackDatum.category = trackDatum.thresholdType - ? `${ - trackDatum.thresholdType - }${trackDatum.profile_data.toFixed(2)}` - : undefined; + // `data` can contain data points with only NaN values + // this is detected by `representingDatum` to be undefined + // in that case select the first element as representing datum + if (representingDatum === undefined) { + representingDatum = dataWithNonNullValues[0]; + } + + trackDatum.profile_data = representingDatum!.value; + trackDatum.na = false; + if (representingDatum!.thresholdType) { + trackDatum.thresholdType = representingDatum!.thresholdType; + trackDatum.category = trackDatum.thresholdType + ? `${ + trackDatum.thresholdType + }${(trackDatum.profile_data as number).toFixed(2)}` + : undefined; + } } } } @@ -482,7 +507,7 @@ function selectRepresentingDataPoint( useAbsolute: boolean ): HeatmapCaseDatum { const fFilter = useAbsolute - ? (d: HeatmapCaseDatum) => Math.abs(d.value) === bestValue + ? (d: HeatmapCaseDatum) => Math.abs(d.value!) === bestValue : (d: HeatmapCaseDatum) => d.value === bestValue; const selData = _.filter(data, fFilter); const selDataNoTreshold = _.filter( @@ -576,20 +601,16 @@ export function makeHeatmapTrackData< featureKey: K, featureId: T[K], cases: Sample[] | Patient[], - data: { - value: number; - uniquePatientKey: string; - uniqueSampleKey: string; - thresholdType?: '>' | '<'; - }[], + data: HeatmapCaseDatum[], sortOrder?: string ): T[] { if (!cases.length) { return []; } - const sampleData = isSampleList(cases); - let keyToData: { [uniqueKey: string]: { value: number }[] }; + + let keyToData: { [uniqueKey: string]: HeatmapCaseDatum[] }; let ret: T[]; + if (isSampleList(cases)) { keyToData = _.groupBy(data, d => d.uniqueSampleKey); ret = cases.map(c => { diff --git a/src/shared/components/oncoprint/DeltaUtils.ts b/src/shared/components/oncoprint/DeltaUtils.ts index 481a5568d67..c91342e9143 100644 --- a/src/shared/components/oncoprint/DeltaUtils.ts +++ b/src/shared/components/oncoprint/DeltaUtils.ts @@ -608,6 +608,7 @@ function transitionTracks( genesetHeatmap: undefined as undefined | TrackId, heatmap: undefined as undefined | TrackId, heatmap01: undefined as undefined | TrackId, + mutation: undefined as undefined | TrackId, genericAssayHeatmap: ({} as any) as GenericAssayProfileToTrackIdMap, genericAssayCategorical: ({} as any) as GenericAssayProfileToTrackIdMap, }; @@ -629,10 +630,12 @@ function transitionTracks( trackIdForRuleSetSharing.genesetHeatmap = trackSpecKeyToTrackId[prevProps.genesetHeatmapTracks[0].key]; } + if (prevProps.heatmapTracks && prevProps.heatmapTracks.length) { // set rule set to existing track if theres a track let heatmap01; let heatmap; + let mutation; for (const spec of prevProps.heatmapTracks) { if ( heatmap01 === undefined && @@ -640,21 +643,34 @@ function transitionTracks( AlterationTypeConstants.METHYLATION ) { heatmap01 = trackSpecKeyToTrackId[spec.key]; + } else if ( + mutation === undefined && + spec.molecularAlterationType === + AlterationTypeConstants.MUTATION_EXTENDED + ) { + mutation = trackSpecKeyToTrackId[spec.key]; } else if ( heatmap === undefined && spec.molecularAlterationType !== AlterationTypeConstants.METHYLATION && spec.molecularAlterationType !== - AlterationTypeConstants.GENERIC_ASSAY + AlterationTypeConstants.GENERIC_ASSAY && + spec.molecularAlterationType !== + AlterationTypeConstants.MUTATION_EXTENDED ) { heatmap = trackSpecKeyToTrackId[spec.key]; } - if (heatmap01 !== undefined && heatmap !== undefined) { + if ( + heatmap01 !== undefined && + heatmap !== undefined && + mutation !== undefined + ) { break; } } trackIdForRuleSetSharing.heatmap = heatmap; trackIdForRuleSetSharing.heatmap01 = heatmap01; + trackIdForRuleSetSharing.mutation = mutation; } else if (prevProps.genesetHeatmapTracks) { for (const gsTrack of prevProps.genesetHeatmapTracks) { if ( @@ -1480,6 +1496,8 @@ export function transitionHeatmapTrack( heatmap?: TrackId; heatmap01?: TrackId; genericAssayHeatmap?: GenericAssayProfileToTrackIdMap; + // Mutation-specific key to separate VAF from other tracks + mutation?: TrackId; }, expansionParentKey?: string ) { @@ -1547,11 +1565,29 @@ export function transitionHeatmapTrack( nextSpec.molecularAlterationType !== AlterationTypeConstants.GENERIC_ASSAY ) { - let trackIdForRuleSetSharingKey: 'heatmap' | 'heatmap01' = - 'heatmap'; - if (nextSpec.molecularAlterationType === 'METHYLATION') { + let trackIdForRuleSetSharingKey: + | 'heatmap' + | 'heatmap01' + | 'mutation'; + + // Each molecular alteration type gets its own rule set + if ( + nextSpec.molecularAlterationType === + AlterationTypeConstants.MUTATION_EXTENDED + ) { + trackIdForRuleSetSharingKey = 'mutation'; + } else if ( + nextSpec.molecularAlterationType === + AlterationTypeConstants.METHYLATION + ) { trackIdForRuleSetSharingKey = 'heatmap01'; + } else { + // Expression and other molecular data tracks + trackIdForRuleSetSharingKey = 'heatmap'; } + + // Only share rule set with tracks of the SAME molecular alteration type + // Prevents VAF tracks (0-1 scale) from sharing rules with mRNA tracks (-3 to 3 scale) if ( typeof trackIdForRuleSetSharing[trackIdForRuleSetSharingKey] !== 'undefined' @@ -1561,6 +1597,7 @@ export function transitionHeatmapTrack( newTrackId ); } + // Store this track ID for future tracks of the same type trackIdForRuleSetSharing[trackIdForRuleSetSharingKey] = newTrackId; } else { // if the track is a generic assay profile, add to trackIdForRuleSetSharing under its `molecularProfileId` diff --git a/src/shared/components/oncoprint/OncoprintUtils.ts b/src/shared/components/oncoprint/OncoprintUtils.ts index ad743352143..d3fadebad4c 100644 --- a/src/shared/components/oncoprint/OncoprintUtils.ts +++ b/src/shared/components/oncoprint/OncoprintUtils.ts @@ -33,7 +33,9 @@ import { makeHeatmapTrackData, } from './DataUtils'; import _, { isNumber } from 'lodash'; -import ResultsViewOncoprint from './ResultsViewOncoprint'; +import ResultsViewOncoprint, { + AdditionalTrackGroupRecord, +} from './ResultsViewOncoprint'; import { action, IObservableArray, ObservableMap, runInAction } from 'mobx'; import GenesetCorrelatedGeneCache from 'shared/cache/GenesetCorrelatedGeneCache'; import { @@ -46,6 +48,7 @@ import { Patient, Sample, Gene, + NumericGeneMolecularData, } from 'cbioportal-ts-api-client'; import { clinicalAttributeIsPROFILEDIN, @@ -71,6 +74,10 @@ import { AnnotatedExtendedAlteration } from 'shared/model/AnnotatedExtendedAlter import { IQueriedCaseData } from 'shared/model/IQueriedCaseData'; import { IQueriedMergedTrackCaseData } from 'shared/model/IQueriedMergedTrackCaseData'; import { OncoprintModel } from 'oncoprintjs'; +import { getVariantAlleleFrequency } from 'shared/lib/MutationUtils'; +import { DataType } from 'pages/studyView/StudyViewUtils'; +import GeneCache from 'shared/cache/GeneCache'; +import { isSampleProfiled } from 'shared/lib/isSampleProfiled'; interface IGenesetExpansionMap { [genesetTrackKey: string]: IHeatmapTrackSpec[]; @@ -531,6 +538,177 @@ export function getCategoricalTrackRuleSetParams( }; } +/** + * Processes molecular data for heatmap tracks based on profile type. + * For MUTATION_EXTENDED profiles, calculates variant allele frequency (VAF) from mutations. + * For other profiles, retrieves molecular data values directly from cache. + */ +function createHeatmapTracksData( + query: any, + profileType: string, + oncoprint: ResultsViewOncoprint +): Array<{ + uniqueSampleKey: string; + uniquePatientKey: string; + value: number | null; + thresholdType?: '>' | '<'; +}> { + if (profileType === AlterationTypeConstants.MUTATION_EXTENDED) { + const mutationPromise = oncoprint.props.store.annotatedMutationCache.get( + { + entrezGeneId: query.entrezGeneId, + } + ); + + const mutations = mutationPromise.result; + const samples = oncoprint.props.store.filteredSamples.result!; + const coverageInfo = oncoprint.props.store.coverageInformation.result!; + + if (mutations) { + const profileMutations = mutations.filter( + m => m.molecularProfileId === query.molecularProfileId + ); + + // Create a map of samples with mutations and their VAF values + const sampleMutationMap = new Map(); + + // Process mutations to get VAF values + profileMutations.forEach(m => { + const vafReport = getVariantAlleleFrequency(m); + if ( + vafReport && + typeof vafReport.vaf === 'number' && + !isNaN(vafReport.vaf) + ) { + // Store the highest VAF if multiple mutations exist for the same sample + const existingVaf = sampleMutationMap.get( + m.uniqueSampleKey + ); + if ( + existingVaf === undefined || + vafReport.vaf > existingVaf! + ) { + sampleMutationMap.set(m.uniqueSampleKey, vafReport.vaf); + } + } else { + // Mutation exists but no valid VAF - only set if no VAF data exists yet + if (!sampleMutationMap.has(m.uniqueSampleKey)) { + sampleMutationMap.set(m.uniqueSampleKey, null); + } + } + }); + + // Filter to only include profiled samples, then create data entries + return samples + .filter(sample => { + return isSampleProfiled( + sample.uniqueSampleKey, + query.molecularProfileId, + query.hugoGeneSymbol, + coverageInfo + ); + }) + .map(sample => { + const vafValue = sampleMutationMap.get( + sample.uniqueSampleKey + ); + return { + uniqueSampleKey: sample.uniqueSampleKey, + uniquePatientKey: sample.uniquePatientKey, + value: vafValue !== undefined ? vafValue : null, // null for profiled but not mutated + }; + }); + } else { + // No mutations data available - return null values for profiled samples only + return samples + .filter(sample => { + return isSampleProfiled( + sample.uniqueSampleKey, + query.molecularProfileId, + query.hugoGeneSymbol, + coverageInfo + ); + }) + .map(sample => ({ + uniqueSampleKey: sample.uniqueSampleKey, + uniquePatientKey: sample.uniquePatientKey, + value: null, + })); + } + } else { + const molecularDataResult = oncoprint.props.store.geneMolecularDataCache.result!.get( + query + ); + if (molecularDataResult && molecularDataResult.data) { + return molecularDataResult.data.map( + (d: NumericGeneMolecularData) => ({ + value: d.value, + uniqueSampleKey: d.uniqueSampleKey, + uniquePatientKey: d.uniquePatientKey, + }) + ); + } + } + return []; +} + +/** + * Generates queries for gene-molecular profile combinations from additional tracks. + * Maps gene symbols to entrez IDs and creates query objects for data retrieval. + * Excludes GENERIC_ASSAY molecular alteration types. + */ +function getGeneProfileQueries( + molecularProfileIdToAdditionalTracks: { + [molecularProfileId: string]: AdditionalTrackGroupRecord; + }, + geneCache: GeneCache +) { + const geneProfiles = _(molecularProfileIdToAdditionalTracks) + .values() + .filter( + d => + d.molecularAlterationType !== + AlterationTypeConstants.GENERIC_ASSAY + ) + .value(); + + return _(geneProfiles) + .map(entry => + _.keys(entry.entities).map(g => ({ + molecularProfileId: entry.molecularProfileId, + entrezGeneId: geneCache.get({ hugoGeneSymbol: g })?.data + ?.entrezGeneId, + hugoGeneSymbol: g.toUpperCase(), + })) + ) + .flatten() + .filter(query => query.entrezGeneId) + .value(); +} + +// Filters gene profile queries to return only those with MUTATION_EXTENDED alteration type. +function getMutationHeatMapQueries( + molecularProfileIdToAdditionalTracks: { + [molecularProfileId: string]: AdditionalTrackGroupRecord; + }, + molecularProfileIdToMolecularProfile: { + [molecularProfileId: string]: MolecularProfile; + }, + geneCache: GeneCache +) { + const cacheQueries = getGeneProfileQueries( + molecularProfileIdToAdditionalTracks, + geneCache + ); + + return cacheQueries.filter(query => { + const profileType = + molecularProfileIdToMolecularProfile[query.molecularProfileId] + ?.molecularAlterationType; + return profileType === AlterationTypeConstants.MUTATION_EXTENDED; + }); +} + export function percentAltered(altered: number, sequenced: number) { if (sequenced === 0) { return 'N/P'; @@ -1130,71 +1308,141 @@ export function makeHeatmapTracksMobxPromise( sampleMode: boolean ) { return remoteData({ - await: () => [ - oncoprint.props.store.filteredSamples, - oncoprint.props.store.filteredPatients, - oncoprint.props.store.molecularProfileIdToMolecularProfile, - oncoprint.props.store.geneMolecularDataCache, - ], - invoke: async () => { + await: () => { const molecularProfileIdToMolecularProfile = oncoprint.props.store .molecularProfileIdToMolecularProfile.result!; const molecularProfileIdToAdditionalTracks = oncoprint.molecularProfileIdToAdditionalTracks; - const geneProfiles = _.filter( - _.values(molecularProfileIdToAdditionalTracks), - d => - d.molecularAlterationType !== - AlterationTypeConstants.GENERIC_ASSAY + const mutationQueries = getMutationHeatMapQueries( + molecularProfileIdToAdditionalTracks, + molecularProfileIdToMolecularProfile, + oncoprint.props.store.geneCache ); + + return [ + oncoprint.props.store.filteredSamples, + oncoprint.props.store.filteredPatients, + oncoprint.props.store.molecularProfileIdToMolecularProfile, + oncoprint.props.store.geneMolecularDataCache, + oncoprint.props.store.coverageInformation, + ...mutationQueries.map(query => + oncoprint.props.store.annotatedMutationCache.get({ + entrezGeneId: query.entrezGeneId!, + }) + ), + ]; + }, + invoke: async () => { + const molecularProfileIdToMolecularProfile = oncoprint.props.store + .molecularProfileIdToMolecularProfile.result!; + const molecularProfileIdToAdditionalTracks = + oncoprint.molecularProfileIdToAdditionalTracks; + const neededGenes = _.flatten( - geneProfiles.map(v => _.keys(v.entities)) + _.values(molecularProfileIdToAdditionalTracks) + .filter( + d => + d.molecularAlterationType !== + AlterationTypeConstants.GENERIC_ASSAY + ) + .map(v => _.keys(v.entities)) ); + await oncoprint.props.store.geneCache.getPromise( neededGenes.map(g => ({ hugoGeneSymbol: g })), true ); - const cacheQueries = _.flatten( - geneProfiles.map(entry => - _.keys(entry.entities).map(g => ({ - molecularProfileId: entry.molecularProfileId, - entrezGeneId: oncoprint.props.store.geneCache.get({ - hugoGeneSymbol: g, - })!.data!.entrezGeneId, - hugoGeneSymbol: g.toUpperCase(), - })) - ) - ); - await oncoprint.props.store.geneMolecularDataCache.result!.getPromise( - cacheQueries, - true - ); + const cacheQueries = getGeneProfileQueries( + molecularProfileIdToAdditionalTracks, + oncoprint.props.store.geneCache + ).map(query => ({ + ...query, + entrezGeneId: oncoprint.props.store.geneCache.get({ + hugoGeneSymbol: query.hugoGeneSymbol.toLowerCase(), + })!.data!.entrezGeneId, + })); + + const nonMutationQueries = cacheQueries.filter(query => { + const profileType = + molecularProfileIdToMolecularProfile[ + query.molecularProfileId + ].molecularAlterationType; + return ( + profileType !== AlterationTypeConstants.MUTATION_EXTENDED + ); + }); + + if (nonMutationQueries.length > 0) { + await oncoprint.props.store.geneMolecularDataCache.result!.getPromise( + nonMutationQueries, + true + ); + } const samples = oncoprint.props.store.filteredSamples.result!; const patients = oncoprint.props.store.filteredPatients.result!; return cacheQueries.map(query => { - const molecularProfileId = query.molecularProfileId; - const gene = query.hugoGeneSymbol; - const data = oncoprint.props.store.geneMolecularDataCache.result!.get( - query - )!.data!; + const { molecularProfileId, hugoGeneSymbol: gene } = query; + const profileType = + molecularProfileIdToMolecularProfile[molecularProfileId] + .molecularAlterationType; + const datatype = + molecularProfileIdToMolecularProfile[molecularProfileId] + .datatype; + const molecularProfileName = + molecularProfileIdToMolecularProfile[molecularProfileId] + .name; + const trackGroupIndex = + molecularProfileIdToAdditionalTracks[molecularProfileId] + .trackGroupIndex; + + const onClickRemoveInTrackMenu = action(() => { + const trackGroup = + oncoprint.molecularProfileIdToAdditionalTracks[ + molecularProfileId + ]; + if (trackGroup) { + const newEntities = _.keys(trackGroup.entities).filter( + entity => entity !== gene + ); + if (newEntities.length === 0) { + oncoprint.removeHeatmapTracksByMolecularProfileId( + molecularProfileId + ); + } else { + oncoprint.setHeatmapTracks( + molecularProfileId, + newEntities + ); + } + } + + if ( + trackGroup === undefined && + oncoprint.sortMode.type === 'heatmap' && + oncoprint.sortMode.clusteredHeatmapProfile === + molecularProfileId + ) { + oncoprint.sortByData(); + } + }); + + const dataForTrack = createHeatmapTracksData( + query, + profileType, + oncoprint + ); return { key: `HEATMAPTRACK_${molecularProfileId},${gene}`, label: gene, - molecularProfileId: molecularProfileId, - molecularProfileName: - molecularProfileIdToMolecularProfile[molecularProfileId] - .name, - molecularAlterationType: - molecularProfileIdToMolecularProfile[molecularProfileId] - .molecularAlterationType, - datatype: - molecularProfileIdToMolecularProfile[molecularProfileId] - .datatype, + molecularProfileId, + molecularProfileName, + molecularAlterationType: profileType, + datatype, data: makeHeatmapTrackData< IGeneHeatmapTrackDatum, 'hugo_gene_symbol' @@ -1202,41 +1450,10 @@ export function makeHeatmapTracksMobxPromise( 'hugo_gene_symbol', gene, sampleMode ? samples : patients, - data + dataForTrack ), - trackGroupIndex: - molecularProfileIdToAdditionalTracks[molecularProfileId] - .trackGroupIndex, - onClickRemoveInTrackMenu: action(() => { - const trackGroup = - oncoprint.molecularProfileIdToAdditionalTracks[ - molecularProfileId - ]; - if (trackGroup) { - const newEntities = _.keys( - trackGroup.entities - ).filter(entity => entity !== gene); - if (newEntities.length === 0) { - oncoprint.removeHeatmapTracksByMolecularProfileId( - molecularProfileId - ); - } else { - oncoprint.setHeatmapTracks( - molecularProfileId, - newEntities - ); - } - } - - if ( - trackGroup === undefined && - oncoprint.sortMode.type === 'heatmap' && - oncoprint.sortMode.clusteredHeatmapProfile === - molecularProfileId - ) { - oncoprint.sortByData(); - } - }), + trackGroupIndex, + onClickRemoveInTrackMenu, }; }); }, @@ -1433,11 +1650,12 @@ export function makeGenericAssayProfileHeatmapTracksMobxPromise( 'entityId', entityId, sampleMode ? samples : patients, - dataCache.get(query)!.data!.map(d => ({ - ...d, - value: parseFloat(d.value), - })), - sortOrder + dataCache + .get({ molecularProfileId, stableId: entityId })! + .data!.map(d => ({ + ...d!, + value: parseFloat(d.value!), + })) ), genericAssayType: genericAssayType, pivotThreshold: pivotThreshold, diff --git a/src/shared/components/oncoprint/ResultsViewOncoprintUtils.tsx b/src/shared/components/oncoprint/ResultsViewOncoprintUtils.tsx index ee96fc647d5..e8c4dcc1671 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprintUtils.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprintUtils.tsx @@ -344,11 +344,22 @@ export function makeTrackGroupHeaders( } else { type = 'heatmap'; } - headerMap[nextEntry.trackGroupIndex] = makeTrackGroupHeader( - type, + + let headerText = molecularProfileIdToMolecularProfile[ nextEntry.molecularProfileId - ].name, + ].name; + + if ( + profile.molecularAlterationType === + AlterationTypeConstants.MUTATION_EXTENDED + ) { + headerText = 'Variant Allele Frequency'; + } + + headerMap[nextEntry.trackGroupIndex] = makeTrackGroupHeader( + type, + headerText, nextEntry.trackGroupIndex, onClickDeleteCallback, getClusteredTrackGroupIndex, diff --git a/src/shared/components/oncoprint/TooltipUtils.ts b/src/shared/components/oncoprint/TooltipUtils.ts index 8947b6c35e9..3da18101315 100644 --- a/src/shared/components/oncoprint/TooltipUtils.ts +++ b/src/shared/components/oncoprint/TooltipUtils.ts @@ -270,6 +270,9 @@ export function makeHeatmapTrackTooltip( (trackSpec as IHeatmapTrackSpec).genericAssayType! )}: `; break; + case AlterationTypeConstants.MUTATION_EXTENDED: + data_header = 'VAF: '; + break; default: data_header = 'Value: '; break; diff --git a/src/shared/components/oncoprint/controls/OncoprintControls.tsx b/src/shared/components/oncoprint/controls/OncoprintControls.tsx index e2edf627fd0..505db0497a7 100644 --- a/src/shared/components/oncoprint/controls/OncoprintControls.tsx +++ b/src/shared/components/oncoprint/controls/OncoprintControls.tsx @@ -43,6 +43,7 @@ import { ClinicalTrackConfigMap, } from 'shared/components/oncoprint/Oncoprint'; import { getServerConfig } from 'config/config'; +import { AlterationTypeConstants } from 'shared/constants'; export interface IOncoprintControlsHandlers extends IDriverAnnotationControlsHandlers { @@ -474,11 +475,21 @@ export default class OncoprintControls extends React.Component< ) { return _.map( this.props.state.heatmapProfilesPromise.result, - profile => ({ - label: profile.name, - value: profile.molecularProfileId, - type: profile.molecularAlterationType, - }) + profile => { + let label = profile.name; + if ( + profile.molecularAlterationType === + AlterationTypeConstants.MUTATION_EXTENDED + ) { + label = `Variant Allele Frequency in Selected Mutations Profile`; + } + + return { + label, + value: profile.molecularProfileId, + type: profile.molecularAlterationType, + }; + } ); } else { return [];