From 0340c228d34866a0befcbfef34b3bee2dd49be78 Mon Sep 17 00:00:00 2001 From: Bryan Lai Date: Wed, 1 Oct 2025 16:55:29 -0400 Subject: [PATCH 1/6] add expression tab to patient page --- src/pages/patientView/PatientViewPageTabs.tsx | 34 +++ .../PatientViewPageStore.ts | 172 ++++++++++++++- .../expression/ExpressionTableWrapper.tsx | 206 ++++++++++++++++++ src/shared/lib/StoreUtils.ts | 63 ++++++ src/shared/lib/isSampleProfiled.ts | 55 +++++ 5 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 src/pages/patientView/expression/ExpressionTableWrapper.tsx diff --git a/src/pages/patientView/PatientViewPageTabs.tsx b/src/pages/patientView/PatientViewPageTabs.tsx index f8f29d0496e..297c44c9111 100644 --- a/src/pages/patientView/PatientViewPageTabs.tsx +++ b/src/pages/patientView/PatientViewPageTabs.tsx @@ -37,6 +37,7 @@ import { HelpWidget } from 'shared/components/HelpWidget/HelpWidget'; import MutationTableWrapper from './mutation/MutationTableWrapper'; import { PatientViewPageInner } from 'pages/patientView/PatientViewPage'; import { Else, If } from 'react-if'; +import ExpressionTableWrapper from './expression/ExpressionTableWrapper'; export enum PatientViewPageTabs { Summary = 'summary', @@ -49,6 +50,7 @@ export enum PatientViewPageTabs { TrialMatchTab = 'trialMatchTab', MutationalSignatures = 'mutationalSignatures', PathwayMapper = 'pathways', + Expression = 'expression', } export const PatientViewResourceTabPrefix = 'openResource_'; @@ -488,6 +490,38 @@ export function tabs( ); + pageComponent.patientViewPageStore.isExpressionProfiledForPatient + .isComplete && + pageComponent.patientViewPageStore.isExpressionProfiledForPatient + .result && + tabs.push( + + {pageComponent.patientViewPageStore.mrnaExpressionData + .isComplete && + pageComponent.patientViewPageStore.proteinExpressionData + .isComplete && + pageComponent.patientViewPageStore.mutationData.isComplete && + pageComponent.patientViewPageStore.structuralVariantData + .isComplete && + pageComponent.patientViewPageStore.discreteCNAData + .isComplete ? ( + + ) : ( + + )} + + ); + tabs.push( [ + this.mrnaExpressionProfiles, + this.analysisMrnaExpressionProfiles, + this.mrnaRankMolecularProfileId, + ], + invoke: async () => { + let mrnaData: NumericGeneMolecularData[]; + + if (this.analysisMrnaExpressionProfiles.result.length > 0) { + mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( + { + projection: 'DETAILED', + molecularProfileId: this.analysisMrnaExpressionProfiles + .result[0].molecularProfileId, + molecularDataFilter: { + sampleIds: this.sampleIds, + } as MolecularDataFilter, + } + ); + } else if (this.mrnaExpressionProfiles.result.length > 0) { + mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( + { + molecularProfileId: this.mrnaExpressionProfiles + .result[0].molecularProfileId, + molecularDataFilter: { + sampleIds: this.sampleIds, + } as MolecularDataFilter, + } + ); + } else { + mrnaData = []; + } + return mrnaData; + }, + default: [], + }); + + readonly mrnaExpressionProfiles = remoteData( + { + await: () => [this.molecularProfilesInStudy], + invoke: () => { + return Promise.resolve( + this.molecularProfilesInStudy.result.filter( + p => p.molecularAlterationType === 'MRNA_EXPRESSION' + ) + ); + }, + }, + [] + ); + + readonly analysisMrnaExpressionProfiles = remoteData( + { + await: () => [this.mrnaExpressionProfiles], + invoke: () => { + return Promise.resolve( + this.mrnaExpressionProfiles.result.filter( + p => p.showProfileInAnalysisTab + ) + ); + }, + }, + [] + ); + + readonly proteinExpressionData = remoteData({ + await: () => [ + this.proteinExpressionProfiles, + this.analysisProteinExpressionProfiles, + ], + invoke: async () => { + let mrnaData: NumericGeneMolecularData[]; + + if (this.analysisProteinExpressionProfiles.result.length > 0) { + mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( + { + projection: 'DETAILED', + molecularProfileId: this + .analysisProteinExpressionProfiles.result[0] + .molecularProfileId, + molecularDataFilter: { + sampleIds: this.sampleIds, + } as MolecularDataFilter, + } + ); + } else if (this.proteinExpressionProfiles.result.length > 0) { + mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( + { + molecularProfileId: this.proteinExpressionProfiles + .result[0].molecularProfileId, + molecularDataFilter: { + sampleIds: this.sampleIds, + } as MolecularDataFilter, + } + ); + } else { + mrnaData = []; + } + return mrnaData; + }, + default: [], + }); + + readonly proteinExpressionProfiles = remoteData( + { + await: () => [this.molecularProfilesInStudy], + invoke: () => { + return Promise.resolve( + this.molecularProfilesInStudy.result.filter( + p => p.molecularAlterationType === 'PROTEIN_LEVEL' + ) + ); + }, + }, + [] + ); + + readonly analysisProteinExpressionProfiles = remoteData( + { + await: () => [this.proteinExpressionProfiles], + invoke: () => { + return Promise.resolve( + this.proteinExpressionProfiles.result.filter( + p => p.showProfileInAnalysisTab + ) + ); + }, + }, + [] + ); + + readonly isExpressionProfiledForPatient = remoteData({ + await: () => [ + this.mrnaExpressionProfiles, + this.proteinExpressionProfiles, + this.derivedUniquePatientKey, + this.coverageInformation, + ], + invoke: () => { + const expressionProfileIds = [ + ...this.mrnaExpressionProfiles.result, + ...this.proteinExpressionProfiles.result, + ].map(p => p.molecularProfileId); + return Promise.resolve( + _.some( + isPatientProfiledInMultiple( + this.derivedUniquePatientKey.result, + expressionProfileIds, + this.coverageInformation.result + ) + ) + ); + }, + }); + // public set patientIdsInCohort(cohortIds: string[]) { // // cannot put action on setter // runInAction(() => (this._patientIdsInCohort = cohortIds || [])); @@ -819,6 +979,16 @@ export class PatientViewPageStore { default: '', }); + readonly derivedUniquePatientKey = remoteData({ + await: () => [this.samples], + invoke: async () => { + for (let sample of this.samples.result) + return sample.uniquePatientKey; + return ''; + }, + default: '', + }); + readonly clinicalDataPatient = remoteData({ await: () => this.pageMode === 'patient' ? [] : [this.derivedPatientId], diff --git a/src/pages/patientView/expression/ExpressionTableWrapper.tsx b/src/pages/patientView/expression/ExpressionTableWrapper.tsx new file mode 100644 index 00000000000..8ed5e92d970 --- /dev/null +++ b/src/pages/patientView/expression/ExpressionTableWrapper.tsx @@ -0,0 +1,206 @@ +import * as React from 'react'; +import LazyMobXTable, { + Column, +} from 'shared/components/lazyMobXTable/LazyMobXTable'; +import _ from 'lodash'; +import { observer } from 'mobx-react'; +import { computed, makeObservable, observable } from 'mobx'; +import { PatientViewPageStore } from '../clinicalInformation/PatientViewPageStore'; +import { prepareExpressionRowDataForTable } from 'shared/lib/StoreUtils'; +export interface IExpressionTableWrapperProps { + store: PatientViewPageStore; +} + +class ExpressionTable extends LazyMobXTable {} + +type ExpressionTableColumn = Column & { order: number }; + +export interface IExpressionRow { + hugoGeneSymbol: string; + mrnaExpression: number; + proteinExpression: number; + mutations: string; + structuralVariants: string; + cna: string; +} + +@observer +export default class ExpressionTableWrapper extends React.Component< + IExpressionTableWrapperProps, + {} +> { + @observable + selectedSignature: string; + constructor(props: IExpressionTableWrapperProps) { + super(props); + makeObservable(this); + } + + @computed get expressionDataForTable() { + return prepareExpressionRowDataForTable( + this.props.store.mrnaExpressionData.result, + this.props.store.proteinExpressionData.result, + this.props.store.mutationData.result, + this.props.store.structuralVariantData.result, + this.props.store.discreteCNAData.result + ); + } + + @computed get mrnaExpressionProfileName() { + if (this.props.store.analysisMrnaExpressionProfiles.result.length > 0) { + return this.props.store.analysisMrnaExpressionProfiles.result[0] + .name; + } else if (this.props.store.mrnaExpressionProfiles.result.length > 0) { + return this.props.store.mrnaExpressionProfiles.result[0].name; + } else { + return ''; + } + } + + @computed get proteinExpressionProfileName() { + if ( + this.props.store.analysisProteinExpressionProfiles.result.length > 0 + ) { + return this.props.store.analysisProteinExpressionProfiles.result[0] + .name; + } else if ( + this.props.store.proteinExpressionProfiles.result.length > 0 + ) { + return this.props.store.proteinExpressionProfiles.result[0].name; + } else { + return ''; + } + } + + @computed get columns() { + const columns: ExpressionTableColumn[] = []; + + columns.push({ + name: 'Gene', + render: (d: IExpressionRow[]) => {d[0].hugoGeneSymbol}, + filter: ( + d: IExpressionRow[], + filterString: string, + filterStringUpper: string + ) => { + return d[0].hugoGeneSymbol.indexOf(filterStringUpper) > -1; + }, + download: (d: IExpressionRow[]) => d[0].hugoGeneSymbol, + sortBy: (d: IExpressionRow[]) => d[0].hugoGeneSymbol, + visible: true, + order: 30, + }); + + if (this.mrnaExpressionProfileName) { + columns.push({ + name: this.mrnaExpressionProfileName, + render: (d: IExpressionRow[]) => ( + + {Number.isNaN(d[0].mrnaExpression) + ? '' + : d[0].mrnaExpression.toFixed(2)} + + ), + download: (d: IExpressionRow[]) => + Number.isNaN(d[0].mrnaExpression) + ? '' + : d[0].mrnaExpression.toFixed(2), + sortBy: (d: IExpressionRow[]) => { + if (d[0].mrnaExpression) { + return d[0].mrnaExpression; + } else { + return null; + } + }, + visible: true, + order: 35, + }); + } + + if (this.proteinExpressionProfileName) { + columns.push({ + name: this.proteinExpressionProfileName, + render: (d: IExpressionRow[]) => ( + + {Number.isNaN(d[0].proteinExpression) + ? '' + : d[0].proteinExpression.toFixed(2)} + + ), + download: (d: IExpressionRow[]) => + Number.isNaN(d[0].proteinExpression) + ? '' + : d[0].proteinExpression.toFixed(2), + sortBy: (d: IExpressionRow[]) => { + if (d[0].proteinExpression) { + return d[0].proteinExpression; + } else { + return null; + } + }, + visible: true, + order: 40, + }); + } + + if (this.props.store.mutationMolecularProfile.result) { + columns.push({ + name: this.props.store.mutationMolecularProfile.result.name, + render: (d: IExpressionRow[]) => {d[0].mutations}, + download: (d: IExpressionRow[]) => d[0].mutations, + sortBy: (d: IExpressionRow[]) => d[0].mutations, + visible: true, + order: 45, + }); + } + + if (this.props.store.structuralVariantProfile.result) { + columns.push({ + name: this.props.store.structuralVariantProfile.result.name, + render: (d: IExpressionRow[]) => ( + {d[0].structuralVariants} + ), + download: (d: IExpressionRow[]) => d[0].structuralVariants, + sortBy: (d: IExpressionRow[]) => d[0].structuralVariants, + visible: true, + order: 50, + }); + } + + if (this.props.store.discreteMolecularProfile.result) { + columns.push({ + name: this.props.store.discreteMolecularProfile.result.name, + render: (d: IExpressionRow[]) => {d[0].cna}, + download: (d: IExpressionRow[]) => d[0].cna, + sortBy: (d: IExpressionRow[]) => d[0].cna, + visible: true, + order: 55, + }); + } + + const orderedColumns = _.sortBy( + columns, + (c: ExpressionTableColumn) => c.order + ); + return orderedColumns; + } + + public render() { + return ( +
+ +
+ ); + } +} diff --git a/src/shared/lib/StoreUtils.ts b/src/shared/lib/StoreUtils.ts index 4713bd898df..afc81c3bf8a 100644 --- a/src/shared/lib/StoreUtils.ts +++ b/src/shared/lib/StoreUtils.ts @@ -109,6 +109,7 @@ import { } from 'shared/model/CustomDriverAnnotationInfo'; import { AnnotatedNumericGeneMolecularData } from 'shared/model/AnnotatedNumericGeneMolecularData'; import { EnsemblFilter } from 'genome-nexus-ts-api-client'; +import { IExpressionRow } from 'pages/patientView/expression/ExpressionTableWrapper'; export const MolecularAlterationType_filenameSuffix: { [K in MolecularProfile['molecularAlterationType']]?: string; @@ -2011,3 +2012,65 @@ export function buildProteinChange(sv: StructuralVariant) { return `${genes[0]} intragenic`; } } + +export function prepareExpressionRowDataForTable( + mrnaExpressionData: NumericGeneMolecularData[], + proteinExpressionData: NumericGeneMolecularData[], + mutationData: Mutation[], + structuralVariantData: StructuralVariant[], + discreteCNAData: DiscreteCopyNumberData[] +): IExpressionRow[][] { + const tableData: IExpressionRow[][] = []; + + const mrnaGeneSymbols = mrnaExpressionData.map(d => d.gene.hugoGeneSymbol); + const proteinGeneSymbols = proteinExpressionData.map( + d => d.gene.hugoGeneSymbol + ); + const geneSymbols = _.uniq([...mrnaGeneSymbols, ...proteinGeneSymbols]); + + const geneToMrnaExpressionDataMap = _.groupBy( + mrnaExpressionData, + d => d.gene.hugoGeneSymbol + ); + const geneToProteinExpressionDataMap = _.groupBy( + proteinExpressionData, + d => d.gene.hugoGeneSymbol + ); + const geneToMutationDataMap = _.keyBy( + mutationData, + d => d.gene.hugoGeneSymbol + ); + const geneToStructuralVariantDataMap = _.keyBy( + structuralVariantData, + d => d.site1HugoSymbol + ); + const geneToDiscreteCNADataMap = _.keyBy( + discreteCNAData, + d => d.gene.hugoGeneSymbol + ); + + for (const geneSymbol of geneSymbols) { + let expressionRowForTable: IExpressionRow = { + hugoGeneSymbol: geneSymbol, + mrnaExpression: averageExpressionValue( + geneToMrnaExpressionDataMap[geneSymbol] || [] + ), + proteinExpression: averageExpressionValue( + geneToProteinExpressionDataMap[geneSymbol] || [] + ), + mutations: geneToMutationDataMap[geneSymbol]?.keyword, + structuralVariants: + geneToStructuralVariantDataMap[geneSymbol]?.eventInfo, + cna: getAlterationString( + geneToDiscreteCNADataMap[geneSymbol]?.alteration + ), + }; + + tableData.push([expressionRowForTable]); + } + return tableData; +} + +export function averageExpressionValue(d: NumericGeneMolecularData[]): number { + return _.mean(d.map(x => x.value)); +} diff --git a/src/shared/lib/isSampleProfiled.ts b/src/shared/lib/isSampleProfiled.ts index b090cbfdfa3..1de4964f181 100644 --- a/src/shared/lib/isSampleProfiled.ts +++ b/src/shared/lib/isSampleProfiled.ts @@ -103,3 +103,58 @@ export function isSampleProfiledInProfile( genePanelMap[profileId][sampleId].profiled === true ); } + +function getPatientProfiledReport( + uniquePatientKey: string, + coverageInformation: CoverageInformation, + hugoGeneSymbol?: string +): { [molecularProfileId: string]: boolean } { + // returns a map whose keys are the profiles which the patient is profiled in + const patientCoverage = coverageInformation.patients[uniquePatientKey]; + + // no patient coverage for patient + if (!patientCoverage) return {}; + + const byGeneCoverage = hugoGeneSymbol + ? patientCoverage.byGene[hugoGeneSymbol] + : _.flatten(_.values(patientCoverage.byGene)); + + // no by gene coverage for gene AND there's no allGene data available + if (!byGeneCoverage && patientCoverage.allGenes.length === 0) return {}; + + const ret: { [m: string]: boolean } = {}; + + // does molecular profile appear in the GENE specific panel data + for (const gpData of byGeneCoverage || []) { + ret[gpData.molecularProfileId] = true; + } + + // does molecular profile appear in CROSS gene panel data + for (const gpData of patientCoverage.allGenes) { + ret[gpData.molecularProfileId] = true; + } + + return ret; +} + +export function isPatientProfiledInMultiple( + uniquePatientKey: string, + molecularProfileIds: string[] | undefined, + coverageInformation: CoverageInformation, + hugoGeneSymbol?: string +): boolean[] { + // returns empty list if molecularProfileIds is undefined + if (!molecularProfileIds) { + return []; + } else { + // returns boolean[] in same order as molecularProfileIds + const profiledReport = getPatientProfiledReport( + uniquePatientKey, + coverageInformation, + hugoGeneSymbol + ); + return molecularProfileIds.map( + molecularProfileId => !!profiledReport[molecularProfileId] + ); + } +} From 21191eb543a5b25bf7d23c03e9bf6a9fe51c410f Mon Sep 17 00:00:00 2001 From: Bryan Lai Date: Wed, 1 Oct 2025 18:05:41 -0400 Subject: [PATCH 2/6] fix undefined gene symbol error --- src/pages/patientView/PatientViewPageTabs.tsx | 3 +- .../PatientViewPageStore.ts | 22 +++++++ .../expression/ExpressionTableWrapper.tsx | 25 ++++---- src/shared/lib/StoreUtils.ts | 59 +++++++++---------- 4 files changed, 64 insertions(+), 45 deletions(-) diff --git a/src/pages/patientView/PatientViewPageTabs.tsx b/src/pages/patientView/PatientViewPageTabs.tsx index 297c44c9111..86b7422797a 100644 --- a/src/pages/patientView/PatientViewPageTabs.tsx +++ b/src/pages/patientView/PatientViewPageTabs.tsx @@ -507,7 +507,8 @@ export function tabs( pageComponent.patientViewPageStore.mutationData.isComplete && pageComponent.patientViewPageStore.structuralVariantData .isComplete && - pageComponent.patientViewPageStore.discreteCNAData + pageComponent.patientViewPageStore.discreteCNAData.isComplete && + pageComponent.patientViewPageStore.allEntrezGeneIdsToGene .isComplete ? ( { + return getClient().getAllGenesUsingGET({ + projection: 'SUMMARY', + }); + }, + }); + + readonly allEntrezGeneIdsToGene = remoteData<{ + [entrezGeneId: number]: { + hugoGeneSymbol: string; + entrezGeneId: number; + }; + }>({ + await: () => [this.allGenes], + invoke: () => + Promise.resolve( + _.keyBy(this.allGenes.result, gene => gene.entrezGeneId) + ), + default: {}, + }); + readonly mrnaExpressionData = remoteData({ await: () => [ this.mrnaExpressionProfiles, diff --git a/src/pages/patientView/expression/ExpressionTableWrapper.tsx b/src/pages/patientView/expression/ExpressionTableWrapper.tsx index 8ed5e92d970..b06b08295c0 100644 --- a/src/pages/patientView/expression/ExpressionTableWrapper.tsx +++ b/src/pages/patientView/expression/ExpressionTableWrapper.tsx @@ -42,7 +42,8 @@ export default class ExpressionTableWrapper extends React.Component< this.props.store.proteinExpressionData.result, this.props.store.mutationData.result, this.props.store.structuralVariantData.result, - this.props.store.discreteCNAData.result + this.props.store.discreteCNAData.result, + this.props.store.allEntrezGeneIdsToGene.result ); } @@ -96,15 +97,13 @@ export default class ExpressionTableWrapper extends React.Component< name: this.mrnaExpressionProfileName, render: (d: IExpressionRow[]) => ( - {Number.isNaN(d[0].mrnaExpression) - ? '' - : d[0].mrnaExpression.toFixed(2)} + {d[0].mrnaExpression + ? d[0].mrnaExpression.toFixed(2) + : ''} ), download: (d: IExpressionRow[]) => - Number.isNaN(d[0].mrnaExpression) - ? '' - : d[0].mrnaExpression.toFixed(2), + d[0].mrnaExpression ? d[0].mrnaExpression.toFixed(2) : '', sortBy: (d: IExpressionRow[]) => { if (d[0].mrnaExpression) { return d[0].mrnaExpression; @@ -122,15 +121,15 @@ export default class ExpressionTableWrapper extends React.Component< name: this.proteinExpressionProfileName, render: (d: IExpressionRow[]) => ( - {Number.isNaN(d[0].proteinExpression) - ? '' - : d[0].proteinExpression.toFixed(2)} + {d[0].proteinExpression + ? d[0].proteinExpression.toFixed(2) + : ''} ), download: (d: IExpressionRow[]) => - Number.isNaN(d[0].proteinExpression) - ? '' - : d[0].proteinExpression.toFixed(2), + d[0].proteinExpression + ? d[0].proteinExpression.toFixed(2) + : '', sortBy: (d: IExpressionRow[]) => { if (d[0].proteinExpression) { return d[0].proteinExpression; diff --git a/src/shared/lib/StoreUtils.ts b/src/shared/lib/StoreUtils.ts index afc81c3bf8a..c53ddbfcb8b 100644 --- a/src/shared/lib/StoreUtils.ts +++ b/src/shared/lib/StoreUtils.ts @@ -2018,51 +2018,52 @@ export function prepareExpressionRowDataForTable( proteinExpressionData: NumericGeneMolecularData[], mutationData: Mutation[], structuralVariantData: StructuralVariant[], - discreteCNAData: DiscreteCopyNumberData[] + discreteCNAData: DiscreteCopyNumberData[], + allEntrezGeneIdsToGene: { + [entrezGeneId: number]: { + hugoGeneSymbol: string; + entrezGeneId: number; + }; + } ): IExpressionRow[][] { const tableData: IExpressionRow[][] = []; - const mrnaGeneSymbols = mrnaExpressionData.map(d => d.gene.hugoGeneSymbol); - const proteinGeneSymbols = proteinExpressionData.map( - d => d.gene.hugoGeneSymbol - ); - const geneSymbols = _.uniq([...mrnaGeneSymbols, ...proteinGeneSymbols]); + const mrnaEntrezGeneIds = mrnaExpressionData.map(d => d.entrezGeneId); + const proteinEntrezGeneIds = proteinExpressionData.map(d => d.entrezGeneId); + const entrezGeneIds = _.uniq([ + ...mrnaEntrezGeneIds, + ...proteinEntrezGeneIds, + ]); - const geneToMrnaExpressionDataMap = _.groupBy( + const geneToMrnaExpressionDataMap = _.keyBy( mrnaExpressionData, - d => d.gene.hugoGeneSymbol + d => d.entrezGeneId ); - const geneToProteinExpressionDataMap = _.groupBy( + const geneToProteinExpressionDataMap = _.keyBy( proteinExpressionData, - d => d.gene.hugoGeneSymbol - ); - const geneToMutationDataMap = _.keyBy( - mutationData, - d => d.gene.hugoGeneSymbol + d => d.entrezGeneId ); + const geneToMutationDataMap = _.keyBy(mutationData, d => d.entrezGeneId); const geneToStructuralVariantDataMap = _.keyBy( structuralVariantData, - d => d.site1HugoSymbol + d => d.site1EntrezGeneId ); const geneToDiscreteCNADataMap = _.keyBy( discreteCNAData, - d => d.gene.hugoGeneSymbol + d => d.entrezGeneId ); - for (const geneSymbol of geneSymbols) { + for (const entrezGeneId of entrezGeneIds) { let expressionRowForTable: IExpressionRow = { - hugoGeneSymbol: geneSymbol, - mrnaExpression: averageExpressionValue( - geneToMrnaExpressionDataMap[geneSymbol] || [] - ), - proteinExpression: averageExpressionValue( - geneToProteinExpressionDataMap[geneSymbol] || [] - ), - mutations: geneToMutationDataMap[geneSymbol]?.keyword, + hugoGeneSymbol: allEntrezGeneIdsToGene[entrezGeneId].hugoGeneSymbol, + mrnaExpression: geneToMrnaExpressionDataMap[entrezGeneId]?.value, + proteinExpression: + geneToProteinExpressionDataMap[entrezGeneId]?.value, + mutations: geneToMutationDataMap[entrezGeneId]?.keyword, structuralVariants: - geneToStructuralVariantDataMap[geneSymbol]?.eventInfo, + geneToStructuralVariantDataMap[entrezGeneId]?.eventInfo, cna: getAlterationString( - geneToDiscreteCNADataMap[geneSymbol]?.alteration + geneToDiscreteCNADataMap[entrezGeneId]?.alteration ), }; @@ -2070,7 +2071,3 @@ export function prepareExpressionRowDataForTable( } return tableData; } - -export function averageExpressionValue(d: NumericGeneMolecularData[]): number { - return _.mean(d.map(x => x.value)); -} From 08985207c8d909b1baa0546ef0daa29d5ff4c98d Mon Sep 17 00:00:00 2001 From: Bryan Lai Date: Fri, 10 Oct 2025 15:49:41 -0400 Subject: [PATCH 3/6] set up profile selections --- src/pages/patientView/PatientViewPageTabs.tsx | 14 +- .../PatientViewPageStore.ts | 144 +++++++------ .../expression/ExpressionTableWrapper.tsx | 202 ++++++++++++++---- src/shared/lib/StoreUtils.ts | 30 +-- 4 files changed, 263 insertions(+), 127 deletions(-) diff --git a/src/pages/patientView/PatientViewPageTabs.tsx b/src/pages/patientView/PatientViewPageTabs.tsx index 86b7422797a..d79b78bcdb5 100644 --- a/src/pages/patientView/PatientViewPageTabs.tsx +++ b/src/pages/patientView/PatientViewPageTabs.tsx @@ -500,16 +500,20 @@ export function tabs( id={PatientViewPageTabs.Expression} linkText={'Expression'} > - {pageComponent.patientViewPageStore.mrnaExpressionData - .isComplete && - pageComponent.patientViewPageStore.proteinExpressionData - .isComplete && + {pageComponent.patientViewPageStore + .mrnaExpressionDataByGeneThenProfile.isComplete && + pageComponent.patientViewPageStore + .proteinExpressionDataByGeneThenProfile.isComplete && pageComponent.patientViewPageStore.mutationData.isComplete && pageComponent.patientViewPageStore.structuralVariantData .isComplete && pageComponent.patientViewPageStore.discreteCNAData.isComplete && pageComponent.patientViewPageStore.allEntrezGeneIdsToGene - .isComplete ? ( + .isComplete && + pageComponent.patientViewPageStore + .analysisMrnaExpressionProfiles.isComplete && + pageComponent.patientViewPageStore + .analysisProteinExpressionProfiles.isComplete ? ( diff --git a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts index b8d6f9cb473..f4a12f95711 100644 --- a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts +++ b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts @@ -496,42 +496,46 @@ export class PatientViewPageStore { default: {}, }); - readonly mrnaExpressionData = remoteData({ - await: () => [ - this.mrnaExpressionProfiles, - this.analysisMrnaExpressionProfiles, - this.mrnaRankMolecularProfileId, - ], + readonly mrnaExpressionDataByGeneThenProfile = remoteData({ + await: () => [this.mrnaExpressionProfiles], invoke: async () => { - let mrnaData: NumericGeneMolecularData[]; - - if (this.analysisMrnaExpressionProfiles.result.length > 0) { - mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( - { - projection: 'DETAILED', - molecularProfileId: this.analysisMrnaExpressionProfiles - .result[0].molecularProfileId, - molecularDataFilter: { - sampleIds: this.sampleIds, - } as MolecularDataFilter, - } - ); - } else if (this.mrnaExpressionProfiles.result.length > 0) { - mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( - { - molecularProfileId: this.mrnaExpressionProfiles - .result[0].molecularProfileId, - molecularDataFilter: { - sampleIds: this.sampleIds, - } as MolecularDataFilter, - } - ); - } else { - mrnaData = []; - } - return mrnaData; + const mrnaExpressionMap: Record< + number, + Record + > = {}; + await Promise.all( + this.mrnaExpressionProfiles.result.map(async p => { + let data = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( + { + projection: 'DETAILED', + molecularProfileId: p.molecularProfileId, + molecularDataFilter: { + sampleIds: this.sampleIds, + } as MolecularDataFilter, + } + ); + data.map(d => { + if (!mrnaExpressionMap[d.entrezGeneId]) { + mrnaExpressionMap[d.entrezGeneId] = {}; + } + if ( + !mrnaExpressionMap[d.entrezGeneId][ + d.molecularProfileId + ] + ) { + mrnaExpressionMap[d.entrezGeneId][ + d.molecularProfileId + ] = []; + } + mrnaExpressionMap[d.entrezGeneId][ + d.molecularProfileId + ].push(d); + }); + }) + ); + return mrnaExpressionMap; }, - default: [], + default: {}, }); readonly mrnaExpressionProfiles = remoteData( @@ -562,42 +566,46 @@ export class PatientViewPageStore { [] ); - readonly proteinExpressionData = remoteData({ - await: () => [ - this.proteinExpressionProfiles, - this.analysisProteinExpressionProfiles, - ], + readonly proteinExpressionDataByGeneThenProfile = remoteData({ + await: () => [this.proteinExpressionProfiles], invoke: async () => { - let mrnaData: NumericGeneMolecularData[]; - - if (this.analysisProteinExpressionProfiles.result.length > 0) { - mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( - { - projection: 'DETAILED', - molecularProfileId: this - .analysisProteinExpressionProfiles.result[0] - .molecularProfileId, - molecularDataFilter: { - sampleIds: this.sampleIds, - } as MolecularDataFilter, - } - ); - } else if (this.proteinExpressionProfiles.result.length > 0) { - mrnaData = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( - { - molecularProfileId: this.proteinExpressionProfiles - .result[0].molecularProfileId, - molecularDataFilter: { - sampleIds: this.sampleIds, - } as MolecularDataFilter, - } - ); - } else { - mrnaData = []; - } - return mrnaData; + const proteinExpressionMap: Record< + number, + Record + > = {}; + await Promise.all( + this.proteinExpressionProfiles.result.map(async p => { + let data = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( + { + projection: 'DETAILED', + molecularProfileId: p.molecularProfileId, + molecularDataFilter: { + sampleIds: this.sampleIds, + } as MolecularDataFilter, + } + ); + data.map(d => { + if (!proteinExpressionMap[d.entrezGeneId]) { + proteinExpressionMap[d.entrezGeneId] = {}; + } + if ( + !proteinExpressionMap[d.entrezGeneId][ + d.molecularProfileId + ] + ) { + proteinExpressionMap[d.entrezGeneId][ + d.molecularProfileId + ] = []; + } + proteinExpressionMap[d.entrezGeneId][ + d.molecularProfileId + ].push(d); + }); + }) + ); + return proteinExpressionMap; }, - default: [], + default: {}, }); readonly proteinExpressionProfiles = remoteData( diff --git a/src/pages/patientView/expression/ExpressionTableWrapper.tsx b/src/pages/patientView/expression/ExpressionTableWrapper.tsx index b06b08295c0..97a2e5dc57e 100644 --- a/src/pages/patientView/expression/ExpressionTableWrapper.tsx +++ b/src/pages/patientView/expression/ExpressionTableWrapper.tsx @@ -4,9 +4,10 @@ import LazyMobXTable, { } from 'shared/components/lazyMobXTable/LazyMobXTable'; import _ from 'lodash'; import { observer } from 'mobx-react'; -import { computed, makeObservable, observable } from 'mobx'; +import { computed, makeObservable } from 'mobx'; import { PatientViewPageStore } from '../clinicalInformation/PatientViewPageStore'; import { prepareExpressionRowDataForTable } from 'shared/lib/StoreUtils'; +import { NumericGeneMolecularData } from 'cbioportal-ts-api-client'; export interface IExpressionTableWrapperProps { store: PatientViewPageStore; } @@ -17,8 +18,8 @@ type ExpressionTableColumn = Column & { order: number }; export interface IExpressionRow { hugoGeneSymbol: string; - mrnaExpression: number; - proteinExpression: number; + mrnaExpression: Record; + proteinExpression: Record; mutations: string; structuralVariants: string; cna: string; @@ -29,8 +30,6 @@ export default class ExpressionTableWrapper extends React.Component< IExpressionTableWrapperProps, {} > { - @observable - selectedSignature: string; constructor(props: IExpressionTableWrapperProps) { super(props); makeObservable(this); @@ -38,8 +37,8 @@ export default class ExpressionTableWrapper extends React.Component< @computed get expressionDataForTable() { return prepareExpressionRowDataForTable( - this.props.store.mrnaExpressionData.result, - this.props.store.proteinExpressionData.result, + this.props.store.mrnaExpressionDataByGeneThenProfile.result, + this.props.store.proteinExpressionDataByGeneThenProfile.result, this.props.store.mutationData.result, this.props.store.structuralVariantData.result, this.props.store.discreteCNAData.result, @@ -47,29 +46,23 @@ export default class ExpressionTableWrapper extends React.Component< ); } - @computed get mrnaExpressionProfileName() { + @computed get defaultMrnaExpressionProfile() { if (this.props.store.analysisMrnaExpressionProfiles.result.length > 0) { - return this.props.store.analysisMrnaExpressionProfiles.result[0] - .name; + return this.props.store.analysisMrnaExpressionProfiles.result[0]; } else if (this.props.store.mrnaExpressionProfiles.result.length > 0) { - return this.props.store.mrnaExpressionProfiles.result[0].name; - } else { - return ''; + return this.props.store.mrnaExpressionProfiles.result[0]; } } - @computed get proteinExpressionProfileName() { + @computed get defaultProteinExpressionProfile() { if ( this.props.store.analysisProteinExpressionProfiles.result.length > 0 ) { - return this.props.store.analysisProteinExpressionProfiles.result[0] - .name; + return this.props.store.analysisProteinExpressionProfiles.result[0]; } else if ( this.props.store.proteinExpressionProfiles.result.length > 0 ) { - return this.props.store.proteinExpressionProfiles.result[0].name; - } else { - return ''; + return this.props.store.proteinExpressionProfiles.result[0]; } } @@ -89,59 +82,188 @@ export default class ExpressionTableWrapper extends React.Component< download: (d: IExpressionRow[]) => d[0].hugoGeneSymbol, sortBy: (d: IExpressionRow[]) => d[0].hugoGeneSymbol, visible: true, - order: 30, + order: 20, }); - if (this.mrnaExpressionProfileName) { + if (this.defaultMrnaExpressionProfile) { columns.push({ - name: this.mrnaExpressionProfileName, + name: this.defaultMrnaExpressionProfile.name, render: (d: IExpressionRow[]) => ( - {d[0].mrnaExpression - ? d[0].mrnaExpression.toFixed(2) + {d[0].mrnaExpression && + d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ] + ? d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2) : ''} ), download: (d: IExpressionRow[]) => - d[0].mrnaExpression ? d[0].mrnaExpression.toFixed(2) : '', + d[0].mrnaExpression && + d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile!.molecularProfileId + ] + ? d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2) + : '', sortBy: (d: IExpressionRow[]) => { - if (d[0].mrnaExpression) { - return d[0].mrnaExpression; + if ( + d[0].mrnaExpression && + d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ] + ) { + return d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ][0].value; } else { return null; } }, visible: true, - order: 35, + order: 25, }); } - if (this.proteinExpressionProfileName) { + this.props.store.mrnaExpressionProfiles.result.map((p, i) => { + if ( + p.molecularProfileId !== + this.defaultMrnaExpressionProfile?.molecularProfileId + ) { + columns.push({ + name: p.name, + render: (d: IExpressionRow[]) => ( + + {d[0].mrnaExpression && + d[0].mrnaExpression[p.molecularProfileId] + ? d[0].mrnaExpression[ + p.molecularProfileId + ][0].value.toFixed(2) + : ''} + + ), + download: (d: IExpressionRow[]) => + d[0].mrnaExpression && + d[0].mrnaExpression[p.molecularProfileId] + ? d[0].mrnaExpression[ + p.molecularProfileId + ][0].value.toFixed(2) + : '', + sortBy: (d: IExpressionRow[]) => { + if ( + d[0].mrnaExpression && + d[0].mrnaExpression[p.molecularProfileId] + ) { + return d[0].mrnaExpression[p.molecularProfileId][0] + .value; + } else { + return null; + } + }, + visible: false, + order: 30, + }); + } + }); + + if (this.defaultProteinExpressionProfile) { columns.push({ - name: this.proteinExpressionProfileName, + name: this.defaultProteinExpressionProfile.name, render: (d: IExpressionRow[]) => ( - {d[0].proteinExpression - ? d[0].proteinExpression.toFixed(2) + {d[0].proteinExpression && + d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ] + ? d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2) : ''} ), download: (d: IExpressionRow[]) => - d[0].proteinExpression - ? d[0].proteinExpression.toFixed(2) + d[0].proteinExpression && + d[0].proteinExpression[ + this.defaultProteinExpressionProfile!.molecularProfileId + ] + ? d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2) : '', sortBy: (d: IExpressionRow[]) => { - if (d[0].proteinExpression) { - return d[0].proteinExpression; + if ( + d[0].proteinExpression && + d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ] + ) { + return d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ][0].value; } else { return null; } }, visible: true, - order: 40, + order: 35, }); } + this.props.store.proteinExpressionProfiles.result.map((p, i) => { + if ( + p.molecularProfileId !== + this.defaultProteinExpressionProfile?.molecularProfileId + ) { + columns.push({ + name: p.name, + render: (d: IExpressionRow[]) => ( + + {d[0].proteinExpression && + d[0].proteinExpression[p.molecularProfileId] + ? d[0].proteinExpression[ + p.molecularProfileId + ][0].value.toFixed(2) + : ''} + + ), + download: (d: IExpressionRow[]) => + d[0].proteinExpression && + d[0].proteinExpression[p.molecularProfileId] + ? d[0].proteinExpression[ + p.molecularProfileId + ][0].value.toFixed(2) + : '', + sortBy: (d: IExpressionRow[]) => { + if ( + d[0].proteinExpression && + d[0].proteinExpression[p.molecularProfileId] + ) { + return d[0].proteinExpression[ + p.molecularProfileId + ][0].value; + } else { + return null; + } + }, + visible: false, + order: 40, + }); + } + }); + if (this.props.store.mutationMolecularProfile.result) { columns.push({ name: this.props.store.mutationMolecularProfile.result.name, @@ -192,10 +314,10 @@ export default class ExpressionTableWrapper extends React.Component< data={this.expressionDataForTable} showPagination={true} initialItemsPerPage={20} - showColumnVisibility={false} + showColumnVisibility={true} initialSortColumn={ - this.mrnaExpressionProfileName || - this.proteinExpressionProfileName + this.defaultMrnaExpressionProfile?.name || + this.defaultProteinExpressionProfile?.name } initialSortDirection="desc" /> diff --git a/src/shared/lib/StoreUtils.ts b/src/shared/lib/StoreUtils.ts index c53ddbfcb8b..e69350e927e 100644 --- a/src/shared/lib/StoreUtils.ts +++ b/src/shared/lib/StoreUtils.ts @@ -2014,8 +2014,14 @@ export function buildProteinChange(sv: StructuralVariant) { } export function prepareExpressionRowDataForTable( - mrnaExpressionData: NumericGeneMolecularData[], - proteinExpressionData: NumericGeneMolecularData[], + mrnaExpressionDataByGeneThenProfile: Record< + string, + Record + >, + proteinExpressionDataByGeneThenProfile: Record< + string, + Record + >, mutationData: Mutation[], structuralVariantData: StructuralVariant[], discreteCNAData: DiscreteCopyNumberData[], @@ -2028,21 +2034,17 @@ export function prepareExpressionRowDataForTable( ): IExpressionRow[][] { const tableData: IExpressionRow[][] = []; - const mrnaEntrezGeneIds = mrnaExpressionData.map(d => d.entrezGeneId); - const proteinEntrezGeneIds = proteinExpressionData.map(d => d.entrezGeneId); + const mrnaEntrezGeneIds = Object.keys( + mrnaExpressionDataByGeneThenProfile + ).map(Number); + const proteinEntrezGeneIds = Object.keys( + proteinExpressionDataByGeneThenProfile + ).map(Number); const entrezGeneIds = _.uniq([ ...mrnaEntrezGeneIds, ...proteinEntrezGeneIds, ]); - const geneToMrnaExpressionDataMap = _.keyBy( - mrnaExpressionData, - d => d.entrezGeneId - ); - const geneToProteinExpressionDataMap = _.keyBy( - proteinExpressionData, - d => d.entrezGeneId - ); const geneToMutationDataMap = _.keyBy(mutationData, d => d.entrezGeneId); const geneToStructuralVariantDataMap = _.keyBy( structuralVariantData, @@ -2056,9 +2058,9 @@ export function prepareExpressionRowDataForTable( for (const entrezGeneId of entrezGeneIds) { let expressionRowForTable: IExpressionRow = { hugoGeneSymbol: allEntrezGeneIdsToGene[entrezGeneId].hugoGeneSymbol, - mrnaExpression: geneToMrnaExpressionDataMap[entrezGeneId]?.value, + mrnaExpression: mrnaExpressionDataByGeneThenProfile[entrezGeneId], proteinExpression: - geneToProteinExpressionDataMap[entrezGeneId]?.value, + proteinExpressionDataByGeneThenProfile[entrezGeneId], mutations: geneToMutationDataMap[entrezGeneId]?.keyword, structuralVariants: geneToStructuralVariantDataMap[entrezGeneId]?.eventInfo, From 575490e1fdff8a0c674690034ea412e6f78b154e Mon Sep 17 00:00:00 2001 From: Bryan Lai Date: Wed, 15 Oct 2025 15:23:51 -0400 Subject: [PATCH 4/6] add support for other cna profiles --- src/pages/patientView/PatientViewPageTabs.tsx | 3 +- .../PatientViewPageStore.ts | 50 +++++++++++ .../expression/ExpressionTableWrapper.tsx | 87 +++++++++++++++++-- src/shared/lib/StoreUtils.ts | 15 ++-- 4 files changed, 140 insertions(+), 15 deletions(-) diff --git a/src/pages/patientView/PatientViewPageTabs.tsx b/src/pages/patientView/PatientViewPageTabs.tsx index d79b78bcdb5..38d72b2afe3 100644 --- a/src/pages/patientView/PatientViewPageTabs.tsx +++ b/src/pages/patientView/PatientViewPageTabs.tsx @@ -507,7 +507,8 @@ export function tabs( pageComponent.patientViewPageStore.mutationData.isComplete && pageComponent.patientViewPageStore.structuralVariantData .isComplete && - pageComponent.patientViewPageStore.discreteCNAData.isComplete && + pageComponent.patientViewPageStore.cnaDataByGeneThenProfile + .isComplete && pageComponent.patientViewPageStore.allEntrezGeneIdsToGene .isComplete && pageComponent.patientViewPageStore diff --git a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts index f4a12f95711..22b76828b87 100644 --- a/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts +++ b/src/pages/patientView/clinicalInformation/PatientViewPageStore.ts @@ -496,6 +496,56 @@ export class PatientViewPageStore { default: {}, }); + readonly cnaDataByGeneThenProfile = remoteData({ + await: () => [this.cnaProfiles], + invoke: async () => { + const cnaMap: Record< + number, + Record + > = {}; + await Promise.all( + this.cnaProfiles.result.map(async p => { + let data = await getClient().fetchAllMolecularDataInMolecularProfileUsingPOST( + { + projection: 'DETAILED', + molecularProfileId: p.molecularProfileId, + molecularDataFilter: { + sampleIds: this.sampleIds, + } as MolecularDataFilter, + } + ); + data.map(d => { + if (!cnaMap[d.entrezGeneId]) { + cnaMap[d.entrezGeneId] = {}; + } + if (!cnaMap[d.entrezGeneId][d.molecularProfileId]) { + cnaMap[d.entrezGeneId][d.molecularProfileId] = []; + } + cnaMap[d.entrezGeneId][d.molecularProfileId].push(d); + }); + }) + ); + return cnaMap; + }, + default: {}, + }); + + readonly cnaProfiles = remoteData( + { + await: () => [this.molecularProfilesInStudy], + invoke: () => { + return Promise.resolve( + this.molecularProfilesInStudy.result.filter( + p => + p.molecularAlterationType === + 'COPY_NUMBER_ALTERATION' + ) + ); + }, + }, + [] + ); + readonly mrnaExpressionDataByGeneThenProfile = remoteData({ await: () => [this.mrnaExpressionProfiles], invoke: async () => { diff --git a/src/pages/patientView/expression/ExpressionTableWrapper.tsx b/src/pages/patientView/expression/ExpressionTableWrapper.tsx index 97a2e5dc57e..f294b455ca7 100644 --- a/src/pages/patientView/expression/ExpressionTableWrapper.tsx +++ b/src/pages/patientView/expression/ExpressionTableWrapper.tsx @@ -8,6 +8,7 @@ import { computed, makeObservable } from 'mobx'; import { PatientViewPageStore } from '../clinicalInformation/PatientViewPageStore'; import { prepareExpressionRowDataForTable } from 'shared/lib/StoreUtils'; import { NumericGeneMolecularData } from 'cbioportal-ts-api-client'; +import { getAlterationString } from 'shared/lib/CopyNumberUtils'; export interface IExpressionTableWrapperProps { store: PatientViewPageStore; } @@ -22,7 +23,7 @@ export interface IExpressionRow { proteinExpression: Record; mutations: string; structuralVariants: string; - cna: string; + cna: Record; } @observer @@ -41,7 +42,7 @@ export default class ExpressionTableWrapper extends React.Component< this.props.store.proteinExpressionDataByGeneThenProfile.result, this.props.store.mutationData.result, this.props.store.structuralVariantData.result, - this.props.store.discreteCNAData.result, + this.props.store.cnaDataByGeneThenProfile.result, this.props.store.allEntrezGeneIdsToGene.result ); } @@ -291,14 +292,90 @@ export default class ExpressionTableWrapper extends React.Component< if (this.props.store.discreteMolecularProfile.result) { columns.push({ name: this.props.store.discreteMolecularProfile.result.name, - render: (d: IExpressionRow[]) => {d[0].cna}, - download: (d: IExpressionRow[]) => d[0].cna, - sortBy: (d: IExpressionRow[]) => d[0].cna, + render: (d: IExpressionRow[]) => ( + + {d[0].cna && + d[0].cna[ + this.props.store.discreteMolecularProfile.result! + .molecularProfileId + ] + ? getAlterationString( + d[0].cna[ + this.props.store.discreteMolecularProfile + .result!.molecularProfileId + ][0].value + ) + : ''} + + ), + download: (d: IExpressionRow[]) => + d[0].cna && + d[0].cna[ + this.props.store.discreteMolecularProfile.result! + .molecularProfileId + ] + ? getAlterationString( + d[0].cna[ + this.props.store.discreteMolecularProfile + .result!.molecularProfileId + ][0].value + ) + : '', + sortBy: (d: IExpressionRow[]) => { + if ( + d[0].cna && + d[0].cna[ + this.props.store.discreteMolecularProfile.result! + .molecularProfileId + ] + ) { + return d[0].cna[ + this.props.store.discreteMolecularProfile.result! + .molecularProfileId + ][0].value; + } else { + return null; + } + }, visible: true, order: 55, }); } + this.props.store.cnaProfiles.result.map((p, i) => { + if ( + p.molecularProfileId !== + this.props.store.discreteMolecularProfile.result + ?.molecularProfileId + ) { + columns.push({ + name: p.name, + render: (d: IExpressionRow[]) => ( + + {d[0].cna && d[0].cna[p.molecularProfileId] + ? d[0].cna[ + p.molecularProfileId + ][0].value.toFixed(2) + : ''} + + ), + download: (d: IExpressionRow[]) => + d[0].cna && d[0].cna[p.molecularProfileId] + ? d[0].cna[p.molecularProfileId][0].value.toFixed(2) + : '', + sortBy: (d: IExpressionRow[]) => { + if (d[0].cna && d[0].cna[p.molecularProfileId]) { + return d[0].cna[p.molecularProfileId][0].value; + } else { + return null; + } + }, + visible: false, + order: 60, + }); + } + }); + const orderedColumns = _.sortBy( columns, (c: ExpressionTableColumn) => c.order diff --git a/src/shared/lib/StoreUtils.ts b/src/shared/lib/StoreUtils.ts index e69350e927e..78b266d3f47 100644 --- a/src/shared/lib/StoreUtils.ts +++ b/src/shared/lib/StoreUtils.ts @@ -2024,7 +2024,10 @@ export function prepareExpressionRowDataForTable( >, mutationData: Mutation[], structuralVariantData: StructuralVariant[], - discreteCNAData: DiscreteCopyNumberData[], + cnaDataByGeneThenProfile: Record< + string, + Record + >, allEntrezGeneIdsToGene: { [entrezGeneId: number]: { hugoGeneSymbol: string; @@ -2050,10 +2053,6 @@ export function prepareExpressionRowDataForTable( structuralVariantData, d => d.site1EntrezGeneId ); - const geneToDiscreteCNADataMap = _.keyBy( - discreteCNAData, - d => d.entrezGeneId - ); for (const entrezGeneId of entrezGeneIds) { let expressionRowForTable: IExpressionRow = { @@ -2061,12 +2060,10 @@ export function prepareExpressionRowDataForTable( mrnaExpression: mrnaExpressionDataByGeneThenProfile[entrezGeneId], proteinExpression: proteinExpressionDataByGeneThenProfile[entrezGeneId], - mutations: geneToMutationDataMap[entrezGeneId]?.keyword, + mutations: geneToMutationDataMap[entrezGeneId]?.proteinChange, structuralVariants: geneToStructuralVariantDataMap[entrezGeneId]?.eventInfo, - cna: getAlterationString( - geneToDiscreteCNADataMap[entrezGeneId]?.alteration - ), + cna: cnaDataByGeneThenProfile[entrezGeneId], }; tableData.push([expressionRowForTable]); From b599622aaf1f3e64cf0c2dde274acf058178c133 Mon Sep 17 00:00:00 2001 From: Bryan Lai Date: Thu, 16 Oct 2025 16:09:12 -0400 Subject: [PATCH 5/6] add expression value average when multiple samples --- .../expression/ExpressionTableWrapper.tsx | 540 ++++++++++++++---- 1 file changed, 424 insertions(+), 116 deletions(-) diff --git a/src/pages/patientView/expression/ExpressionTableWrapper.tsx b/src/pages/patientView/expression/ExpressionTableWrapper.tsx index f294b455ca7..873544b75bd 100644 --- a/src/pages/patientView/expression/ExpressionTableWrapper.tsx +++ b/src/pages/patientView/expression/ExpressionTableWrapper.tsx @@ -9,6 +9,12 @@ import { PatientViewPageStore } from '../clinicalInformation/PatientViewPageStor import { prepareExpressionRowDataForTable } from 'shared/lib/StoreUtils'; import { NumericGeneMolecularData } from 'cbioportal-ts-api-client'; import { getAlterationString } from 'shared/lib/CopyNumberUtils'; +import { SampleLabelHTML } from 'shared/components/sampleLabel/SampleLabel'; +import { + getCNAByAlteration, + getCNAColorByAlteration, +} from 'pages/studyView/StudyViewUtils'; +import { getCnaTypes } from 'shared/lib/pathwayMapper/PathwayMapperHelpers'; export interface IExpressionTableWrapperProps { store: PatientViewPageStore; } @@ -69,6 +75,8 @@ export default class ExpressionTableWrapper extends React.Component< @computed get columns() { const columns: ExpressionTableColumn[] = []; + const hasMultipleSamples: boolean = + this.props.store.samples.result.length > 1; columns.push({ name: 'Gene', @@ -89,42 +97,120 @@ export default class ExpressionTableWrapper extends React.Component< if (this.defaultMrnaExpressionProfile) { columns.push({ name: this.defaultMrnaExpressionProfile.name, - render: (d: IExpressionRow[]) => ( - - {d[0].mrnaExpression && - d[0].mrnaExpression[ + render: (d: IExpressionRow[]) => { + if ( + d[0].mrnaExpression?.[ this.defaultMrnaExpressionProfile! .molecularProfileId ] - ? d[0].mrnaExpression[ - this.defaultMrnaExpressionProfile! - .molecularProfileId - ][0].value.toFixed(2) - : ''} - - ), - download: (d: IExpressionRow[]) => - d[0].mrnaExpression && - d[0].mrnaExpression[ - this.defaultMrnaExpressionProfile!.molecularProfileId - ] - ? d[0].mrnaExpression[ - this.defaultMrnaExpressionProfile! - .molecularProfileId - ][0].value.toFixed(2) - : '', - sortBy: (d: IExpressionRow[]) => { + ) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ], + d => d.value + ).toFixed(2); + return ( + + {mean} ( + {d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ].map((data, i) => ( + + {data.value.toFixed(2)}{' '} + + {i < + d[0].mrnaExpression[ + this + .defaultMrnaExpressionProfile! + .molecularProfileId + ].length - + 1 && ', '} + + ))} + ) + + ); + } else { + return ( + + {d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2)} + + ); + } + } + return ; + }, + download: (d: IExpressionRow[]) => { if ( - d[0].mrnaExpression && - d[0].mrnaExpression[ + d[0].mrnaExpression?.[ this.defaultMrnaExpressionProfile! .molecularProfileId ] ) { - return d[0].mrnaExpression[ + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ], + d => d.value + ).toFixed(2); + return mean; + } else { + return d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2); + } + } else { + return ''; + } + }, + sortBy: (d: IExpressionRow[]) => { + if ( + d[0].mrnaExpression?.[ this.defaultMrnaExpressionProfile! .molecularProfileId - ][0].value; + ] + ) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ], + d => d.value + ); + return mean; + } else { + return d[0].mrnaExpression[ + this.defaultMrnaExpressionProfile! + .molecularProfileId + ][0].value; + } } else { return null; } @@ -141,30 +227,92 @@ export default class ExpressionTableWrapper extends React.Component< ) { columns.push({ name: p.name, - render: (d: IExpressionRow[]) => ( - - {d[0].mrnaExpression && - d[0].mrnaExpression[p.molecularProfileId] - ? d[0].mrnaExpression[ - p.molecularProfileId - ][0].value.toFixed(2) - : ''} - - ), - download: (d: IExpressionRow[]) => - d[0].mrnaExpression && - d[0].mrnaExpression[p.molecularProfileId] - ? d[0].mrnaExpression[ - p.molecularProfileId - ][0].value.toFixed(2) - : '', + render: (d: IExpressionRow[]) => { + if (d[0].mrnaExpression?.[p.molecularProfileId]) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].mrnaExpression[p.molecularProfileId], + d => d.value + ).toFixed(2); + return ( + + {mean} ( + {d[0].mrnaExpression[ + p.molecularProfileId + ].map((data, i) => ( + + {data.value.toFixed(2)}{' '} + + {i < + d[0].mrnaExpression[ + p.molecularProfileId + ].length - + 1 && ', '} + + ))} + ) + + ); + } else { + return ( + + {d[0].mrnaExpression[ + p.molecularProfileId + ][0].value.toFixed(2)} + + ); + } + } + return ; + }, + download: (d: IExpressionRow[]) => { + if (d[0].mrnaExpression?.[p.molecularProfileId]) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].mrnaExpression[p.molecularProfileId], + d => d.value + ).toFixed(2); + return mean; + } else { + return d[0].mrnaExpression[ + p.molecularProfileId + ][0].value.toFixed(2); + } + } else { + return ''; + } + }, sortBy: (d: IExpressionRow[]) => { - if ( - d[0].mrnaExpression && - d[0].mrnaExpression[p.molecularProfileId] - ) { - return d[0].mrnaExpression[p.molecularProfileId][0] - .value; + if (d[0].mrnaExpression?.[p.molecularProfileId]) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].mrnaExpression[p.molecularProfileId], + d => d.value + ); + return mean; + } else { + return d[0].mrnaExpression[ + p.molecularProfileId + ][0].value; + } } else { return null; } @@ -178,42 +326,120 @@ export default class ExpressionTableWrapper extends React.Component< if (this.defaultProteinExpressionProfile) { columns.push({ name: this.defaultProteinExpressionProfile.name, - render: (d: IExpressionRow[]) => ( - - {d[0].proteinExpression && - d[0].proteinExpression[ + render: (d: IExpressionRow[]) => { + if ( + d[0].mrnaExpression?.[ this.defaultProteinExpressionProfile! .molecularProfileId ] - ? d[0].proteinExpression[ - this.defaultProteinExpressionProfile! - .molecularProfileId - ][0].value.toFixed(2) - : ''} - - ), - download: (d: IExpressionRow[]) => - d[0].proteinExpression && - d[0].proteinExpression[ - this.defaultProteinExpressionProfile!.molecularProfileId - ] - ? d[0].proteinExpression[ - this.defaultProteinExpressionProfile! - .molecularProfileId - ][0].value.toFixed(2) - : '', - sortBy: (d: IExpressionRow[]) => { + ) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ], + d => d.value + ).toFixed(2); + return ( + + {mean} ( + {d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ].map((data, i) => ( + + {data.value.toFixed(2)}{' '} + + {i < + d[0].proteinExpression[ + this + .defaultProteinExpressionProfile! + .molecularProfileId + ].length - + 1 && ', '} + + ))} + ) + + ); + } else { + return ( + + {d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2)} + + ); + } + } + return ; + }, + download: (d: IExpressionRow[]) => { if ( - d[0].proteinExpression && - d[0].proteinExpression[ + d[0].proteinExpression?.[ this.defaultProteinExpressionProfile! .molecularProfileId ] ) { - return d[0].proteinExpression[ + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ], + d => d.value + ).toFixed(2); + return mean; + } else { + return d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ][0].value.toFixed(2); + } + } else { + return ''; + } + }, + sortBy: (d: IExpressionRow[]) => { + if ( + d[0].proteinExpression?.[ this.defaultProteinExpressionProfile! .molecularProfileId - ][0].value; + ] + ) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ], + d => d.value + ); + return mean; + } else { + return d[0].proteinExpression[ + this.defaultProteinExpressionProfile! + .molecularProfileId + ][0].value; + } } else { return null; } @@ -230,31 +456,98 @@ export default class ExpressionTableWrapper extends React.Component< ) { columns.push({ name: p.name, - render: (d: IExpressionRow[]) => ( - - {d[0].proteinExpression && - d[0].proteinExpression[p.molecularProfileId] - ? d[0].proteinExpression[ - p.molecularProfileId - ][0].value.toFixed(2) - : ''} - - ), - download: (d: IExpressionRow[]) => - d[0].proteinExpression && - d[0].proteinExpression[p.molecularProfileId] - ? d[0].proteinExpression[ - p.molecularProfileId - ][0].value.toFixed(2) - : '', + render: (d: IExpressionRow[]) => { + if (d[0].proteinExpression?.[p.molecularProfileId]) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].proteinExpression[ + p.molecularProfileId + ], + d => d.value + ).toFixed(2); + return ( + + {mean} ( + {d[0].proteinExpression[ + p.molecularProfileId + ].map((data, i) => ( + + {data.value.toFixed(2)}{' '} + + {i < + d[0].proteinExpression[ + p.molecularProfileId + ].length - + 1 && ', '} + + ))} + ) + + ); + } else { + return ( + + {d[0].proteinExpression[ + p.molecularProfileId + ][0].value.toFixed(2)} + + ); + } + } + return ; + }, + download: (d: IExpressionRow[]) => { + if (d[0].proteinExpression?.[p.molecularProfileId]) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].proteinExpression[ + p.molecularProfileId + ], + d => d.value + ).toFixed(2); + return mean; + } else { + return d[0].proteinExpression[ + p.molecularProfileId + ][0].value.toFixed(2); + } + } else { + return ''; + } + }, sortBy: (d: IExpressionRow[]) => { - if ( - d[0].proteinExpression && - d[0].proteinExpression[p.molecularProfileId] - ) { - return d[0].proteinExpression[ - p.molecularProfileId - ][0].value; + if (d[0].proteinExpression?.[p.molecularProfileId]) { + if (hasMultipleSamples) { + const mean = _.meanBy( + d[0].proteinExpression[ + p.molecularProfileId + ], + d => d.value + ); + return mean; + } else { + return d[0].proteinExpression[ + p.molecularProfileId + ][0].value; + } } else { return null; } @@ -270,7 +563,8 @@ export default class ExpressionTableWrapper extends React.Component< name: this.props.store.mutationMolecularProfile.result.name, render: (d: IExpressionRow[]) => {d[0].mutations}, download: (d: IExpressionRow[]) => d[0].mutations, - sortBy: (d: IExpressionRow[]) => d[0].mutations, + sortBy: (d: IExpressionRow[]) => + d[0].mutations ? d[0].mutations : null, visible: true, order: 45, }); @@ -283,7 +577,8 @@ export default class ExpressionTableWrapper extends React.Component< {d[0].structuralVariants} ), download: (d: IExpressionRow[]) => d[0].structuralVariants, - sortBy: (d: IExpressionRow[]) => d[0].structuralVariants, + sortBy: (d: IExpressionRow[]) => + d[0].structuralVariants ? d[0].structuralVariants : null, visible: true, order: 50, }); @@ -292,25 +587,39 @@ export default class ExpressionTableWrapper extends React.Component< if (this.props.store.discreteMolecularProfile.result) { columns.push({ name: this.props.store.discreteMolecularProfile.result.name, - render: (d: IExpressionRow[]) => ( - - {d[0].cna && - d[0].cna[ + render: (d: IExpressionRow[]) => { + let color = getCNAColorByAlteration( + d[0].cna?.[ this.props.store.discreteMolecularProfile.result! .molecularProfileId ] - ? getAlterationString( + ? getCNAByAlteration( d[0].cna[ this.props.store.discreteMolecularProfile .result!.molecularProfileId ][0].value ) - : ''} - - ), + : '' + ); + return ( + + {d[0].cna?.[ + this.props.store.discreteMolecularProfile + .result!.molecularProfileId + ] + ? getCnaTypes( + d[0].cna[ + this.props.store + .discreteMolecularProfile.result! + .molecularProfileId + ][0].value + ) + : ''} + + ); + }, download: (d: IExpressionRow[]) => - d[0].cna && - d[0].cna[ + d[0].cna?.[ this.props.store.discreteMolecularProfile.result! .molecularProfileId ] @@ -323,8 +632,7 @@ export default class ExpressionTableWrapper extends React.Component< : '', sortBy: (d: IExpressionRow[]) => { if ( - d[0].cna && - d[0].cna[ + d[0].cna?.[ this.props.store.discreteMolecularProfile.result! .molecularProfileId ] @@ -352,7 +660,7 @@ export default class ExpressionTableWrapper extends React.Component< name: p.name, render: (d: IExpressionRow[]) => ( - {d[0].cna && d[0].cna[p.molecularProfileId] + {d[0].cna?.[p.molecularProfileId] ? d[0].cna[ p.molecularProfileId ][0].value.toFixed(2) @@ -360,11 +668,11 @@ export default class ExpressionTableWrapper extends React.Component< ), download: (d: IExpressionRow[]) => - d[0].cna && d[0].cna[p.molecularProfileId] + d[0].cna?.[p.molecularProfileId] ? d[0].cna[p.molecularProfileId][0].value.toFixed(2) : '', sortBy: (d: IExpressionRow[]) => { - if (d[0].cna && d[0].cna[p.molecularProfileId]) { + if (d[0].cna?.[p.molecularProfileId]) { return d[0].cna[p.molecularProfileId][0].value; } else { return null; From 96e326d2c5db543d6816bd5f7ee0399d61c3a7a4 Mon Sep 17 00:00:00 2001 From: Bryan Lai Date: Wed, 22 Oct 2025 13:59:55 -0400 Subject: [PATCH 6/6] add expression value average and edit mutation column display --- src/pages/patientView/PatientViewPageTabs.tsx | 5 + .../patientView/PatientViewPageUtils.tsx | 25 +++ .../PatientViewPageStore.ts | 14 ++ .../expression/ExpressionTableWrapper.tsx | 186 ++++++++++++++++-- src/shared/lib/StoreUtils.ts | 9 +- 5 files changed, 219 insertions(+), 20 deletions(-) diff --git a/src/pages/patientView/PatientViewPageTabs.tsx b/src/pages/patientView/PatientViewPageTabs.tsx index 38d72b2afe3..6a559571855 100644 --- a/src/pages/patientView/PatientViewPageTabs.tsx +++ b/src/pages/patientView/PatientViewPageTabs.tsx @@ -511,12 +511,17 @@ export function tabs( .isComplete && pageComponent.patientViewPageStore.allEntrezGeneIdsToGene .isComplete && + pageComponent.patientViewPageStore.allHugoGeneSymbolsToGene + .isComplete && pageComponent.patientViewPageStore .analysisMrnaExpressionProfiles.isComplete && pageComponent.patientViewPageStore .analysisProteinExpressionProfiles.isComplete ? ( ) : ( ({ + await: () => [this.allGenes], + invoke: () => + Promise.resolve( + _.keyBy(this.allGenes.result, gene => gene.hugoGeneSymbol) + ), + default: {}, + }); + readonly cnaDataByGeneThenProfile = remoteData({ await: () => [this.cnaProfiles], invoke: async () => { diff --git a/src/pages/patientView/expression/ExpressionTableWrapper.tsx b/src/pages/patientView/expression/ExpressionTableWrapper.tsx index 873544b75bd..75e37f9ec99 100644 --- a/src/pages/patientView/expression/ExpressionTableWrapper.tsx +++ b/src/pages/patientView/expression/ExpressionTableWrapper.tsx @@ -7,16 +7,27 @@ import { observer } from 'mobx-react'; import { computed, makeObservable } from 'mobx'; import { PatientViewPageStore } from '../clinicalInformation/PatientViewPageStore'; import { prepareExpressionRowDataForTable } from 'shared/lib/StoreUtils'; -import { NumericGeneMolecularData } from 'cbioportal-ts-api-client'; +import { + Mutation, + NumericGeneMolecularData, + StructuralVariant, +} from 'cbioportal-ts-api-client'; import { getAlterationString } from 'shared/lib/CopyNumberUtils'; import { SampleLabelHTML } from 'shared/components/sampleLabel/SampleLabel'; -import { - getCNAByAlteration, - getCNAColorByAlteration, -} from 'pages/studyView/StudyViewUtils'; +import { getCNAByAlteration } from 'pages/studyView/StudyViewUtils'; import { getCnaTypes } from 'shared/lib/pathwayMapper/PathwayMapperHelpers'; +import { getCNAColorByAlteration } from '../PatientViewPageUtils'; +import TumorColumnFormatter from '../mutation/column/TumorColumnFormatter'; +import ProteinChangeColumnFormatter from 'shared/components/mutationTable/column/ProteinChangeColumnFormatter'; +import AnnotationColumnFormatter from 'shared/components/mutationTable/column/AnnotationColumnFormatter'; +import { + calculateOncoKbContentPadding, + DEFAULT_ONCOKB_CONTENT_WIDTH, +} from 'shared/lib/AnnotationColumnUtils'; +import autobind from 'autobind-decorator'; export interface IExpressionTableWrapperProps { store: PatientViewPageStore; + mergeOncoKbIcons?: boolean; } class ExpressionTable extends LazyMobXTable {} @@ -27,8 +38,8 @@ export interface IExpressionRow { hugoGeneSymbol: string; mrnaExpression: Record; proteinExpression: Record; - mutations: string; - structuralVariants: string; + mutations: Mutation[]; + structuralVariants: StructuralVariant[]; cna: Record; } @@ -73,6 +84,30 @@ export default class ExpressionTableWrapper extends React.Component< } } + @autobind + protected resolveTumorType(mutation: Mutation) { + // first, try to get it from uniqueSampleKeyToTumorType map + if (this.props.store.uniqueSampleKeyToTumorType) { + return this.props.store.uniqueSampleKeyToTumorType[ + mutation.uniqueSampleKey + ]; + } + + // second, try the study cancer type + if (this.props.store.studyIdToStudy.result) { + const studyMetaData = this.props.store.studyIdToStudy.result[ + mutation.studyId + ]; + + if (studyMetaData.cancerTypeId !== 'mixed') { + return studyMetaData.cancerType.name; + } + } + + // return Unknown, this should not happen... + return 'Unknown'; + } + @computed get columns() { const columns: ExpressionTableColumn[] = []; const hasMultipleSamples: boolean = @@ -561,10 +596,91 @@ export default class ExpressionTableWrapper extends React.Component< if (this.props.store.mutationMolecularProfile.result) { columns.push({ name: this.props.store.mutationMolecularProfile.result.name, - render: (d: IExpressionRow[]) => {d[0].mutations}, - download: (d: IExpressionRow[]) => d[0].mutations, + render: (d: IExpressionRow[]) => { + if (d[0]?.mutations) { + return ( + <> + + {ProteinChangeColumnFormatter.renderWithMutationStatus( + d[0].mutations, + this.props.store + .indexedVariantAnnotations + )} + {AnnotationColumnFormatter.renderFunction( + d[0].mutations, + { + oncoKbData: this.props.store + .oncoKbData, + oncoKbCancerGenes: this.props.store + .oncoKbCancerGenes, + usingPublicOncoKbInstance: this + .props.store + .usingPublicOncoKbInstance, + mergeOncoKbIcons: this.props + .mergeOncoKbIcons, + oncoKbContentPadding: calculateOncoKbContentPadding( + DEFAULT_ONCOKB_CONTENT_WIDTH + ), + enableCivic: false, + enableOncoKb: true, + enableHotspot: false, + enableRevue: false, + resolveTumorType: this + .resolveTumorType, + } + )} + + {hasMultipleSamples && + TumorColumnFormatter.renderFunction( + d[0].mutations, + this.props.store.sampleManager.result!, + this.props.store + .sampleToDiscreteGenePanelId.result, + this.props.store + .genePanelIdToEntrezGeneIds.result + )} + + ); + } else if ( + _.every( + TumorColumnFormatter.getProfiledSamplesForGene( + this.props.store.allHugoGeneSymbolsToGene + .result[d[0].hugoGeneSymbol].entrezGeneId, + this.props.store.sampleIds, + this.props.store.sampleToMutationGenePanelId + .result, + this.props.store.genePanelIdToEntrezGeneIds + .result + ), + profiledStatus => !!!profiledStatus + ) + ) { + return ( + + + + + + ); + } else { + return ; + } + }, + download: (d: IExpressionRow[]) => + d[0]?.mutations ? d[0].mutations[0].proteinChange : '', sortBy: (d: IExpressionRow[]) => - d[0].mutations ? d[0].mutations : null, + d[0]?.mutations ? d[0].mutations[0].proteinChange : null, visible: true, order: 45, }); @@ -573,12 +689,52 @@ export default class ExpressionTableWrapper extends React.Component< if (this.props.store.structuralVariantProfile.result) { columns.push({ name: this.props.store.structuralVariantProfile.result.name, - render: (d: IExpressionRow[]) => ( - {d[0].structuralVariants} - ), - download: (d: IExpressionRow[]) => d[0].structuralVariants, + render: (d: IExpressionRow[]) => { + return ( + <> + + {d[0]?.structuralVariants + ? d[0].structuralVariants[0].eventInfo + : ''} + + {d[0]?.structuralVariants ? ( + TumorColumnFormatter.renderFunction( + d[0].structuralVariants.map(datum => { + // if both are available, return both genes in an array + // otherwise, return whichever is available + const genes = + datum.site1EntrezGeneId && + datum.site2EntrezGeneId + ? [ + datum.site1EntrezGeneId, + datum.site2EntrezGeneId, + ] + : datum.site1EntrezGeneId || + datum.site2EntrezGeneId; + return { + sampleId: datum.sampleId, + entrezGeneId: genes, + sv: true, + }; + }), + this.props.store.sampleManager.result!, + this.props.store.sampleToDiscreteGenePanelId + .result, + this.props.store.genePanelIdToEntrezGeneIds + .result + ) + ) : ( + + )} + + ); + }, + download: (d: IExpressionRow[]) => + d[0]?.structuralVariants[0].eventInfo, sortBy: (d: IExpressionRow[]) => - d[0].structuralVariants ? d[0].structuralVariants : null, + d[0]?.structuralVariants + ? d[0].structuralVariants[0].eventInfo + : null, visible: true, order: 50, }); diff --git a/src/shared/lib/StoreUtils.ts b/src/shared/lib/StoreUtils.ts index 78b266d3f47..8196618602d 100644 --- a/src/shared/lib/StoreUtils.ts +++ b/src/shared/lib/StoreUtils.ts @@ -2048,8 +2048,8 @@ export function prepareExpressionRowDataForTable( ...proteinEntrezGeneIds, ]); - const geneToMutationDataMap = _.keyBy(mutationData, d => d.entrezGeneId); - const geneToStructuralVariantDataMap = _.keyBy( + const geneToMutationDataMap = _.groupBy(mutationData, d => d.entrezGeneId); + const geneToStructuralVariantDataMap = _.groupBy( structuralVariantData, d => d.site1EntrezGeneId ); @@ -2060,9 +2060,8 @@ export function prepareExpressionRowDataForTable( mrnaExpression: mrnaExpressionDataByGeneThenProfile[entrezGeneId], proteinExpression: proteinExpressionDataByGeneThenProfile[entrezGeneId], - mutations: geneToMutationDataMap[entrezGeneId]?.proteinChange, - structuralVariants: - geneToStructuralVariantDataMap[entrezGeneId]?.eventInfo, + mutations: geneToMutationDataMap[entrezGeneId], + structuralVariants: geneToStructuralVariantDataMap[entrezGeneId], cna: cnaDataByGeneThenProfile[entrezGeneId], };