diff --git a/end-to-end-test/local/screenshots/reference/results_view_comparison_tab_alteration_enrichments_several_groups_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/results_view_comparison_tab_alteration_enrichments_several_groups_element_chrome_1600x1000.png index 3b9c6c08bf7..2ad3058c6eb 100644 Binary files a/end-to-end-test/local/screenshots/reference/results_view_comparison_tab_alteration_enrichments_several_groups_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/results_view_comparison_tab_alteration_enrichments_several_groups_element_chrome_1600x1000.png differ diff --git a/packages/cbioportal-ts-api-client/src/index.tsx b/packages/cbioportal-ts-api-client/src/index.tsx index d31a7c5dc98..3237ca62647 100644 --- a/packages/cbioportal-ts-api-client/src/index.tsx +++ b/packages/cbioportal-ts-api-client/src/index.tsx @@ -36,6 +36,9 @@ export { GenericAssayDataBinFilter, GenericAssayDataCountFilter, GenericAssayDataCountItem, + GenericAssayCountSummary, + GenericAssayBinaryEnrichment, + GenericAssayCategoricalEnrichment, GenericAssayEnrichment, Geneset, GenesetCorrelation, diff --git a/src/pages/groupComparison/GenericAssayEnrichmentCollections.tsx b/src/pages/groupComparison/GenericAssayEnrichmentCollections.tsx new file mode 100644 index 00000000000..6a003b6a6a9 --- /dev/null +++ b/src/pages/groupComparison/GenericAssayEnrichmentCollections.tsx @@ -0,0 +1,263 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import autobind from 'autobind-decorator'; +import { MolecularProfile } from 'cbioportal-ts-api-client'; +import { MakeMobxView } from '../../shared/components/MobxView'; +import Loader from '../../shared/components/loadingIndicator/LoadingIndicator'; +import ErrorMessage from '../../shared/components/ErrorMessage'; +import { MakeEnrichmentsTabUI } from './GroupComparisonUtils'; +import _ from 'lodash'; +import ComparisonStore from '../../shared/lib/comparison/ComparisonStore'; +import GenericAssayBinaryEnrichmentsContainer from 'pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsContainer'; +import EnrichmentsDataSetDropdown from 'pages/resultsView/enrichments/EnrichmentsDataSetDropdown'; +import { makeObservable, observable } from 'mobx'; +import GenericAssayEnrichmentsContainer from 'pages/resultsView/enrichments/GenericAssayEnrichmentsContainer'; +import GenericAssayCategoricalEnrichmentsContainer from 'pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsContainer'; + +export interface IGenericAssayEnrichmentCollectionsProps { + store: ComparisonStore; + genericAssayType: string; + resultsViewMode?: boolean; +} + +@observer +export default class GenericAssayEnrichmentCollections extends React.Component< + IGenericAssayEnrichmentCollectionsProps, + {} +> { + constructor(props: IGenericAssayEnrichmentCollectionsProps) { + super(props); + makeObservable(this); + } + + @observable isBinary: boolean | undefined = false; + @observable isCategorical: boolean | undefined = false; + @observable isNumerical: boolean | undefined = false; + + @autobind + private onChangeProfile(profileMap: { + [studyId: string]: MolecularProfile; + }) { + const type = Object.values(profileMap)[0].datatype; + this.props.store.setAllGenericAssayEnrichmentProfileMap( + profileMap, + this.props.genericAssayType + ); + if (type === 'BINARY') { + this.isBinary = true; + this.isCategorical = false; + this.isNumerical = false; + } else if (type === 'CATEGORICAL') { + this.isBinary = false; + this.isCategorical = true; + this.isNumerical = false; + } else { + this.isBinary = false; + this.isCategorical = false; + this.isNumerical = true; + } + } + + readonly tabUI = MakeEnrichmentsTabUI( + () => this.props.store, + () => this.enrichmentsUI, + this.props.genericAssayType, + true, + true, + false + ); + + readonly enrichmentsUI = MakeMobxView({ + await: () => { + const ret: any[] = [ + this.props.store.gaBinaryEnrichmentDataByAssayType, + this.props.store + .selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType, + this.props.store.gaBinaryEnrichmentGroupsByAssayType, + this.props.store.gaCategoricalEnrichmentDataByAssayType, + this.props.store.gaCategoricalEnrichmentGroupsByAssayType, + this.props.store.gaEnrichmentDataByAssayType, + this.props.store.gaEnrichmentGroupsByAssayType, + this.props.store.studies, + ]; + if ( + this.props.store.gaBinaryEnrichmentDataByAssayType.isComplete && + this.props.store.gaBinaryEnrichmentDataByAssayType.result![ + this.props.genericAssayType + ] + ) { + ret.push( + this.props.store.gaBinaryEnrichmentDataByAssayType.result![ + this.props.genericAssayType + ] + ); + } + + if ( + this.props.store.gaCategoricalEnrichmentDataByAssayType + .isComplete && + this.props.store.gaCategoricalEnrichmentDataByAssayType.result![ + this.props.genericAssayType + ] + ) { + ret.push( + this.props.store.gaCategoricalEnrichmentDataByAssayType + .result![this.props.genericAssayType] + ); + } + if ( + this.props.store.gaEnrichmentDataByAssayType.isComplete && + this.props.store.gaEnrichmentDataByAssayType.result![ + this.props.genericAssayType + ] + ) { + ret.push( + this.props.store.gaEnrichmentDataByAssayType.result![ + this.props.genericAssayType + ] + ); + } + return ret; + }, + render: () => { + // since generic assay enrichments tab is enabled only for one study, selectedGenericAssayEnrichmentProfileMap + // would contain only one key. + const studyId = Object.keys( + this.props.store + .selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType + .result![this.props.genericAssayType] + )[0]; + + // select the first found profile in the study as the default selection for selected genericAssayType + const selectedProfile = this.props.store + .selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType + .result![this.props.genericAssayType][studyId]; + + let profileList: MolecularProfile[] = []; + const genericAssayBinaryEnrichmentProfiles = this.props.store + .genericAssayBinaryEnrichmentProfilesGroupedByGenericAssayType + .result![this.props.genericAssayType]; + const genericAssayCategoricalEnrichmentProfiles = this.props.store + .genericAssayCategoricalEnrichmentProfilesGroupedByGenericAssayType + .result![this.props.genericAssayType]; + const genericAssayEnrichmentProfiles = this.props.store + .genericAssayEnrichmentProfilesGroupedByGenericAssayType + .result![this.props.genericAssayType]; + + if (genericAssayBinaryEnrichmentProfiles !== undefined) { + profileList = profileList.concat( + genericAssayBinaryEnrichmentProfiles + ); + } + if (genericAssayCategoricalEnrichmentProfiles !== undefined) { + profileList = profileList.concat( + genericAssayCategoricalEnrichmentProfiles + ); + } + if (genericAssayEnrichmentProfiles !== undefined) { + profileList = profileList.concat( + genericAssayEnrichmentProfiles + ); + } + + if (selectedProfile.datatype == 'BINARY') { + this.isBinary = true; + this.isCategorical = false; + this.isNumerical = false; + } else if (selectedProfile.datatype == 'CATEGORICAL') { + this.isBinary = false; + this.isCategorical = true; + this.isNumerical = false; + } else { + this.isBinary = false; + this.isCategorical = false; + this.isNumerical = true; + } + + return ( +
+ + {this.isBinary && ( + + )} + {this.isCategorical && ( + + )} + {this.isNumerical && ( + + )} +
+ ); + }, + renderPending: () => ( + + ), + renderError: () => , + }); + + render() { + return this.tabUI.component; + } +} diff --git a/src/pages/groupComparison/GroupComparisonPage.tsx b/src/pages/groupComparison/GroupComparisonPage.tsx index 47255ad88d1..73abcfdb05e 100644 --- a/src/pages/groupComparison/GroupComparisonPage.tsx +++ b/src/pages/groupComparison/GroupComparisonPage.tsx @@ -36,7 +36,6 @@ import styles from './styles.module.scss'; import { OverlapStrategy } from '../../shared/lib/comparison/ComparisonStore'; import { buildCBioPortalPageUrl } from 'shared/api/urls'; import MethylationEnrichments from './MethylationEnrichments'; -import GenericAssayEnrichments from './GenericAssayEnrichments'; import _ from 'lodash'; import AlterationEnrichments from './AlterationEnrichments'; import AlterationEnrichmentTypeSelector from '../../shared/lib/comparison/AlterationEnrichmentTypeSelector'; @@ -46,12 +45,13 @@ import { buildCustomTabs, prepareCustomTabConfigurations, } from 'shared/lib/customTabs/customTabHelpers'; -import { getSortedGenericAssayTabSpecs } from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils'; +import { getSortedGenericAssayAllTabSpecs } from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils'; import { HelpWidget } from 'shared/components/HelpWidget/HelpWidget'; import GroupComparisonPathwayMapper from './pathwayMapper/GroupComparisonPathwayMapper'; import GroupComparisonMutationsTab from './GroupComparisonMutationsTab'; import GroupComparisonPathwayMapperUserSelectionStore from './pathwayMapper/GroupComparisonPathwayMapperUserSelectionStore'; import { Tour } from 'tours'; +import GenericAssayEnrichmentCollections from './GenericAssayEnrichmentCollections'; export interface IGroupComparisonPageProps { routing: any; @@ -135,6 +135,8 @@ export default class GroupComparisonPage extends React.Component< this.store.methylationEnrichmentProfiles, this.store.survivalClinicalDataExists, this.store.genericAssayEnrichmentProfilesGroupedByGenericAssayType, + this.store + .genericAssayBinaryEnrichmentProfilesGroupedByGenericAssayType, this.store.alterationsEnrichmentData, this.store.alterationsEnrichmentAnalysisGroups, this.store.genesSortedByMutationFrequency, @@ -313,34 +315,39 @@ export default class GroupComparisonPage extends React.Component< )} - {this.store.showGenericAssayTab && - getSortedGenericAssayTabSpecs( + {(this.store.showGenericAssayCategoricalTab || + this.store.showGenericAssayBinaryTab || + this.store.showGenericAssayTab) && + getSortedGenericAssayAllTabSpecs( this.store - .genericAssayEnrichmentProfilesGroupedByGenericAssayType + .genericAssayAllEnrichmentProfilesGroupedByGenericAssayType .result - ).map(genericAssayTabSpecs => { + ).map(genericAssayAllTabSpecs => { return ( - ); })} - {buildCustomTabs(this.customTabs)} ); diff --git a/src/pages/groupComparison/GroupComparisonTabs.ts b/src/pages/groupComparison/GroupComparisonTabs.ts index 6b77ea52f4d..83db87562b4 100644 --- a/src/pages/groupComparison/GroupComparisonTabs.ts +++ b/src/pages/groupComparison/GroupComparisonTabs.ts @@ -7,6 +7,8 @@ export enum GroupComparisonTab { DNAMETHYLATION = 'dna_methylation', ALTERATIONS = 'alterations', GENERIC_ASSAY_PREFIX = 'generic_assay', + GENERIC_ASSAY_BINARY_PREFIX = 'generic_assay_binary', + GENERIC_ASSAY_CATEGORICAL_PREFIX = 'generic_assay_categorical', MUTATIONS = 'mutations', PATHWAYS = 'pathways', } diff --git a/src/pages/resultsView/ResultsViewPageStoreUtils.ts b/src/pages/resultsView/ResultsViewPageStoreUtils.ts index d98a7d7aa2c..62fba902882 100644 --- a/src/pages/resultsView/ResultsViewPageStoreUtils.ts +++ b/src/pages/resultsView/ResultsViewPageStoreUtils.ts @@ -4,6 +4,8 @@ import { ClinicalData, DiscreteCopyNumberData, GenericAssayEnrichment, + GenericAssayBinaryEnrichment, + GenericAssayCategoricalEnrichment, MolecularProfile, Mutation, NumericGeneMolecularData, @@ -749,6 +751,61 @@ export function makeGenericAssayEnrichmentDataPromise(params: { }); } +export function makeGenericAssayBinaryEnrichmentDataPromise(params: { + resultViewPageStore?: ResultsViewPageStore; + await: MobxPromise_await; + getSelectedProfileMap: () => { [studyId: string]: MolecularProfile }; + fetchData: () => Promise; +}): MobxPromise { + return remoteData({ + await: () => { + const ret = params.await(); + if (params.resultViewPageStore) { + ret.push(params.resultViewPageStore.selectedMolecularProfiles); + } + return ret; + }, + invoke: async () => { + const profileMap = params.getSelectedProfileMap(); + if (profileMap) { + let data = await params.fetchData(); + return calculateQValuesAndSortEnrichmentData( + data, + sortGenericAssayEnrichmentData + ); + } else { + return []; + } + }, + }); +} + +export function makeGenericAssayCategoricalEnrichmentDataPromise(params: { + resultViewPageStore?: ResultsViewPageStore; + await: MobxPromise_await; + getSelectedProfileMap: () => { [studyId: string]: MolecularProfile }; + fetchData: () => Promise; +}): MobxPromise { + return remoteData({ + await: () => { + const ret = params.await(); + if (params.resultViewPageStore) { + ret.push(params.resultViewPageStore.selectedMolecularProfiles); + } + return ret; + }, + invoke: async () => { + const profileMap = params.getSelectedProfileMap(); + if (profileMap) { + let data = await params.fetchData(); + return data; + } else { + return []; + } + }, + }); +} + function sortGenericAssayEnrichmentData( data: GenericAssayEnrichmentWithQ[] ): GenericAssayEnrichmentWithQ[] { diff --git a/src/pages/resultsView/comparison/ComparisonTab.tsx b/src/pages/resultsView/comparison/ComparisonTab.tsx index a45794a5f6d..286fb7dbe63 100644 --- a/src/pages/resultsView/comparison/ComparisonTab.tsx +++ b/src/pages/resultsView/comparison/ComparisonTab.tsx @@ -30,12 +30,15 @@ import GroupSelector from '../../groupComparison/groupSelector/GroupSelector'; import CaseFilterWarning from '../../../shared/components/banners/CaseFilterWarning'; import MethylationEnrichments from 'pages/groupComparison/MethylationEnrichments'; import AlterationEnrichments from 'pages/groupComparison/AlterationEnrichments'; +import GenericAssayEnrichmentCollections from 'pages/groupComparison/GenericAssayEnrichmentCollections'; import AlterationEnrichmentTypeSelector from 'shared/lib/comparison/AlterationEnrichmentTypeSelector'; -import GenericAssayEnrichments from 'pages/groupComparison/GenericAssayEnrichments'; import styles from 'pages/resultsView/comparison/styles.module.scss'; import { getServerConfig } from 'config/config'; import { AlterationFilterMenuSection } from 'pages/groupComparison/GroupComparisonUtils'; -import { getSortedGenericAssayTabSpecs } from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils'; +import { + getSortedGenericAssayAllTabSpecs, + getSortedGenericAssayTabSpecs, +} from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils'; export interface IComparisonTabProps { urlWrapper: ResultsViewURLWrapper; @@ -263,28 +266,34 @@ export default class ComparisonTab extends React.Component< /> )} - {this.store.showGenericAssayTab && - getSortedGenericAssayTabSpecs( + {(this.store.showGenericAssayCategoricalTab || + this.store.showGenericAssayBinaryTab || + this.store.showGenericAssayTab) && + getSortedGenericAssayAllTabSpecs( this.store - .genericAssayEnrichmentProfilesGroupedByGenericAssayType + .genericAssayAllEnrichmentProfilesGroupedByGenericAssayType .result - ).map(genericAssayTabSpecs => { + ).map(genericAssayAllTabSpecs => { return ( - diff --git a/src/pages/resultsView/enrichments/EnrichmentsUtil.tsx b/src/pages/resultsView/enrichments/EnrichmentsUtil.tsx index 95507aba0d2..64aafcc005a 100644 --- a/src/pages/resultsView/enrichments/EnrichmentsUtil.tsx +++ b/src/pages/resultsView/enrichments/EnrichmentsUtil.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import { AlterationEnrichment, GenericAssayEnrichment, + GenericAssayBinaryEnrichment, + GenericAssayCategoricalEnrichment, GenomicEnrichment, MolecularProfile, } from 'cbioportal-ts-api-client'; @@ -9,6 +11,8 @@ import { AlterationEnrichmentRow } from 'shared/model/AlterationEnrichmentRow'; import { ExpressionEnrichmentRow, GenericAssayEnrichmentRow, + GenericAssayBinaryEnrichmentRow, + GenericAssayCategoricalEnrichmentRow, } from 'shared/model/EnrichmentRow'; import { formatLogOddsRatio, roundLogRatio } from 'shared/lib/FormatUtils'; import _ from 'lodash'; @@ -33,6 +37,11 @@ import { GenericAssayEnrichmentTableColumn, GenericAssayEnrichmentTableColumnType, } from './GenericAssayEnrichmentsTable'; +import { + GenericAssayBinaryEnrichmentTableColumn, + GenericAssayBinaryEnrichmentTableColumnType, +} from './GenericAssayBinaryEnrichmentsTable'; +import { GenericAssayCategoricalEnrichmentTableColumn } from './GenericAssayCategoricalEnrichmentsTable'; export type AlterationEnrichmentWithQ = AlterationEnrichment & { logRatio?: number; @@ -73,6 +82,13 @@ export enum GeneOptionLabel { SYNC_WITH_TABLE = 'Sync with table (up to 100 genes)', } +export enum GaBinaryOptionLabel { + HIGHEST_FREQUENCY = 'Entities with highest frequency in any group', + AVERAGE_FREQUENCY = 'Entities with highest average frequency', + SIGNIFICANT_P_VALUE = 'Entities with most significant p-value', + SYNC_WITH_TABLE = 'Sync with table (up to 100 entities)', +} + export enum AlterationContainerType { MUTATION = 'MUTATION', COPY_NUMBER = 'COPY_NUMBER', @@ -120,6 +136,14 @@ export function formatPercentage( ); } +export function formatGenericAssayPercentage( + group: string, + data: GenericAssayBinaryEnrichmentRow +): string { + const datum = data.groupsSet[group]; + return datum.count + ' (' + datum.alteredPercentage.toFixed(2) + '%)'; +} + export function getProfiledCount( group: string, data: AlterationEnrichmentRow @@ -134,6 +158,20 @@ export function getAlteredCount( return data.groupsSet[group].alteredCount; } +export function getCount( + group: string, + data: GenericAssayBinaryEnrichmentRow +): number { + return data.groupsSet[group].count; +} + +export function getTotalCount( + group: string, + data: GenericAssayBinaryEnrichmentRow +): number { + return data.groupsSet[group].totalCount; +} + function volcanoPlotYCoord(pValue: number) { if (pValue === 0 || Math.log10(pValue) < -10) { return 10; @@ -228,6 +266,45 @@ export function getGenericAssayScatterData( }); } +export function getGenericAssayBinaryScatterData( + genericAssayBinaryEnrichments: GenericAssayBinaryEnrichmentRow[] +): any[] { + return genericAssayBinaryEnrichments.map(genericAssayBinaryEnrichment => { + return { + x: roundLogRatio(Number(genericAssayBinaryEnrichment.logRatio), 10), + y: volcanoPlotYCoord(genericAssayBinaryEnrichment.pValue), + stableId: genericAssayBinaryEnrichment.stableId, + entityName: genericAssayBinaryEnrichment.entityName, + pValue: genericAssayBinaryEnrichment.pValue, + qValue: genericAssayBinaryEnrichment.qValue, + logRatio: genericAssayBinaryEnrichment.logRatio, + hovered: false, + }; + }); +} + +export function getGaBinaryFrequencyScatterData( + genericAssayBinaryEnrichments: GenericAssayBinaryEnrichmentRow[], + group1: string, + group2: string +): IMiniFrequencyScatterChartData[] { + return genericAssayBinaryEnrichments + .filter(a => a.pValue !== undefined && a.qValue !== undefined) + .map(genericAssayBinaryEnrichment => { + return { + x: + genericAssayBinaryEnrichment.groupsSet[group1] + .alteredPercentage, + y: + genericAssayBinaryEnrichment.groupsSet[group2] + .alteredPercentage, + pValue: genericAssayBinaryEnrichment.pValue!, + qValue: genericAssayBinaryEnrichment.qValue!, + hugoGeneSymbol: '', + logRatio: genericAssayBinaryEnrichment.logRatio!, + }; + }); +} export function getAlterationRowData( alterationEnrichments: AlterationEnrichmentWithQ[], queryGenes: string[], @@ -376,11 +453,130 @@ export function getGenericAssayEnrichmentRowData( }); } +export function getGenericAssayBinaryEnrichmentRowData( + genericAssayBinaryEnrichments: GenericAssayBinaryEnrichment[], + groups: { name: string; nameOfEnrichmentDirection?: string }[] +): GenericAssayBinaryEnrichmentRow[] { + return genericAssayBinaryEnrichments.map(genericAssayBinaryEnrichment => { + let countsWithAlteredPercentage = _.map( + genericAssayBinaryEnrichment.counts, + datum => { + const alteredPercentage = + datum.count > 0 && datum.totalCount > 0 + ? (datum.count / datum.totalCount) * 100 + : 0; + return { + ...datum, + alteredPercentage, + }; + } + ); + let enrichedGroup = ''; + // fallback to stable id if name is not specified + let entityName = + 'NAME' in genericAssayBinaryEnrichment.genericEntityMetaProperties + ? genericAssayBinaryEnrichment.genericEntityMetaProperties[ + 'NAME' + ] + : genericAssayBinaryEnrichment.stableId; + let logRatio: number | undefined = undefined; + let groupsSet = _.keyBy( + countsWithAlteredPercentage, + group => group.name + ); + + if (groups.length === 2) { + let group1Data = groupsSet[groups[0].name]; + let group2Data = groupsSet[groups[1].name]; + if (genericAssayBinaryEnrichment.pValue !== undefined) { + logRatio = Math.log2( + group1Data.alteredPercentage / group2Data.alteredPercentage + ); + let group1Name = + groups[0].nameOfEnrichmentDirection || groups[0].name; + let group2Name = + groups[1].nameOfEnrichmentDirection || groups[1].name; + enrichedGroup = logRatio > 0 ? group1Name : group2Name; + } + } else if (genericAssayBinaryEnrichment.pValue !== undefined) { + countsWithAlteredPercentage.sort( + (a, b) => b.alteredPercentage - a.alteredPercentage + ); + enrichedGroup = countsWithAlteredPercentage[0].name; + } + + return { + checked: false, + disabled: false, + logRatio, + pValue: genericAssayBinaryEnrichment.pValue, + qValue: genericAssayBinaryEnrichment.qValue, + enrichedGroup, + stableId: genericAssayBinaryEnrichment.stableId, + entityName, + groupsSet, + }; + }); +} + +export function getGenericAssayCategoricalEnrichmentRowData( + genericAssayCategoricalEnrichments: GenericAssayCategoricalEnrichment[], + groups: { name: string; nameOfEnrichmentDirection?: string }[] +): GenericAssayCategoricalEnrichmentRow[] { + return genericAssayCategoricalEnrichments.map( + genericAssayCategoricalEnrichment => { + let enrichedGroup = ''; + // fallback to stable id if name is not specified + let entityName = + 'NAME' in + genericAssayCategoricalEnrichment.genericEntityMetaProperties + ? genericAssayCategoricalEnrichment + .genericEntityMetaProperties['NAME'] + : genericAssayCategoricalEnrichment.stableId; + let logRatio: number | undefined = undefined; + let groupsSet = _.keyBy( + genericAssayCategoricalEnrichment.groupsStatistics, + group => group.name + ); + + if (groups.length === 2) { + let group1Data = groupsSet[groups[0].name]; + let group2Data = groupsSet[groups[1].name]; + logRatio = + group1Data.meanExpression - group2Data.meanExpression; + let group1Name = + groups[0].nameOfEnrichmentDirection || groups[0].name; + let group2Name = + groups[1].nameOfEnrichmentDirection || groups[1].name; + enrichedGroup = logRatio > 0 ? group1Name : group2Name; + } else { + enrichedGroup = genericAssayCategoricalEnrichment.groupsStatistics.sort( + (a, b) => b.meanExpression - a.meanExpression + )[0].name; + } + + return { + checked: false, + disabled: false, + enrichedGroup: enrichedGroup, + pValue: genericAssayCategoricalEnrichment.pValue, + qValue: genericAssayCategoricalEnrichment.qValue, + stableId: genericAssayCategoricalEnrichment.stableId, + entityName, + attributeType: 'Sample', + statisticalTest: 'Chi-squared Test', + groupsSet, + }; + } + ); +} export function getFilteredData( data: ( | ExpressionEnrichmentRow | AlterationEnrichmentRow | GenericAssayEnrichmentRow + | GenericAssayBinaryEnrichmentRow + | GenericAssayCategoricalEnrichmentRow )[], expressedGroups: string[], qValueFilter: boolean, @@ -426,7 +622,10 @@ export function getFilteredData( result = result && filterFunction( - (enrichmentDatum as GenericAssayEnrichmentRow).stableId + (enrichmentDatum as + | GenericAssayEnrichmentRow + | GenericAssayBinaryEnrichmentRow + | GenericAssayCategoricalEnrichmentRow).stableId ); } else { result = @@ -442,6 +641,19 @@ export function getFilteredData( }); } +export function getFilteredCategoricalData( + data: GenericAssayCategoricalEnrichmentRow[], + filterFunction: (value: string) => boolean +): GenericAssayCategoricalEnrichmentRow[] { + return data.filter(enrichmentDatum => { + let result = false; + result = filterFunction( + (enrichmentDatum as GenericAssayCategoricalEnrichmentRow).stableId + ); + return result; + }); +} + export function getBarChartTooltipContent( tooltipModel: any, selectedGene: string @@ -556,6 +768,22 @@ export function pickMethylationEnrichmentProfiles( }); } +export function pickAllGenericAssayEnrichmentProfiles( + profiles: MolecularProfile[] +) { + // TODO: enable all patient-level profile after confirming patient-level data is compatible with enrichment feature + return profiles.filter(p => { + return ( + p.molecularAlterationType === + AlterationTypeConstants.GENERIC_ASSAY && + (((p.datatype === DataTypeConstants.BINARY || + p.datatype === DataTypeConstants.CATEGORICAL) && + p.patientLevel === false) || + p.datatype === DataTypeConstants.LIMITVALUE) + ); + }); +} + export function pickGenericAssayEnrichmentProfiles( profiles: MolecularProfile[] ) { @@ -569,6 +797,30 @@ export function pickGenericAssayEnrichmentProfiles( }); } +export function pickGenericAssayBinaryEnrichmentProfiles( + profiles: MolecularProfile[] +) { + return profiles.filter(p => { + return ( + p.molecularAlterationType === + AlterationTypeConstants.GENERIC_ASSAY && + p.datatype === DataTypeConstants.BINARY + ); + }); +} + +export function pickGenericAssayCategoricalEnrichmentProfiles( + profiles: MolecularProfile[] +) { + return profiles.filter(p => { + return ( + p.molecularAlterationType === + AlterationTypeConstants.GENERIC_ASSAY && + p.datatype === DataTypeConstants.CATEGORICAL && + p.patientLevel === false + ); + }); +} export function getAlterationEnrichmentColumns( groups: { name: string; description: string; color?: string }[], alteredVsUnalteredMode?: boolean @@ -1002,8 +1254,154 @@ export function getGenericAssayEnrichmentColumns( return columns; } +export function getGenericAssayBinaryEnrichmentColumns( + groups: { name: string; description: string; color?: string }[], + alteredVsUnalteredMode?: boolean +): GenericAssayBinaryEnrichmentTableColumn[] { + // minimum 2 group are required for enrichment analysis + if (groups.length < 2) { + return []; + } + let columns: GenericAssayBinaryEnrichmentTableColumn[] = []; + const nameToGroup = _.keyBy(groups, g => g.name); + + let enrichedGroupColum: GenericAssayBinaryEnrichmentTableColumn = { + name: alteredVsUnalteredMode + ? GenericAssayBinaryEnrichmentTableColumnType.TENDENCY + : groups.length === 2 + ? GenericAssayBinaryEnrichmentTableColumnType.ENRICHED + : GenericAssayBinaryEnrichmentTableColumnType.MOST_ENRICHED, + render: (d: GenericAssayBinaryEnrichmentRow) => { + if (d.pValue === undefined) { + return -; + } + let groupColor = undefined; + const significant = d.qValue < 0.05; + if (!alteredVsUnalteredMode && significant) { + groupColor = nameToGroup[d.enrichedGroup].color; + } + return ( +
+ {alteredVsUnalteredMode + ? d.enrichedGroup + : formatAlterationTendency(d.enrichedGroup)} +
+ ); + }, + filter: ( + d: GenericAssayBinaryEnrichmentRow, + filterString: string, + filterStringUpper: string + ) => (d.enrichedGroup || '').toUpperCase().includes(filterStringUpper), + sortBy: (d: GenericAssayBinaryEnrichmentRow) => d.enrichedGroup || '-', + download: (d: GenericAssayBinaryEnrichmentRow) => + d.enrichedGroup || '-', + tooltip: The group with the highest alteration frequency, + }; + + if (groups.length === 2) { + let group1 = groups[0]; + let group2 = groups[1]; + columns.push({ + name: GenericAssayBinaryEnrichmentTableColumnType.LOG_RATIO, + render: (d: GenericAssayBinaryEnrichmentRow) => ( + {d.logRatio ? formatLogOddsRatio(d.logRatio) : '-'} + ), + tooltip: ( + + Log2 based ratio of (pct in {group1.name}/ pct in{' '} + {group2.name}) + + ), + sortBy: (d: GenericAssayBinaryEnrichmentRow) => Number(d.logRatio), + download: (d: GenericAssayBinaryEnrichmentRow) => + d.logRatio ? formatLogOddsRatio(d.logRatio) : '-', + }); + + enrichedGroupColum.tooltip = ( + + + + + + + + + + + + + +
Log ratio {'>'} 0: Enriched in {group1.name}
Log ratio <= 0: Enriched in {group2.name}
q-Value < 0.05: Significant association
+ ); + } + columns.push(enrichedGroupColum); + groups.forEach(group => { + columns.push({ + name: group.name, + headerRender: PERCENTAGE_IN_headerRender, + render: (d: GenericAssayBinaryEnrichmentRow) => { + let overlay = ( + + {getTotalCount(group.name, d)} samples in {group.name}{' '} + are profiled for {d.entityName},  + {formatGenericAssayPercentage(group.name, d)} of which + are altered in {d.entityName} + + ); + return ( + + + {formatGenericAssayPercentage(group.name, d)} + + + ); + }, + tooltip: ( + + {group.name}: {group.description} + + ), + sortBy: (d: GenericAssayBinaryEnrichmentRow) => + getCount(group.name, d), + download: (d: GenericAssayBinaryEnrichmentRow) => + formatGenericAssayPercentage(group.name, d), + }); + }); + return columns; +} + +export function getGenericAssayCategoricalEnrichmentColumns( + groups: { name: string; description: string; color?: string }[], + alteredVsUnalteredMode?: boolean +): GenericAssayCategoricalEnrichmentTableColumn[] { + // minimum 2 group are required for enrichment analysis + if (groups.length < 2) { + return []; + } + let columns: GenericAssayCategoricalEnrichmentTableColumn[] = []; + + return columns; +} export function getEnrichmentBarPlotData( - data: { [gene: string]: AlterationEnrichmentRow }, + data: { + [gene: string]: + | AlterationEnrichmentRow + | GenericAssayBinaryEnrichmentRow; + }, genes: string[] ): IMultipleCategoryBarPlotData[] { const usedGenes: { [gene: string]: boolean } = {}; @@ -1063,8 +1461,8 @@ export function getEnrichmentBarPlotData( } export function compareByAlterationPercentage( - kv1: AlterationEnrichmentRow, - kv2: AlterationEnrichmentRow + kv1: AlterationEnrichmentRow | GenericAssayBinaryEnrichmentRow, + kv2: AlterationEnrichmentRow | GenericAssayBinaryEnrichmentRow ) { const t1 = _.reduce( kv1.groupsSet, @@ -1179,6 +1577,91 @@ export function getGeneListOptions( ]; } +export function getGaBinarydataListOptions( + data: GenericAssayBinaryEnrichmentRow[], + includeAlteration?: boolean +): { label: GaBinaryOptionLabel; entities: string[] }[] { + if (_.isEmpty(data)) { + return [ + { + label: GaBinaryOptionLabel.SYNC_WITH_TABLE, + entities: [], + }, + ]; + } + + let dataWithOptionName: (GenericAssayBinaryEnrichmentRow & { + optionName?: string; + })[] = data; + + let dataSortedByAlteredPercentage = _.clone(dataWithOptionName).sort( + compareByAlterationPercentage + ); + + let dataSortedByAvgFrequency = _.clone(dataWithOptionName).sort(function( + dataItem1, + dataItem2 + ) { + const averageAlteredPercentage1 = + _.sumBy( + _.values(dataItem1.groupsSet), + group => group.alteredPercentage + ) / _.keys(dataItem1.groupsSet).length; + + const averageAlteredPercentage2 = + _.sumBy( + _.values(dataItem2.groupsSet), + group => group.alteredPercentage + ) / _.keys(dataItem2.groupsSet).length; + + return averageAlteredPercentage2 - averageAlteredPercentage1; + }); + + let dataSortedBypValue = _.clone(dataWithOptionName).sort(function( + dataItem1, + dataItem2 + ) { + if (dataItem1.pValue !== undefined && dataItem2.pValue !== undefined) { + return 0; + } + if (dataItem1.pValue !== undefined) { + return 1; + } + if (dataItem2.pValue !== undefined) { + return -1; + } + return Number(dataItem1.pValue) - Number(dataItem2.pValue); + }); + + return [ + { + label: GaBinaryOptionLabel.HIGHEST_FREQUENCY, + entities: _.map( + dataSortedByAlteredPercentage, + datum => datum.optionName || datum.entityName + ), + }, + { + label: GaBinaryOptionLabel.AVERAGE_FREQUENCY, + entities: _.map( + dataSortedByAvgFrequency, + datum => datum.optionName || datum.entityName + ), + }, + { + label: GaBinaryOptionLabel.SIGNIFICANT_P_VALUE, + entities: _.map( + dataSortedBypValue, + datum => datum.optionName || datum.entityName + ), + }, + { + label: GaBinaryOptionLabel.SYNC_WITH_TABLE, + entities: [], + }, + ]; +} + export const ContinousDataPvalueTooltip: React.FunctionComponent = ({ groupSize, }) => { diff --git a/src/pages/resultsView/enrichments/GenericAssayBarPlot.tsx b/src/pages/resultsView/enrichments/GenericAssayBarPlot.tsx new file mode 100644 index 00000000000..121741f0f27 --- /dev/null +++ b/src/pages/resultsView/enrichments/GenericAssayBarPlot.tsx @@ -0,0 +1,518 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, computed, makeObservable } from 'mobx'; +import { DownloadControls, DefaultTooltip } from 'cbioportal-frontend-commons'; +import autobind from 'autobind-decorator'; +import MultipleCategoryBarPlot from 'pages/groupComparison/MultipleCategoryBarPlot'; +import ReactSelect from 'react-select'; +import _ from 'lodash'; +import { + getEnrichmentBarPlotData, + getGaBinarydataListOptions, + GaBinaryOptionLabel, +} from './EnrichmentsUtil'; +import styles from './frequencyPlotStyles.module.scss'; +import { toConditionalPrecision } from 'shared/lib/NumberUtils'; +import { FormControl } from 'react-bootstrap'; +import { GenericAssayBinaryEnrichmentsTableDataStore } from './GenericAssayBinaryEnrichmentsTableDataStore'; +import { GenericAssayBinaryEnrichmentRow } from 'shared/model/EnrichmentRow'; + +export interface IGenericAssayBarPlotProps { + data: GenericAssayBinaryEnrichmentRow[]; + groupOrder?: string[]; + isTwoGroupAnalysis?: boolean; + yAxisLabel: string; + categoryToColor?: { + [id: string]: string; + }; + dataStore: GenericAssayBinaryEnrichmentsTableDataStore; +} +export declare type SingleEntityQuery = { + entity: string; +}; +const DEFAULT_ENTITIES_COUNT = 10; + +const MAXIMUM_ALLOWED_ENTITIES = 100; + +const CHART_BAR_WIDTH = 10; + +@observer +export default class GenericAssayBarPlot extends React.Component< + IGenericAssayBarPlotProps, + {} +> { + @observable tooltipModel: any; + @observable.ref _entityQuery: string | undefined = undefined; + @observable selectedEntities: SingleEntityQuery[] | undefined; + @observable numberOfEntities: Number | undefined = 0; + @observable _label: GaBinaryOptionLabel | undefined; + @observable isEntitySelectionPopupVisible: boolean | undefined = false; + @observable private svgContainer: SVGElement | null; + + constructor(props: any) { + super(props); + makeObservable(this); + } + + @computed get entityListOptions() { + return getGaBinarydataListOptions(this.props.data); + } + + @computed get defaultOption() { + return this.entityListOptions.length > 1 + ? this.entityListOptions[1] + : this.entityListOptions[0]; + } + + @computed get gaBinaryDataSet() { + return _.keyBy(this.props.data, datum => { + return datum.entityName; + }); + } + + @computed get barPlotData() { + return getEnrichmentBarPlotData( + this.gaBinaryDataSet, + this.barPlotOrderedEntities + ); + } + + @computed get barPlotOrderedEntities() { + let entities: string[] = []; + if (this._label === GaBinaryOptionLabel.SYNC_WITH_TABLE) { + return this.tableSelectedEntities; + } + if (!this.selectedEntities) { + entities = this.defaultOption.entities.slice( + 0, + DEFAULT_ENTITIES_COUNT + ); + } else { + entities = this.selectedEntities + .slice(0, Number(this.numberOfEntities)) + .map(entityWithAlteration => entityWithAlteration.entity); + } + return entities; + } + + @computed get horzCategoryOrder() { + //include significant entities + return _.flatMap(this.barPlotOrderedEntities, entity => [ + entity + '*', + entity, + ]); + } + + @autobind + private getTooltip(datum: any) { + let entityName = datum.majorCategory as string; + // get rid of a trailing * + entityName = entityName.replace(/\*$/, ''); + let entityData = this.gaBinaryDataSet[entityName]; + //use groupOrder inorder of sorted groups + let groupRows = _.map(this.props.groupOrder, groupName => { + const group = entityData.groupsSet[groupName]; + let style: any = {}; + //bold row corresponding to highlighed bar + if (datum.minorCategory === group.name) { + style = { fontWeight: 'bold' }; + } + return ( + + {group.name} + + {group.alteredPercentage.toFixed(2)}% ({group.count}/ + {group.totalCount}) + + + ); + }); + + return ( +
+ + {entityName} {this.props.yAxisLabel} + +
+ + + + + + + + {groupRows} +
GroupPercentage Altered
+ p-Value:{' '} + {entityData.pValue + ? toConditionalPrecision(entityData.pValue, 3, 0.01) + : '-'} +
+ q-Value:{' '} + {entityData.qValue + ? toConditionalPrecision(entityData.qValue, 3, 0.01) + : '-'} +
+ ); + } + + @computed private get selectedOption() { + if (this._label && this._entityQuery !== undefined) { + return { + label: this._label, + value: this._entityQuery, + }; + } + //default option + return { + label: this.defaultOption.label, + value: this.defaultOption.entities + .slice(0, DEFAULT_ENTITIES_COUNT) + .join('\n'), + }; + } + + @computed get toolbar() { + return ( + +
+ {this.selectedOption.label} +
+
+
+ { + this.isEntitySelectionPopupVisible = visible; + }} + overlay={ + { + this._entityQuery = value; + this.selectedEntities = entities; + this._label = label; + this.numberOfEntities = number; + this.isEntitySelectionPopupVisible = false; + }} + defaultNumberOfEntities={ + DEFAULT_ENTITIES_COUNT + } + /> + } + placement="bottomLeft" + > +
+ +
+
+ this.svgContainer} + filename={'GroupComparisonGeneFrequencyPlot'} + dontFade={true} + type="button" + /> +
+
+
+ ); + } + + @computed private get tableSelectedEntities() { + if (this.props.dataStore.visibleData !== null) { + return this.props.dataStore.visibleData + .map(x => x.entityName) + .slice(0, MAXIMUM_ALLOWED_ENTITIES); + } + return []; + } + + public render() { + return ( +
+ {this.toolbar} +
+ (this.svgContainer = ref)} + /> +
+
+ ); + } +} + +interface IEntitySelectionProps { + tableData: SingleEntityQuery[]; + options: { label: GaBinaryOptionLabel; entities: string[] }[]; + selectedOption?: { label: GaBinaryOptionLabel; value: string }; + onSelectedEntitiesChange: ( + value: string, + orderedEntities: SingleEntityQuery[], + numberOfEntities: Number, + label: GaBinaryOptionLabel + ) => void; + defaultNumberOfEntities: number; + maxNumberOfEntities?: number; +} + +@observer +export class EntitySelection extends React.Component< + IEntitySelectionProps, + {} +> { + static defaultProps: Partial = { + maxNumberOfEntities: MAXIMUM_ALLOWED_ENTITIES, + }; + + constructor(props: IEntitySelectionProps) { + super(props); + makeObservable(this); + (window as any).entitySelection = this; + } + + @observable.ref _entityQuery: string | undefined = undefined; + @observable selectedEntitiesHasError = false; + @observable private numberOfEntities = this.props.defaultNumberOfEntities; + @observable private _selectedEntityListOption: + | { + label: GaBinaryOptionLabel; + value: string; + entities: string[]; + } + | undefined; + @observable.ref entitiesToPlot: SingleEntityQuery[] = []; + + @computed get entityListOptions() { + return _.map(this.props.options, option => { + return { + label: option.label, + value: option.entities.join('\n'), + }; + }); + } + + @computed get entityOptionSet() { + return _.keyBy(this.props.options, option => option.label); + } + + @computed get selectedEntityListOption() { + if ( + this._selectedEntityListOption === undefined && + this.props.selectedOption + ) { + const selectedOption = this.props.selectedOption; + return this.entityListOptions.find(opt => + opt.value.startsWith(selectedOption.value) + ); + } + return this._selectedEntityListOption; + } + + @computed get entityQuery() { + return this._entityQuery === undefined && this.props.selectedOption + ? this.props.selectedOption.value + : this._entityQuery || ''; + } + + @computed get hasUnsupportedOQL() { + return false; + } + + @computed get addGenesButtonDisabled() { + if (this.inSyncMode) { + return ( + this.props.selectedOption !== undefined && + this.props.selectedOption.label === + GaBinaryOptionLabel.SYNC_WITH_TABLE + ); + } else { + return ( + this.hasUnsupportedOQL || + (this.props.selectedOption && + this.props.selectedOption.value === this._entityQuery) || + this.selectedEntitiesHasError || + _.isEmpty(this._entityQuery) + ); + } + } + + @action.bound + public onEntityListOptionChange(option: any) { + this._selectedEntityListOption = option; + if (option.value !== '') { + const entities = this.entityOptionSet[option.label].entities; + this._entityQuery = entities + .slice(0, this.numberOfEntities) + .join('\n'); + } else { + this._entityQuery = ''; + } + this.updateEntityQuery(); + } + + @computed private get inSyncMode() { + return ( + this._selectedEntityListOption && + this._selectedEntityListOption.label === + GaBinaryOptionLabel.SYNC_WITH_TABLE + ); + } + + @action.bound + private handleTotalInputChange(e: any) { + const newCount: number = e.target.value.replace(/[^0-9]/g, ''); + if (newCount <= this.props.maxNumberOfEntities!) { + this.numberOfEntities = newCount; + } + } + + @action.bound + private handleTotalInputKeyPress(target: any) { + if (target.charCode === 13) { + if (isNaN(this.numberOfEntities)) { + this.numberOfEntities = 0; + return; + } + this.updateEntityQuery(); + } + } + + @action.bound + private onBlur() { + if (isNaN(this.numberOfEntities)) { + this.numberOfEntities = 0; + return; + } + this.updateEntityQuery(); + } + + @action.bound + private updateEntityQuery() { + //removes leading 0s + this.numberOfEntities = Number(this.numberOfEntities); + if (this.selectedEntityListOption) { + const label = this.selectedEntityListOption.label; + const entities = this.entityOptionSet[label].entities; + if (entities.length > 0) { + this._entityQuery = entities + .slice(0, this.numberOfEntities) + .join('\n'); + const tmp: SingleEntityQuery[] = []; + for (const s of entities.slice(0, this.numberOfEntities)) { + tmp.push({ + entity: s, + }); + } + this.entitiesToPlot = tmp; + } + } + } + + public render() { + return ( +
+ {this.props.options.length > 0 && ( +
+ +
+ )} + {!this.inSyncMode && ( +
+
+
+ + +
+
+ )} +
+
+ +
+
+ ); + } +} diff --git a/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsContainer.tsx b/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsContainer.tsx new file mode 100644 index 00000000000..9497271a7f5 --- /dev/null +++ b/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsContainer.tsx @@ -0,0 +1,528 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, computed, action, makeObservable } from 'mobx'; +import styles from './styles.module.scss'; +import { + MolecularProfile, + Sample, + GenericAssayBinaryEnrichment, +} from 'cbioportal-ts-api-client'; +import { + getGenericAssayBinaryEnrichmentRowData, + getGenericAssayBinaryEnrichmentColumns, + getGenericAssayBinaryScatterData, + getFilteredData, + getGaBinaryFrequencyScatterData, +} from 'pages/resultsView/enrichments/EnrichmentsUtil'; +import _ from 'lodash'; +import autobind from 'autobind-decorator'; +import { MiniOncoprint } from 'shared/components/miniOncoprint/MiniOncoprint'; +import { + CheckedSelect, + Option, + DefaultTooltip, +} from 'cbioportal-frontend-commons'; +import GenericAssayBinaryEnrichmentsTable, { + GenericAssayBinaryEnrichmentTableColumnType, +} from './GenericAssayBinaryEnrichmentsTable'; +import { GenericAssayBinaryEnrichmentsTableDataStore } from './GenericAssayBinaryEnrichmentsTableDataStore'; +import { GenericAssayBinaryEnrichmentRow } from 'shared/model/EnrichmentRow'; +import { EnrichmentAnalysisComparisonGroup } from 'pages/groupComparison/GroupComparisonUtils'; +import GenericAssayMiniScatterChart from './GenericAssayMiniScatterChart'; +import MiniFrequencyScatterChart from './MiniFrequencyScatterChart'; +import WindowStore from 'shared/components/window/WindowStore'; +import GenericAssayBarPlot from './GenericAssayBarPlot'; + +export interface IGenericAssayBinaryEnrichmentsContainerProps { + data: GenericAssayBinaryEnrichment[]; + selectedProfile: MolecularProfile; + groups: EnrichmentAnalysisComparisonGroup[]; + sampleKeyToSample: { + [uniqueSampleKey: string]: Sample; + }; + genericAssayType: string; + alteredVsUnalteredMode?: boolean; + patientLevelEnrichments: boolean; + onSetPatientLevelEnrichments: (patientLevel: boolean) => void; +} + +@observer +export default class GenericAssayBinaryEnrichmentsContainer extends React.Component< + IGenericAssayBinaryEnrichmentsContainerProps, + {} +> { + constructor(props: IGenericAssayBinaryEnrichmentsContainerProps) { + super(props); + makeObservable(this); + } + + static defaultProps: Partial< + IGenericAssayBinaryEnrichmentsContainerProps + > = { + alteredVsUnalteredMode: true, + }; + + @observable significanceFilter: boolean = false; + @observable.ref clickedEntityStableId: string; + @observable.ref selectedStableIds: string[] | null; + @observable.ref highlightedRow: GenericAssayBinaryEnrichmentRow | undefined; + @observable.ref _enrichedGroups: string[] = this.props.groups.map( + group => group.name + ); + + @computed get data(): GenericAssayBinaryEnrichmentRow[] { + return getGenericAssayBinaryEnrichmentRowData( + this.props.data, + this.props.groups + ); + } + + @computed get filteredData(): GenericAssayBinaryEnrichmentRow[] { + return getFilteredData( + this.data, + this._enrichedGroups, + this.significanceFilter, + this.filterByStableId, + true + ); + } + + @autobind + private filterByStableId(stableId: string) { + if (this.selectedStableIds) { + return this.selectedStableIds.includes(stableId); + } else { + // no need to filter the data since there is no selection + return true; + } + } + + @autobind + private toggleSignificanceFilter() { + this.significanceFilter = !this.significanceFilter; + } + + @autobind + private onEntityClick(stableId: string) { + this.clickedEntityStableId = stableId; + } + + @autobind + private onSelection(stableIds: string[]) { + this.selectedStableIds = stableIds; + } + + @autobind + private onSelectionCleared() { + this.selectedStableIds = null; + } + + private dataStore = new GenericAssayBinaryEnrichmentsTableDataStore( + () => { + return this.filteredData; + }, + () => { + return this.highlightedRow; + }, + (c: GenericAssayBinaryEnrichmentRow) => { + this.highlightedRow = c; + } + ); + + //used in 2 groups analysis + @computed get group1() { + return this.props.groups[0]; + } + + //used in 2 groups analysis + @computed get group2() { + return this.props.groups[1]; + } + + @computed get group1QueriedCasesCount() { + let caseIds: Set = new Set( + this.group1.samples.map(sample => + this.props.patientLevelEnrichments + ? sample.uniquePatientKey + : sample.uniqueSampleKey + ) + ); + return caseIds.size; + } + + @computed get group2QueriedCasesCount() { + let caseIds: Set = new Set( + this.group2.samples.map(sample => + this.props.patientLevelEnrichments + ? sample.uniquePatientKey + : sample.uniqueSampleKey + ) + ); + return caseIds.size; + } + + @computed get selectedEntitiesSet() { + return _.keyBy(this.selectedStableIds || []); + } + + @computed get isTwoGroupAnalysis(): boolean { + return this.props.groups.length == 2; + } + + @computed get customColumns() { + const cols = getGenericAssayBinaryEnrichmentColumns( + this.props.groups, + this.props.alteredVsUnalteredMode + ); + if (this.isTwoGroupAnalysis) { + cols.push({ + name: 'Alteration Overlap', + headerRender: () => Co-occurrence Pattern, + render: data => { + if (data.pValue === undefined) { + return -; + } + const groups = _.map(data.groupsSet); + // we want to order groups according to order in prop.groups + const group1 = groups.find( + group => group.name === this.props.groups[0].name + )!; + const group2 = groups.find( + group => group.name === this.props.groups[1].name + )!; + + if (!group1 || !group2) { + throw 'No matching groups in Alteration Overlap Cell'; + } + + const totalQueriedCases = + this.group1QueriedCasesCount + + this.group2QueriedCasesCount; + const group1Width = + (this.group1QueriedCasesCount / totalQueriedCases) * + 100; + const group2Width = 100 - group1Width; + const group1Unprofiled = + ((this.group1QueriedCasesCount - group1.totalCount) / + totalQueriedCases) * + 100; + const group1Unaltered = + ((group1.totalCount - group1.count) / + totalQueriedCases) * + 100; + const group2Unprofiled = + ((this.group2QueriedCasesCount - group2.totalCount) / + totalQueriedCases) * + 100; + const group1Altered = + (group1.count / totalQueriedCases) * 100; + const group2Altered = + (group2.count / totalQueriedCases) * 100; + + const alterationLanguage = 'alterations'; + + const overlay = () => { + return ( +
+

+ {data.entityName} {alterationLanguage} in: +

+ + + + + + + + + + + +
+ {group1.name}: + + {group1.count} of{' '} + {group1.totalCount} of profiled{' '} + {this.props + .patientLevelEnrichments + ? 'patients' + : 'samples'}{' '} + ( + {numeral( + group1.alteredPercentage + ).format('0.0')} + %) +
+ {group2.name}: + + {group2.count} of{' '} + {group2.totalCount} of profiled{' '} + {this.props + .patientLevelEnrichments + ? 'patients' + : 'samples'}{' '} + ( + {numeral( + group2.alteredPercentage + ).format('0.0')} + %) +
+
+ ); + }; + + return ( + +
+ +
+
+ ); + }, + tooltip: ( + + + + + + + + + +
Upper row + :{' '} + {this.props.patientLevelEnrichments + ? 'Patients' + : 'Samples'}{' '} + colored according to group. +
Lower row + :{' '} + {this.props.patientLevelEnrichments + ? 'Patients' + : 'Samples'}{' '} + with an alteration in the listed gene are + highlighted. +
+ ), + }); + } + + return cols; + } + + @computed private get entityPlotMaxWidth() { + //820 include width of two scatter plots + return WindowStore.size.width - (this.isTwoGroupAnalysis ? 820 : 40); + } + + @computed private get gaBarplotYAxislabel() { + return this.props.selectedProfile.name + ' altered ratio'; + } + + @computed private get categoryToColor() { + return _.reduce( + this.props.groups, + (acc, next) => { + if (next.color) { + acc[next.name] = next.color; + } + return acc; + }, + {} as { [id: string]: string } + ); + } + @computed get visibleOrderedColumnNames() { + const columns = []; + columns.push(GenericAssayBinaryEnrichmentTableColumnType.ENTITY_ID); + + this.props.groups.forEach(group => { + columns.push(group.name); + }); + + if (this.isTwoGroupAnalysis) { + columns.push('Alteration Overlap'); + columns.push(GenericAssayBinaryEnrichmentTableColumnType.LOG_RATIO); + } + + columns.push( + GenericAssayBinaryEnrichmentTableColumnType.P_VALUE, + GenericAssayBinaryEnrichmentTableColumnType.Q_VALUE + ); + + if (this.isTwoGroupAnalysis) { + columns.push( + this.props.alteredVsUnalteredMode + ? GenericAssayBinaryEnrichmentTableColumnType.TENDENCY + : GenericAssayBinaryEnrichmentTableColumnType.ENRICHED + ); + } else { + columns.push( + GenericAssayBinaryEnrichmentTableColumnType.MOST_ENRICHED + ); + } + return columns; + } + + @action.bound + onChange(values: { value: string }[]) { + this._enrichedGroups = _.map(values, datum => datum.value); + } + + @computed get selectedValues() { + return this._enrichedGroups.map(id => ({ value: id })); + } + + @computed get options(): Option[] { + return _.map(this.props.groups, group => { + return { + label: group.nameOfEnrichmentDirection + ? group.nameOfEnrichmentDirection + : group.name, + value: group.name, + }; + }); + } + + @computed get selectedRow() { + if (this.clickedEntityStableId) { + return this.props.data.filter( + d => d.stableId === this.clickedEntityStableId + )[0]; + } + return undefined; + } + + public render() { + if (this.props.data.length === 0) { + return ( +
+ No data/result available +
+ ); + } + + const data: any[] = getGenericAssayBinaryScatterData(this.data); + const maxData: any = _.maxBy(data, d => { + return Math.ceil(Math.abs(d.x)); + }); + + return ( +
+
+ {this.isTwoGroupAnalysis && ( + + )} + {this.isTwoGroupAnalysis && ( + + )} +
+ group.name + )} + yAxisLabel={this.gaBarplotYAxislabel} + categoryToColor={this.categoryToColor} + dataStore={this.dataStore} + /> +
+
+ +
+
+
+
+ +
+
+ +
+
+ column.name + )} + genericAssayType={this.props.genericAssayType} + groupSize={this.props.groups.length} + /> +
+
+ ); + } +} diff --git a/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsTable.tsx b/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsTable.tsx new file mode 100644 index 00000000000..a29980eab2d --- /dev/null +++ b/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsTable.tsx @@ -0,0 +1,164 @@ +import * as React from 'react'; +import _ from 'lodash'; +import LazyMobXTable, { + Column, +} from '../../../shared/components/lazyMobXTable/LazyMobXTable'; +import { observer } from 'mobx-react'; +import { computed, makeObservable } from 'mobx'; +import { formatSignificanceValueWithStyle } from 'shared/lib/FormatUtils'; +import { toConditionalPrecision } from 'shared/lib/NumberUtils'; +import styles from './styles.module.scss'; +import autobind from 'autobind-decorator'; +import { GenericAssayBinaryEnrichmentsTableDataStore } from './GenericAssayBinaryEnrichmentsTableDataStore'; +import { GenericAssayBinaryEnrichmentRow } from 'shared/model/EnrichmentRow'; +import { GENERIC_ASSAY_CONFIG } from 'shared/lib/GenericAssayUtils/GenericAssayConfig'; +import { + deriveDisplayTextFromGenericAssayType, + formatGenericAssayCompactLabelByNameAndId, +} from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils'; +import { ContinousDataPvalueTooltip } from './EnrichmentsUtil'; + +export interface IGenericAssayBinaryEnrichmentTableProps { + genericAssayType: string; + visibleOrderedColumnNames?: string[]; + customColumns?: { [id: string]: GenericAssayBinaryEnrichmentTableColumn }; + data: GenericAssayBinaryEnrichmentRow[]; + initialSortColumn?: string; + dataStore: GenericAssayBinaryEnrichmentsTableDataStore; + onEntityClick?: (stableId: string) => void; + mutexTendency?: boolean; + groupSize?: number; +} + +export enum GenericAssayBinaryEnrichmentTableColumnType { + ENTITY_ID = 'Entity ID', + LOG_RATIO = 'Log Ratio', + P_VALUE = 'P_VALUE', + Q_VALUE = 'Q_VALUE', + TENDENCY = 'Tendency', + EXPRESSED = 'Higher in', + MEAN_SUFFIX = ' mean', + STANDARD_DEVIATION_SUFFIX = ' standard deviation', + ENRICHED = 'Enriched in', + MOST_ENRICHED = 'Most enriched in', +} + +export type GenericAssayBinaryEnrichmentTableColumn = Column< + GenericAssayBinaryEnrichmentRow +> & { order?: number }; + +@observer +export default class GenericAssayBinaryEnrichmentsTable extends React.Component< + IGenericAssayBinaryEnrichmentTableProps, + {} +> { + constructor(props: IGenericAssayBinaryEnrichmentTableProps) { + super(props); + makeObservable(this); + } + + public static defaultProps = { + columns: [ + GenericAssayBinaryEnrichmentTableColumnType.ENTITY_ID, + GenericAssayBinaryEnrichmentTableColumnType.P_VALUE, + GenericAssayBinaryEnrichmentTableColumnType.Q_VALUE, + ], + initialSortColumn: 'q-Value', + mutexTendency: true, + }; + + @autobind + private onRowClick(d: GenericAssayBinaryEnrichmentRow) { + this.props.onEntityClick!(d.stableId); + this.props.dataStore.setHighlighted(d); + } + + private get entityTitle() { + return ( + GENERIC_ASSAY_CONFIG.genericAssayConfigByType[ + this.props.genericAssayType + ]?.globalConfig?.entityTitle || + deriveDisplayTextFromGenericAssayType(this.props.genericAssayType) + ); + } + + @computed get columns(): { + [columnEnum: string]: GenericAssayBinaryEnrichmentTableColumn; + } { + const columns: { + [columnEnum: string]: GenericAssayBinaryEnrichmentTableColumn; + } = this.props.customColumns || {}; + + columns[GenericAssayBinaryEnrichmentTableColumnType.ENTITY_ID] = { + name: this.entityTitle, + render: (d: GenericAssayBinaryEnrichmentRow) => { + return ( + + + {formatGenericAssayCompactLabelByNameAndId( + d.stableId, + d.entityName + )} + + + ); + }, + tooltip: {this.entityTitle}, + filter: ( + d: GenericAssayBinaryEnrichmentRow, + filterString: string, + filterStringUpper: string + ) => d.entityName.toUpperCase().includes(filterStringUpper), + sortBy: (d: GenericAssayBinaryEnrichmentRow) => d.entityName, + download: (d: GenericAssayBinaryEnrichmentRow) => d.entityName, + }; + + columns[GenericAssayBinaryEnrichmentTableColumnType.P_VALUE] = { + name: 'p-Value', + render: (d: GenericAssayBinaryEnrichmentRow) => ( + + {toConditionalPrecision(d.pValue, 3, 0.01)} + + ), + tooltip: ( + + ), + sortBy: (d: GenericAssayBinaryEnrichmentRow) => d.pValue, + download: (d: GenericAssayBinaryEnrichmentRow) => + toConditionalPrecision(d.pValue, 3, 0.01), + }; + + columns[GenericAssayBinaryEnrichmentTableColumnType.Q_VALUE] = { + name: 'q-Value', + render: (d: GenericAssayBinaryEnrichmentRow) => ( + + {formatSignificanceValueWithStyle(d.qValue)} + + ), + tooltip: Derived from Benjamini-Hochberg procedure, + sortBy: (d: GenericAssayBinaryEnrichmentRow) => d.qValue, + download: (d: GenericAssayBinaryEnrichmentRow) => + toConditionalPrecision(d.qValue, 3, 0.01), + }; + return columns; + } + + public render() { + const orderedColumns = _.sortBy( + this.props.visibleOrderedColumnNames!.map( + column => this.columns[column] + ), + (c: GenericAssayBinaryEnrichmentTableColumn) => c.order + ); + return ( + + ); + } +} diff --git a/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsTableDataStore.tsx b/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsTableDataStore.tsx new file mode 100644 index 00000000000..b58a4232356 --- /dev/null +++ b/src/pages/resultsView/enrichments/GenericAssayBinaryEnrichmentsTableDataStore.tsx @@ -0,0 +1,18 @@ +import { SimpleGetterLazyMobXTableApplicationDataStore } from 'shared/lib/ILazyMobXTableApplicationDataStore'; +import { GenericAssayBinaryEnrichmentRow } from 'shared/model/EnrichmentRow'; + +export class GenericAssayBinaryEnrichmentsTableDataStore extends SimpleGetterLazyMobXTableApplicationDataStore< + GenericAssayBinaryEnrichmentRow +> { + constructor( + getData: () => GenericAssayBinaryEnrichmentRow[], + getHighlighted: () => GenericAssayBinaryEnrichmentRow | undefined, + public setHighlighted: (c: GenericAssayBinaryEnrichmentRow) => void + ) { + super(getData); + this.dataHighlighter = (d: GenericAssayBinaryEnrichmentRow) => { + const highlighted = getHighlighted(); + return !!(highlighted && d.stableId === highlighted.stableId); + }; + } +} diff --git a/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsContainer.tsx b/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsContainer.tsx new file mode 100644 index 00000000000..1568dd0001a --- /dev/null +++ b/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsContainer.tsx @@ -0,0 +1,795 @@ +import * as React from 'react'; +import { observer, Observer } from 'mobx-react'; +import { observable, computed, action, makeObservable } from 'mobx'; +import { + MolecularProfile, + Sample, + GenericAssayCategoricalEnrichment, +} from 'cbioportal-ts-api-client'; +import client from 'shared/api/cbioportalClientInstance'; +import { + getGenericAssayCategoricalEnrichmentRowData, + getGenericAssayCategoricalEnrichmentColumns, + getFilteredCategoricalData, +} from 'pages/resultsView/enrichments/EnrichmentsUtil'; +import LoadingIndicator from 'shared/components/loadingIndicator/LoadingIndicator'; +import ReactSelect from 'react-select1'; +import _ from 'lodash'; +import autobind from 'autobind-decorator'; +import ScrollBar from 'shared/components/Scrollbar/ScrollBar'; +import { + Option, + DownloadControls, + remoteData, +} from 'cbioportal-frontend-commons'; +import GenericAssayCategoricalEnrichmentsTable, { + GenericAssayCategoricalEnrichmentTableColumnType, +} from './GenericAssayCategoricalEnrichmentsTable'; +import { GenericAssayCategoricalEnrichmentsTableDataStore } from './GenericAssayCategoricalEnrichmentsTableDataStore'; +import { GenericAssayCategoricalEnrichmentRow } from 'shared/model/EnrichmentRow'; +import { getRemoteDataGroupStatus } from 'cbioportal-utils'; +import { EnrichmentAnalysisComparisonGroup } from 'pages/groupComparison/GroupComparisonUtils'; +import { + IAxisData, + IAxisLogScaleParams, + IStringAxisData, +} from 'pages/resultsView/plots/PlotsTabUtils'; +import CategoryPlot, { + CategoryPlotType, +} from 'pages/groupComparison/CategoryPlot'; +import { OncoprintJS } from 'oncoprintjs'; +import ComparisonStore, { + OverlapStrategy, +} from 'shared/lib/comparison/ComparisonStore'; +import { RESERVED_CLINICAL_VALUE_COLORS } from 'shared/lib/Colors'; +import { + filterSampleList, + getComparisonCategoricalNaValue, +} from 'pages/groupComparison/ClinicalDataUtils'; + +export interface IGenericAssayCategoricalEnrichmentsContainerProps { + data: GenericAssayCategoricalEnrichment[]; + selectedProfile: MolecularProfile; + groups: EnrichmentAnalysisComparisonGroup[]; + sampleKeyToSample: { + [uniqueSampleKey: string]: Sample; + }; + genericAssayType: string; + alteredVsUnalteredMode?: boolean; + patientLevelEnrichments: boolean; + onSetPatientLevelEnrichments: (patientLevel: boolean) => void; + dataStore: ComparisonStore; +} +export enum CategoricalNumericalVisualisationType { + Plot = 'Plot', + Table = 'Table', +} +export const categoryPlotTypeOptions = [ + { value: CategoryPlotType.Bar, label: 'Bar chart' }, + { value: CategoryPlotType.StackedBar, label: 'Stacked bar chart' }, + { + value: CategoryPlotType.PercentageStackedBar, + label: '100% stacked bar chart', + }, + { value: CategoryPlotType.Heatmap, label: 'Heatmap' }, +]; + +const SVG_ID = 'categorical-plot-svg'; +function isNumerical(datatype?: string) { + return datatype && datatype.toLowerCase() === 'number'; +} + +export const numericalVisualisationTypeOptions = [ + { value: CategoricalNumericalVisualisationType.Plot, label: 'Plot' }, + { value: CategoricalNumericalVisualisationType.Table, label: 'Table' }, +]; + +@observer +export default class GenericAssayCategoricalEnrichmentsContainer extends React.Component< + IGenericAssayCategoricalEnrichmentsContainerProps, + {} +> { + constructor(props: IGenericAssayCategoricalEnrichmentsContainerProps) { + super(props); + makeObservable(this); + } + + static defaultProps: Partial< + IGenericAssayCategoricalEnrichmentsContainerProps + > = { + alteredVsUnalteredMode: true, + }; + // TODO: modify judgement + @computed get isNumericalPlot() { + return isNumerical(this.highlightedRow!.attributeType); + } + + @computed get showLogScaleControls() { + return this.isNumericalPlot; + } + + @computed get showPAndQControls() { + return !this.isTable && !this.isHeatmap; + } + + @computed get showHorizontalBarControls() { + return !this.showLogScaleControls && !this.isHeatmap; + } + + @computed get showSwapAxisControls() { + return !this.isTable; + } + + @computed get isTable() { + return ( + this.isNumericalPlot && + this.numericalVisualisationType === + CategoricalNumericalVisualisationType.Table + ); + } + + @computed get isHeatmap() { + return ( + !this.isNumericalPlot && + this.categoryPlotType === CategoryPlotType.Heatmap + ); + } + @computed get horzLabel() { + return this.swapAxes + ? `${this.highlightedRow!.attributeType}${ + this.logScale ? ' (log2)' : '' + }` + : `Group`; + } + + @computed get vertLabel() { + return this.swapAxes + ? 'Group' + : `${this.highlightedRow!.attributeType}${ + this.logScale ? ' (log2)' : '' + }`; + } + @observable significanceFilter: boolean = false; + @observable.ref clickedEntityStableId: string; + @observable.ref selectedStableIds: string[] | null; + @observable.ref highlightedRow: + | GenericAssayCategoricalEnrichmentRow + | undefined; + @observable.ref _enrichedGroups: string[] = this.props.groups.map( + group => group.name + ); + @observable private logScale = false; + @observable logScaleFunction: IAxisLogScaleParams | undefined; + @observable swapAxes = false; + @observable showPAndQ = false; + @observable horizontalBars = false; + @observable showNA = true; + + private scrollPane: HTMLDivElement; + + private oncoprintJs: OncoprintJS | null = null; + @autobind + private oncoprintJsRef(oncoprint: OncoprintJS) { + this.oncoprintJs = oncoprint; + } + + @observable categoryPlotType: CategoryPlotType = + CategoryPlotType.PercentageStackedBar; + + @observable + numericalVisualisationType: CategoricalNumericalVisualisationType = + CategoricalNumericalVisualisationType.Plot; + + @action.bound + private onPlotTypeSelect(option: any) { + this.categoryPlotType = option.value; + } + + @action.bound + private onNumericalVisualisationTypeSelect(option: any) { + this.numericalVisualisationType = option.value; + } + + @action.bound + private onClickLogScale() { + this.logScale = !this.logScale; + if (this.logScale) { + const MIN_LOG_ARGUMENT = 0.01; + this.logScaleFunction = { + label: 'log2', + fLogScale: (x: number, offset: number) => + Math.log2(Math.max(x, MIN_LOG_ARGUMENT)), + fInvLogScale: (x: number) => Math.pow(2, x), + }; + } else { + this.logScaleFunction = undefined; + } + } + + @action.bound + private onClickSwapAxes() { + this.swapAxes = !this.swapAxes; + } + + @action.bound + private onClickTogglePAndQ() { + this.showPAndQ = !this.showPAndQ; + } + + @action.bound + private onClickHorizontalBars() { + this.horizontalBars = !this.horizontalBars; + } + + @action.bound + private onClickShowNA() { + this.showNA = !this.showNA; + } + + @autobind + private getSvg() { + if (this.categoryPlotType === CategoryPlotType.Heatmap) { + return this.oncoprintJs && this.oncoprintJs.toSVG(true); + } + return document.getElementById(SVG_ID) as SVGElement | null; + } + + @autobind + private toolbar() { + if (this.isTable) { + return <>; + } + return ( +
+ +
+ ); + } + + @autobind + private assignScrollPaneRef(el: HTMLDivElement) { + this.scrollPane = el; + } + + @computed get data(): GenericAssayCategoricalEnrichmentRow[] { + return getGenericAssayCategoricalEnrichmentRowData( + this.props.data, + this.props.groups + ); + } + + @computed get filteredData(): GenericAssayCategoricalEnrichmentRow[] { + return getFilteredCategoricalData(this.data, this.filterByStableId); + } + + public readonly groupMembershipAxisData = remoteData({ + await: () => [], + invoke: async () => { + const categoryOrder = _.map(this.props.groups, group => group.name); + const axisData = { + data: [], + datatype: 'string', + categoryOrder, + } as IStringAxisData; + + const sampleKeyToGroupSampleData = _.reduce( + this.props.groups, + (acc, group) => { + group.samples.forEach(sample => { + const uniqueSampleKey = sample.uniqueSampleKey; + if (acc[uniqueSampleKey] === undefined) { + acc[uniqueSampleKey] = { + uniqueSampleKey, + value: [], + }; + } + acc[uniqueSampleKey].value.push(group.name); + }); + return acc; + }, + {} as { + [uniqueSampleKey: string]: { + uniqueSampleKey: string; + value: string[]; + }; + } + ); + + axisData.data = _.values(sampleKeyToGroupSampleData); + return Promise.resolve(axisData); + }, + }); + + readonly gaCategoricalAxisData = remoteData({ + invoke: async () => { + const axisData: IAxisData = { data: [], datatype: 'string' }; + let normalizedCategory: { [id: string]: string } = {}; + if (this.highlightedRow !== undefined) { + const molecularData = await client.fetchGenericAssayDataInMolecularProfileUsingPOST( + { + molecularProfileId: this.props.selectedProfile + .molecularProfileId, + genericAssayDataFilter: { + genericAssayStableIds: [ + (this + .highlightedRow as GenericAssayCategoricalEnrichmentRow) + .stableId, + ], + sampleIds: _.map( + this.props.sampleKeyToSample, + sample => sample.sampleId + ), + } as any, + } + ); + for (const d of molecularData) { + const lowerCaseValue = d.value.toLowerCase(); + if (normalizedCategory[lowerCaseValue] === undefined) { + //consider first value as category value + normalizedCategory[lowerCaseValue] = d.value; + } + } + + const axisData_Data = axisData.data; + + for (const d of molecularData) { + const value = d.value; + axisData_Data.push({ + uniqueSampleKey: d.uniqueSampleKey, + value: normalizedCategory[d.value.toLowerCase()], + }); + } + } + return Promise.resolve(axisData); + }, + }); + + private readonly gaCategoricalAxisDataFiltered = remoteData({ + await: () => [this.gaCategoricalAxisData, this.props.dataStore.samples], + invoke: () => { + const axisData = this.gaCategoricalAxisData.result!; + const sampleList: Sample[] = + this.props.dataStore.overlapStrategy === OverlapStrategy.EXCLUDE + ? filterSampleList( + this.props.dataStore.samples.result!, + this.props.dataStore._activeGroupsNotOverlapRemoved + .result! + ) + : this.props.dataStore.samples.result!; + if ( + this.showNA && + axisData.datatype === 'string' && + sampleList.length > 0 + ) { + const naSamples = _.difference( + _.uniq(sampleList.map(x => x.uniqueSampleKey)), + _.uniq(axisData.data.map(x => x.uniqueSampleKey)) + ); + return Promise.resolve({ + ...axisData, + data: axisData.data.concat( + naSamples.map(x => ({ + uniqueSampleKey: x, + value: 'NA', + })) + ), + }); + } else { + // filter out NA-like values (e.g. unknown) + return Promise.resolve({ + ...axisData, + data: axisData.data.filter( + x => + typeof x.value !== 'string' || + _.every( + getComparisonCategoricalNaValue(), + naValue => + naValue.toLowerCase() !== + (x.value as string).toLowerCase() + ) + ), + }); + } + }, + }); + @autobind + private getScrollPane() { + return this.scrollPane; + } + + @computed private get getUtilitiesMenu() { + if (!this.highlightedRow) { + return ; + } + return ( +
+ {this.isNumericalPlot && ( + <> +
+ +
+ +
+
+ + )} + {!this.showLogScaleControls && ( +
+ +
+ +
+
+ )} +
+ {this.showSwapAxisControls && ( + + )} + {this.showHorizontalBarControls && ( + + )} + {this.showLogScaleControls && ( + + )} + {this.gaCategoricalAxisData && ( + + )} + {this.showPAndQControls && ( + + )} +
+
+ ); + } + + @computed get vertAxisDataPromise() { + return this.swapAxes + ? this.groupMembershipAxisData + : this.gaCategoricalAxisDataFiltered; + } + + @computed get horzAxisDataPromise() { + return this.swapAxes + ? this.gaCategoricalAxisDataFiltered + : this.groupMembershipAxisData; + } + + @computed get categoryToColor() { + //add group colors and reserved category colors + return _.reduce( + this.props.dataStore.uidToGroup.result!, + (acc, next) => { + acc[next.nameWithOrdinal] = next.color; + return acc; + }, + RESERVED_CLINICAL_VALUE_COLORS + ); + } + + @computed get groupToColor() { + let groups = this.props.groups; + if (!groups) { + return {}; + } + return groups.reduce((result, ag) => { + result[ag.name as string] = ag.color; + return result; + }, {} as any); + } + + @computed get plot() { + if (!this.highlightedRow) { + this.highlightedRow = this.filteredData[0]; + } + if (this.filteredData.length === 0 || !this.highlightedRow) { + return ; + } + const promises = [this.horzAxisDataPromise, this.vertAxisDataPromise]; + const groupStatus = getRemoteDataGroupStatus(...promises); + const isPercentage = + this.categoryPlotType === CategoryPlotType.PercentageStackedBar; + const isStacked = + isPercentage || + this.categoryPlotType === CategoryPlotType.StackedBar; + switch (groupStatus) { + case 'pending': + return ( + + ); + case 'error': + return Error loading plot data.; + default: { + if (!this.horzAxisDataPromise || !this.vertAxisDataPromise) { + return Error loading plot data.; + } + + let plotElt: any = null; + plotElt = ( + + ); + + return ( +
+ + {this.toolbar} +
+ {plotElt} +
+
+ ); + } + } + } + + @autobind + private filterByStableId(stableId: string) { + if (this.selectedStableIds) { + return this.selectedStableIds.includes(stableId); + } else { + // no need to filter the data since there is no selection + return true; + } + } + + @autobind + private onEntityClick(stableId: string) { + this.clickedEntityStableId = stableId; + } + + @autobind + private onSelection(stableIds: string[]) { + this.selectedStableIds = stableIds; + } + + @autobind + private onSelectionCleared() { + this.selectedStableIds = null; + } + + private tableDataStore = new GenericAssayCategoricalEnrichmentsTableDataStore( + () => { + return this.filteredData; + }, + () => { + return this.highlightedRow; + }, + (c: GenericAssayCategoricalEnrichmentRow) => { + this.highlightedRow = c; + } + ); + + //used in 2 groups analysis + @computed get group1() { + return this.props.groups[0]; + } + + //used in 2 groups analysis + @computed get group2() { + return this.props.groups[1]; + } + + @computed get selectedEntitiesSet() { + return _.keyBy(this.selectedStableIds || []); + } + + @computed get isTwoGroupAnalysis(): boolean { + return this.props.groups.length == 2; + } + + @computed get customColumns() { + return getGenericAssayCategoricalEnrichmentColumns( + this.props.groups, + this.props.alteredVsUnalteredMode + ); + } + @computed get visibleOrderedColumnNames() { + const columns = []; + columns.push( + GenericAssayCategoricalEnrichmentTableColumnType.ENTITY_ID + ); + + columns.push( + GenericAssayCategoricalEnrichmentTableColumnType.ATTRIBUTE_TYPE, + GenericAssayCategoricalEnrichmentTableColumnType.STATISTICAL_TEST_NAME, + GenericAssayCategoricalEnrichmentTableColumnType.P_VALUE, + GenericAssayCategoricalEnrichmentTableColumnType.Q_VALUE + ); + + return columns; + } + + @action.bound + onChange(values: { value: string }[]) { + this._enrichedGroups = _.map(values, datum => datum.value); + } + + @computed get selectedValues() { + return this._enrichedGroups.map(id => ({ value: id })); + } + + @computed get options(): Option[] { + return _.map(this.props.groups, group => { + return { + label: group.nameOfEnrichmentDirection + ? group.nameOfEnrichmentDirection + : group.name, + value: group.name, + }; + }); + } + + @computed get selectedRow() { + if (this.clickedEntityStableId) { + return this.props.data.filter( + d => d.stableId === this.clickedEntityStableId + )[0]; + } + return undefined; + } + + public render() { + if (this.props.data.length === 0) { + return ( +
+ No data/result available +
+ ); + } + + return ( +
+
+ column.name + )} + genericAssayType={this.props.genericAssayType} + groupSize={this.props.groups.length} + /> +
+
+ {this.getUtilitiesMenu} + {this.plot} +
+
+ ); + } +} diff --git a/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsTable.tsx b/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsTable.tsx new file mode 100644 index 00000000000..c1356ac8118 --- /dev/null +++ b/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsTable.tsx @@ -0,0 +1,198 @@ +import * as React from 'react'; +import _ from 'lodash'; +import LazyMobXTable, { + Column, +} from '../../../shared/components/lazyMobXTable/LazyMobXTable'; +import { observer } from 'mobx-react'; +import { computed, makeObservable } from 'mobx'; +import { formatSignificanceValueWithStyle } from 'shared/lib/FormatUtils'; +import { toConditionalPrecision } from 'shared/lib/NumberUtils'; +import styles from './styles.module.scss'; +import autobind from 'autobind-decorator'; +import { GenericAssayCategoricalEnrichmentsTableDataStore } from './GenericAssayCategoricalEnrichmentsTableDataStore'; +import { GenericAssayCategoricalEnrichmentRow } from 'shared/model/EnrichmentRow'; +import { GENERIC_ASSAY_CONFIG } from 'shared/lib/GenericAssayUtils/GenericAssayConfig'; +import { + deriveDisplayTextFromGenericAssayType, + formatGenericAssayCompactLabelByNameAndId, +} from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils'; +import { ContinousDataPvalueTooltip } from './EnrichmentsUtil'; + +export interface IGenericAssayCategoricalEnrichmentTableProps { + genericAssayType: string; + visibleOrderedColumnNames?: string[]; + customColumns?: { + [id: string]: GenericAssayCategoricalEnrichmentTableColumn; + }; + data: GenericAssayCategoricalEnrichmentRow[]; + initialSortColumn?: string; + dataStore: GenericAssayCategoricalEnrichmentsTableDataStore; + onEntityClick?: (stableId: string) => void; + mutexTendency?: boolean; + groupSize?: number; +} + +export enum GenericAssayCategoricalEnrichmentTableColumnType { + ENTITY_ID = 'Entity ID', + P_VALUE = 'P_VALUE', + Q_VALUE = 'Q_VALUE', + ATTRIBUTE_TYPE = 'Attribute Type', + STATISTICAL_TEST_NAME = 'Statistical Test', +} + +export type GenericAssayCategoricalEnrichmentTableColumn = Column< + GenericAssayCategoricalEnrichmentRow +> & { order?: number }; + +@observer +export default class GenericAssayCategoricalEnrichmentsTable extends React.Component< + IGenericAssayCategoricalEnrichmentTableProps, + {} +> { + constructor(props: IGenericAssayCategoricalEnrichmentTableProps) { + super(props); + makeObservable(this); + } + + public static defaultProps = { + columns: [ + GenericAssayCategoricalEnrichmentTableColumnType.ENTITY_ID, + GenericAssayCategoricalEnrichmentTableColumnType.ATTRIBUTE_TYPE, + GenericAssayCategoricalEnrichmentTableColumnType.STATISTICAL_TEST_NAME, + GenericAssayCategoricalEnrichmentTableColumnType.P_VALUE, + GenericAssayCategoricalEnrichmentTableColumnType.Q_VALUE, + ], + initialSortColumn: 'q-Value', + mutexTendency: true, + }; + + @autobind + private onRowClick(d: GenericAssayCategoricalEnrichmentRow) { + this.props.onEntityClick!(d.stableId); + this.props.dataStore.setHighlighted(d); + } + + private get entityTitle() { + return ( + GENERIC_ASSAY_CONFIG.genericAssayConfigByType[ + this.props.genericAssayType + ]?.globalConfig?.entityTitle || + deriveDisplayTextFromGenericAssayType(this.props.genericAssayType) + ); + } + + @computed get columns(): { + [columnEnum: string]: GenericAssayCategoricalEnrichmentTableColumn; + } { + const columns: { + [columnEnum: string]: GenericAssayCategoricalEnrichmentTableColumn; + } = {}; + + columns[GenericAssayCategoricalEnrichmentTableColumnType.ENTITY_ID] = { + name: this.entityTitle, + render: (d: GenericAssayCategoricalEnrichmentRow) => { + return ( + + + {formatGenericAssayCompactLabelByNameAndId( + d.stableId, + d.entityName + )} + + + ); + }, + tooltip: {this.entityTitle}, + filter: ( + d: GenericAssayCategoricalEnrichmentRow, + filterString: string, + filterStringUpper: string + ) => d.entityName.toUpperCase().includes(filterStringUpper), + sortBy: (d: GenericAssayCategoricalEnrichmentRow) => d.entityName, + download: (d: GenericAssayCategoricalEnrichmentRow) => d.entityName, + }; + + columns[ + GenericAssayCategoricalEnrichmentTableColumnType.ATTRIBUTE_TYPE + ] = { + name: + GenericAssayCategoricalEnrichmentTableColumnType.ATTRIBUTE_TYPE, + render: (d: GenericAssayCategoricalEnrichmentRow) => ( + {d.attributeType} + ), + tooltip: Attribute Type, + sortBy: (d: GenericAssayCategoricalEnrichmentRow) => + String(d.attributeType), + download: (d: GenericAssayCategoricalEnrichmentRow) => + d.attributeType, + }; + + columns[ + GenericAssayCategoricalEnrichmentTableColumnType.STATISTICAL_TEST_NAME + ] = { + name: + GenericAssayCategoricalEnrichmentTableColumnType.STATISTICAL_TEST_NAME, + render: (d: GenericAssayCategoricalEnrichmentRow) => ( + {d.statisticalTest} + ), + tooltip: Statistic Test, + sortBy: (d: GenericAssayCategoricalEnrichmentRow) => + String(d.statisticalTest), + download: (d: GenericAssayCategoricalEnrichmentRow) => + d.statisticalTest, + }; + + columns[GenericAssayCategoricalEnrichmentTableColumnType.P_VALUE] = { + name: 'p-Value', + render: (d: GenericAssayCategoricalEnrichmentRow) => ( + + {toConditionalPrecision(d.pValue, 3, 0.01)} + + ), + tooltip: ( + + ), + sortBy: (d: GenericAssayCategoricalEnrichmentRow) => d.pValue, + download: (d: GenericAssayCategoricalEnrichmentRow) => + toConditionalPrecision(d.pValue, 3, 0.01), + }; + + columns[GenericAssayCategoricalEnrichmentTableColumnType.Q_VALUE] = { + name: 'q-Value', + render: (d: GenericAssayCategoricalEnrichmentRow) => ( + + {formatSignificanceValueWithStyle(d.qValue)} + + ), + tooltip: Derived from Benjamini-Hochberg procedure, + sortBy: (d: GenericAssayCategoricalEnrichmentRow) => d.qValue, + download: (d: GenericAssayCategoricalEnrichmentRow) => + toConditionalPrecision(d.qValue, 3, 0.01), + }; + return columns; + } + + public render() { + const orderedColumns = _.sortBy( + this.props.visibleOrderedColumnNames!.map( + column => this.columns[column] + ), + (c: GenericAssayCategoricalEnrichmentTableColumn) => c.order + ); + + return ( + + ); + } +} diff --git a/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsTableDataStore.tsx b/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsTableDataStore.tsx new file mode 100644 index 00000000000..3c670f8d1d6 --- /dev/null +++ b/src/pages/resultsView/enrichments/GenericAssayCategoricalEnrichmentsTableDataStore.tsx @@ -0,0 +1,18 @@ +import { SimpleGetterLazyMobXTableApplicationDataStore } from 'shared/lib/ILazyMobXTableApplicationDataStore'; +import { GenericAssayCategoricalEnrichmentRow } from 'shared/model/EnrichmentRow'; + +export class GenericAssayCategoricalEnrichmentsTableDataStore extends SimpleGetterLazyMobXTableApplicationDataStore< + GenericAssayCategoricalEnrichmentRow +> { + constructor( + getData: () => GenericAssayCategoricalEnrichmentRow[], + getHighlighted: () => GenericAssayCategoricalEnrichmentRow | undefined, + public setHighlighted: (c: GenericAssayCategoricalEnrichmentRow) => void + ) { + super(getData); + this.dataHighlighter = (d: GenericAssayCategoricalEnrichmentRow) => { + const highlighted = getHighlighted(); + return !!(highlighted && d.stableId === highlighted.stableId); + }; + } +} diff --git a/src/pages/resultsView/enrichments/MiniFrequencyScatterChart.tsx b/src/pages/resultsView/enrichments/MiniFrequencyScatterChart.tsx index d4215c60941..0c1c098c5c1 100644 --- a/src/pages/resultsView/enrichments/MiniFrequencyScatterChart.tsx +++ b/src/pages/resultsView/enrichments/MiniFrequencyScatterChart.tsx @@ -40,6 +40,7 @@ export interface IMiniFrequencyScatterChartProps { onSelection: (hugoGeneSymbols: string[]) => void; onSelectionCleared: () => void; selectedGenesSet: { [hugoGeneSymbol: string]: any }; + yAxisLablePrefix?: string; } const MAX_DOT_SIZE = 10; @@ -104,7 +105,8 @@ export default class MiniFrequencyScatterChart extends React.Component< } @computed get yLabel() { - return `Altered Frequency in ${truncateWithEllipsis( + const prefix = this.props.yAxisLablePrefix || 'Altered Frequency'; + return `${prefix} in ${truncateWithEllipsis( this.props.yGroupName, this.maxLabelWidth, 'Arial', diff --git a/src/shared/featureFlags.ts b/src/shared/featureFlags.ts index 1e87701342b..06c8db19ccf 100644 --- a/src/shared/featureFlags.ts +++ b/src/shared/featureFlags.ts @@ -1,4 +1,5 @@ export enum FeatureFlagEnum { STUDY_VIEW_STRUCT_VAR_TABLE = 'STUDY_VIEW_STRUCT_VAR_TABLE', LEFT_TRUNCATION_ADJUSTMENT = 'LEFT_TRUNCATION_ADJUSTMENT', + GENERIC_ASSAY_GROUP_COMPARISON = 'GENERIC_ASSAY_GROUP_COMPARISON', } diff --git a/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts b/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts index 9ad20456eeb..97acfddba2e 100644 --- a/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts +++ b/src/shared/lib/GenericAssayUtils/GenericAssayCommonUtils.ts @@ -397,3 +397,20 @@ export function getSortedGenericAssayTabSpecs( return _.sortBy(genericAssayTabSpecs, specs => specs.linkText); } + +export function getSortedGenericAssayAllTabSpecs( + genericAssayAllEnrichmentProfilesGroupedByGenericAssayType: { + [key: string]: MolecularProfile[]; + } = {} +): { genericAssayType: string; linkText: string }[] { + const genericAssayAllTabSpecs: { + genericAssayType: string; + linkText: string; + }[] = _.keys( + genericAssayAllEnrichmentProfilesGroupedByGenericAssayType + ).map(genericAssayType => ({ + genericAssayType, + linkText: deriveDisplayTextFromGenericAssayType(genericAssayType), + })); + return _.sortBy(genericAssayAllTabSpecs, specs => specs.linkText); +} diff --git a/src/shared/lib/comparison/ComparisonStore.ts b/src/shared/lib/comparison/ComparisonStore.ts index aa2649c6909..6f4784a120e 100644 --- a/src/shared/lib/comparison/ComparisonStore.ts +++ b/src/shared/lib/comparison/ComparisonStore.ts @@ -44,7 +44,10 @@ import _ from 'lodash'; import { compareByAlterationPercentage, getAlterationRowData, + pickAllGenericAssayEnrichmentProfiles, pickCopyNumberEnrichmentProfiles, + pickGenericAssayBinaryEnrichmentProfiles, + pickGenericAssayCategoricalEnrichmentProfiles, pickGenericAssayEnrichmentProfiles, pickMethylationEnrichmentProfiles, pickMRNAEnrichmentProfiles, @@ -55,6 +58,8 @@ import { import { makeEnrichmentDataPromise, makeGenericAssayEnrichmentDataPromise, + makeGenericAssayBinaryEnrichmentDataPromise, + makeGenericAssayCategoricalEnrichmentDataPromise, } from '../../../pages/resultsView/ResultsViewPageStoreUtils'; import internalClient from '../../api/cbioportalInternalClientInstance'; import autobind from 'autobind-decorator'; @@ -107,6 +112,7 @@ import { AlterationEnrichmentRow } from 'shared/model/AlterationEnrichmentRow'; import AnalysisStore from './AnalysisStore'; import { AnnotatedMutation } from 'shared/model/AnnotatedMutation'; import { compileMutations } from './AnalysisStoreUtils'; +import { FeatureFlagEnum } from 'shared/featureFlags'; export enum OverlapStrategy { INCLUDE = 'Include', @@ -175,6 +181,12 @@ export default abstract class ComparisonStore extends AnalysisStore GroupComparisonTab.GENERIC_ASSAY_PREFIX ) || this.showGenericAssayTab ); + this.tabHasBeenShown.set( + GroupComparisonTab.GENERIC_ASSAY_BINARY_PREFIX, + !!this.tabHasBeenShown.get( + GroupComparisonTab.GENERIC_ASSAY_BINARY_PREFIX + ) || this.showGenericAssayBinaryTab + ); this.tabHasBeenShown.set( GroupComparisonTab.ALTERATIONS, !!this.tabHasBeenShown.get( @@ -596,6 +608,28 @@ export default abstract class ComparisonStore extends AnalysisStore ), }); + public readonly genericAssayAllEnrichmentProfilesGroupedByGenericAssayType = remoteData( + { + await: () => [this.molecularProfilesInActiveStudies], + invoke: () => { + const availableProfiles = this.appStore.featureFlagStore.has( + FeatureFlagEnum.GENERIC_ASSAY_GROUP_COMPARISON + ) + ? this.molecularProfilesInActiveStudies.result! + : pickGenericAssayEnrichmentProfiles( + this.molecularProfilesInActiveStudies.result! + ); + return Promise.resolve( + _.groupBy( + pickAllGenericAssayEnrichmentProfiles( + availableProfiles + ), + profile => profile.genericAssayType + ) + ); + }, + } + ); public readonly genericAssayEnrichmentProfilesGroupedByGenericAssayType = remoteData( { await: () => [this.molecularProfilesInActiveStudies], @@ -611,6 +645,36 @@ export default abstract class ComparisonStore extends AnalysisStore } ); + public readonly genericAssayBinaryEnrichmentProfilesGroupedByGenericAssayType = remoteData( + { + await: () => [this.molecularProfilesInActiveStudies], + invoke: () => + Promise.resolve( + _.groupBy( + pickGenericAssayBinaryEnrichmentProfiles( + this.molecularProfilesInActiveStudies.result! + ), + profile => profile.genericAssayType + ) + ), + } + ); + + public readonly genericAssayCategoricalEnrichmentProfilesGroupedByGenericAssayType = remoteData( + { + await: () => [this.molecularProfilesInActiveStudies], + invoke: () => + Promise.resolve( + _.groupBy( + pickGenericAssayCategoricalEnrichmentProfiles( + this.molecularProfilesInActiveStudies.result! + ), + profile => profile.genericAssayType + ) + ), + } + ); + @observable.ref private _mutationEnrichmentProfileMap: { [studyId: string]: MolecularProfile; } = {}; @@ -630,12 +694,29 @@ export default abstract class ComparisonStore extends AnalysisStore [studyId: string]: MolecularProfile; } = {}; @observable.ref + private _selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType: { + [geneircAssayType: string]: { + [studyId: string]: MolecularProfile; + }; + } = {}; + @observable.ref private _selectedGenericAssayEnrichmentProfileMapGroupedByGenericAssayType: { [geneircAssayType: string]: { [studyId: string]: MolecularProfile; }; } = {}; - + @observable.ref + private _selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType: { + [geneircAssayType: string]: { + [studyId: string]: MolecularProfile; + }; + } = {}; + @observable.ref + private _selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType: { + [geneircAssayType: string]: { + [studyId: string]: MolecularProfile; + }; + } = {}; readonly selectedStudyMutationEnrichmentProfileMap = remoteData({ await: () => [this.mutationEnrichmentProfiles], invoke: () => { @@ -797,7 +878,45 @@ export default abstract class ComparisonStore extends AnalysisStore } }, }); - + readonly selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType = remoteData( + { + await: () => [ + this.genericAssayAllEnrichmentProfilesGroupedByGenericAssayType, + ], + invoke: () => { + if ( + _.isEmpty( + this + ._selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType + ) + ) { + return Promise.resolve( + _.mapValues( + this + .genericAssayAllEnrichmentProfilesGroupedByGenericAssayType + .result!, + genericAssayEnrichmentProfiles => { + const molecularProfilesbyStudyId = _.groupBy( + genericAssayEnrichmentProfiles, + profile => profile.studyId + ); + // Select only one molecular profile for each study + return _.mapValues( + molecularProfilesbyStudyId, + molecularProfiles => molecularProfiles[0] + ); + } + ) + ); + } else { + return Promise.resolve( + this + ._selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType + ); + } + }, + } + ); readonly selectedGenericAssayEnrichmentProfileMapGroupedByGenericAssayType = remoteData( { await: () => [ @@ -838,6 +957,87 @@ export default abstract class ComparisonStore extends AnalysisStore } ); + readonly selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType = remoteData( + { + await: () => [ + this + .genericAssayBinaryEnrichmentProfilesGroupedByGenericAssayType, + ], + invoke: () => { + if ( + _.isEmpty( + this + ._selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType + ) + ) { + return Promise.resolve( + _.mapValues( + this + .genericAssayBinaryEnrichmentProfilesGroupedByGenericAssayType + .result!, + genericAssayEnrichmentProfiles => { + const molecularProfilesbyStudyId = _.groupBy( + genericAssayEnrichmentProfiles, + profile => profile.studyId + ); + // Select only one molecular profile for each study + return _.mapValues( + molecularProfilesbyStudyId, + molecularProfiles => molecularProfiles[0] + ); + } + ) + ); + } else { + return Promise.resolve( + this + ._selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType + ); + } + }, + } + ); + + readonly selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType = remoteData( + { + await: () => [ + this + .genericAssayCategoricalEnrichmentProfilesGroupedByGenericAssayType, + ], + invoke: () => { + if ( + _.isEmpty( + this + ._selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType + ) + ) { + return Promise.resolve( + _.mapValues( + this + .genericAssayCategoricalEnrichmentProfilesGroupedByGenericAssayType + .result!, + genericAssayEnrichmentProfiles => { + const molecularProfilesbyStudyId = _.groupBy( + genericAssayEnrichmentProfiles, + profile => profile.studyId + ); + // Select only one molecular profile for each study + return _.mapValues( + molecularProfilesbyStudyId, + molecularProfiles => molecularProfiles[0] + ); + } + ) + ); + } else { + return Promise.resolve( + this + ._selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType + ); + } + }, + } + ); @action public setMutationEnrichmentProfileMap(profileMap: { [studyId: string]: MolecularProfile; @@ -880,6 +1080,23 @@ export default abstract class ComparisonStore extends AnalysisStore this._methylationEnrichmentProfileMap = profileMap; } + @action + public setAllGenericAssayEnrichmentProfileMap( + profileMap: { + [studyId: string]: MolecularProfile; + }, + genericAssayType: string + ) { + const clonedMap = _.clone( + this + .selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType + .result! + ); + clonedMap[genericAssayType] = profileMap; + // trigger the function to recompute + this._selectedAllGenericAssayEnrichmentProfileMapGroupedByGenericAssayType = clonedMap; + } + @action public setGenericAssayEnrichmentProfileMap( profileMap: { @@ -1532,6 +1749,106 @@ export default abstract class ComparisonStore extends AnalysisStore }, }); + readonly gaBinaryEnrichmentGroupsByAssayType = remoteData({ + await: () => [ + this + .selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType, + this.enrichmentAnalysisGroups, + ], + invoke: () => { + return Promise.resolve( + _.mapValues( + this + .selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType + .result!, + selectedGenericAssayBinaryEnrichmentProfileMap => { + let studyIds = Object.keys( + selectedGenericAssayBinaryEnrichmentProfileMap + ); + // assumes single study for now + if (studyIds.length === 1) { + return this.enrichmentAnalysisGroups.result!.reduce( + ( + acc: EnrichmentAnalysisComparisonGroup[], + group + ) => { + // filter samples having mutation profile + const filteredSamples = group.samples.filter( + sample => + selectedGenericAssayBinaryEnrichmentProfileMap[ + sample.studyId + ] !== undefined + ); + if (filteredSamples.length > 0) { + acc.push({ + ...group, + samples: filteredSamples, + description: `samples in ${group.name}`, + }); + } + return acc; + }, + [] + ); + } else { + return []; + } + } + ) + ); + }, + }); + + readonly gaCategoricalEnrichmentGroupsByAssayType = remoteData({ + await: () => [ + this + .selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType, + this.enrichmentAnalysisGroups, + ], + invoke: () => { + return Promise.resolve( + _.mapValues( + this + .selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType + .result!, + selectedGenericAssayCategoricalEnrichmentProfileMap => { + let studyIds = Object.keys( + selectedGenericAssayCategoricalEnrichmentProfileMap + ); + // assumes single study for now + if (studyIds.length === 1) { + return this.enrichmentAnalysisGroups.result!.reduce( + ( + acc: EnrichmentAnalysisComparisonGroup[], + group + ) => { + // filter samples having mutation profile + const filteredSamples = group.samples.filter( + sample => + selectedGenericAssayCategoricalEnrichmentProfileMap[ + sample.studyId + ] !== undefined + ); + if (filteredSamples.length > 0) { + acc.push({ + ...group, + samples: filteredSamples, + description: `samples in ${group.name}`, + }); + } + return acc; + }, + [] + ); + } else { + return []; + } + } + ) + ); + }, + }); + readonly gaEnrichmentDataQueryByAssayType = remoteData({ await: () => [ this.gaEnrichmentGroupsByAssayType, @@ -1570,6 +1887,81 @@ export default abstract class ComparisonStore extends AnalysisStore }, }); + readonly gaBinaryEnrichmentDataQueryByAssayType = remoteData({ + await: () => [ + this.gaBinaryEnrichmentGroupsByAssayType, + this + .selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType, + ], + invoke: () => { + return Promise.resolve( + _.mapValues( + this.gaBinaryEnrichmentGroupsByAssayType.result!, + ( + genericAssayEnrichmentAnalysisGroups, + genericAssayType + ) => { + return genericAssayEnrichmentAnalysisGroups.map( + group => { + const molecularProfileCaseIdentifiers = group.samples.map( + sample => ({ + caseId: sample.sampleId, + molecularProfileId: this + .selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType + .result![genericAssayType][ + sample.studyId + ].molecularProfileId, + }) + ); + return { + name: group.name, + molecularProfileCaseIdentifiers, + }; + } + ); + } + ) + ); + }, + }); + + readonly gaCategoricalEnrichmentDataQueryByAssayType = remoteData({ + await: () => [ + this.gaCategoricalEnrichmentGroupsByAssayType, + this + .selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType, + ], + invoke: () => { + return Promise.resolve( + _.mapValues( + this.gaCategoricalEnrichmentGroupsByAssayType.result!, + ( + genericAssayEnrichmentAnalysisGroups, + genericAssayType + ) => { + return genericAssayEnrichmentAnalysisGroups.map( + group => { + const molecularProfileCaseIdentifiers = group.samples.map( + sample => ({ + caseId: sample.sampleId, + molecularProfileId: this + .selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType + .result![genericAssayType][ + sample.studyId + ].molecularProfileId, + }) + ); + return { + name: group.name, + molecularProfileCaseIdentifiers, + }; + } + ); + } + ) + ); + }, + }); readonly gaEnrichmentDataByAssayType = remoteData({ await: () => [this.gaEnrichmentDataQueryByAssayType], invoke: () => { @@ -1609,6 +2001,86 @@ export default abstract class ComparisonStore extends AnalysisStore }, }); + readonly gaBinaryEnrichmentDataByAssayType = remoteData({ + await: () => [this.gaBinaryEnrichmentDataQueryByAssayType], + invoke: () => { + return Promise.resolve( + _.mapValues( + this.gaBinaryEnrichmentDataQueryByAssayType.result!, + ( + genericAssayEnrichmentDataRequestGroups, + genericAssayType + ) => { + return makeGenericAssayBinaryEnrichmentDataPromise({ + await: () => [], + getSelectedProfileMap: () => + this + .selectedGenericAssayBinaryEnrichmentProfileMapGroupedByGenericAssayType + .result![genericAssayType], // returns an empty array if the selected study doesn't have any generic assay profiles + fetchData: () => { + if ( + genericAssayEnrichmentDataRequestGroups && + genericAssayEnrichmentDataRequestGroups.length > + 1 + ) { + return internalClient.fetchGenericAssayBinaryDataEnrichmentInMultipleMolecularProfilesUsingPOST( + { + enrichmentType: 'SAMPLE', + groups: genericAssayEnrichmentDataRequestGroups, + } + ); + } else { + return Promise.resolve([]); + } + }, + }); + } + ) + ); + }, + }); + + readonly gaCategoricalEnrichmentDataByAssayType = remoteData({ + await: () => [this.gaCategoricalEnrichmentDataQueryByAssayType], + invoke: () => { + return Promise.resolve( + _.mapValues( + this.gaCategoricalEnrichmentDataQueryByAssayType.result!, + ( + genericAssayEnrichmentDataRequestGroups, + genericAssayType + ) => { + return makeGenericAssayCategoricalEnrichmentDataPromise( + { + await: () => [], + getSelectedProfileMap: () => + this + .selectedGenericAssayCategoricalEnrichmentProfileMapGroupedByGenericAssayType + .result![genericAssayType], // returns an empty array if the selected study doesn't have any generic assay profiles + fetchData: () => { + if ( + genericAssayEnrichmentDataRequestGroups && + genericAssayEnrichmentDataRequestGroups.length > + 1 + ) { + return internalClient.fetchGenericAssayCategoricalDataEnrichmentInMultipleMolecularProfilesUsingPOST( + { + enrichmentType: 'SAMPLE', + groups: genericAssayEnrichmentDataRequestGroups, + } + ); + } else { + return Promise.resolve([]); + } + }, + } + ); + } + ) + ); + }, + }); + @computed get survivalTabShowable() { return ( this.survivalClinicalDataExists.isComplete && @@ -1780,6 +2252,31 @@ export default abstract class ComparisonStore extends AnalysisStore ); } + @computed get genericAssayBinaryTabShowable() { + return ( + this.genericAssayBinaryEnrichmentProfilesGroupedByGenericAssayType + .isComplete && + _.size( + this + .genericAssayBinaryEnrichmentProfilesGroupedByGenericAssayType + .result! + ) > 0 + ); + } + + @computed get genericAssayCategoricalTabShowable() { + return ( + this + .genericAssayCategoricalEnrichmentProfilesGroupedByGenericAssayType + .isComplete && + _.size( + this + .genericAssayCategoricalEnrichmentProfilesGroupedByGenericAssayType + .result! + ) > 0 + ); + } + @computed get showGenericAssayTab() { return !!( this.genericAssayTabShowable || @@ -1791,6 +2288,28 @@ export default abstract class ComparisonStore extends AnalysisStore ); } + @computed get showGenericAssayBinaryTab() { + return !!( + this.genericAssayBinaryTabShowable || + (this.activeGroups.isComplete && + this.activeGroups.result!.length === 0 && + this.tabHasBeenShown.get( + GroupComparisonTab.GENERIC_ASSAY_BINARY_PREFIX + )) + ); + } + + @computed get showGenericAssayCategoricalTab() { + return !!( + this.genericAssayCategoricalTabShowable || + (this.activeGroups.isComplete && + this.activeGroups.result!.length === 0 && + this.tabHasBeenShown.get( + GroupComparisonTab.GENERIC_ASSAY_CATEGORICAL_PREFIX + )) + ); + } + @computed get genericAssayTabUnavailable() { return ( (this.activeGroups.isComplete && @@ -1801,6 +2320,26 @@ export default abstract class ComparisonStore extends AnalysisStore ); } + @computed get genericAssayBinaryTabUnavailable() { + return ( + (this.activeGroups.isComplete && + this.activeGroups.result.length < 2) || //less than two active groups + (this.activeStudyIds.isComplete && + this.activeStudyIds.result.length > 1) || //more than one active study + !this.genericAssayBinaryTabShowable + ); + } + + @computed get genericAssayCategoricalTabUnavailable() { + return ( + (this.activeGroups.isComplete && + this.activeGroups.result.length < 2) || //less than two active groups + (this.activeStudyIds.isComplete && + this.activeStudyIds.result.length > 1) || //more than one active study + !this.genericAssayCategoricalTabShowable + ); + } + public readonly sampleMap = remoteData({ await: () => [this.samples], invoke: () => { diff --git a/src/shared/model/EnrichmentRow.ts b/src/shared/model/EnrichmentRow.ts index 9c36fd18868..07bfe31666c 100644 --- a/src/shared/model/EnrichmentRow.ts +++ b/src/shared/model/EnrichmentRow.ts @@ -1,4 +1,7 @@ -import { GroupStatistics } from 'cbioportal-ts-api-client'; +import { + GenericAssayCountSummary, + GroupStatistics, +} from 'cbioportal-ts-api-client'; export interface BaseEnrichmentRow { checked: boolean; @@ -20,3 +23,30 @@ export interface GenericAssayEnrichmentRow extends BaseEnrichmentRow { stableId: string; entityName: string; } + +export interface GenericAssayBinaryEnrichmentRow { + checked: boolean; + disabled: boolean; + logRatio?: number; + pValue: number; + qValue: number; + enrichedGroup: string; + stableId: string; + entityName: string; + groupsSet: { + [id: string]: GenericAssayCountSummary & { alteredPercentage: number }; + }; +} + +export interface GenericAssayCategoricalEnrichmentRow { + checked: boolean; + disabled: boolean; + enrichedGroup: string; + pValue: number; + qValue: number; + stableId: string; + entityName: string; + attributeType: string; + statisticalTest: string; + groupsSet: { [id: string]: GroupStatistics }; +}