import { CoverageInformation } from '../../lib/GenePanelUtils';
import {
ClinicalAttribute,
MolecularProfile,
GenericAssayMeta,
} from 'cbioportal-ts-api-client';
import { SpecialAttribute } from '../../cache/ClinicalDataCache';
import _ from 'lodash';
import naturalSort from 'javascript-natural-sort';
import * as React from 'react';
import { ISelectOption } from './controls/OncoprintControls';
import {
COMMON_GENERIC_ASSAY_PROPERTY,
formatGenericAssayCommonLabel,
formatGenericAssayCompactLabelByNameAndId,
GenericAssayDataType,
getGenericAssayMetaPropertyOrDefault,
makeGenericAssayOption,
} from 'shared/lib/GenericAssayUtils/GenericAssayCommonUtils';
import { TrackGroupHeader, TrackGroupIndex } from 'oncoprintjs';
import ResultsViewOncoprint, {
AdditionalTrackGroupRecord,
} from 'shared/components/oncoprint/ResultsViewOncoprint';
import { AlterationTypeConstants } from 'shared/constants';
import { Group } from 'shared/api/session-service/sessionServiceModels';
import { GENERIC_ASSAY_CONFIG } from 'shared/lib/GenericAssayUtils/GenericAssayConfig';
import { AlterationTypeText } from 'shared/constants';
export function getAnnotatingProgressMessage(
usingOncokb: boolean,
usingHotspot: boolean
) {
let message;
if (usingOncokb && usingHotspot) {
message = 'Annotating with OncoKB™ and Cancer Hotspots';
} else if (usingOncokb) {
message = 'Annotating with OncoKB™';
} else if (usingHotspot) {
message = 'Annotating with Cancer Hotspots';
} else {
message = 'Processing data';
}
return {message};
}
export function convertComparisonGroupClinicalAttribute(
id: string,
groupIdToAttributeId: boolean
) {
if (groupIdToAttributeId) {
return `${SpecialAttribute.ComparisonGroupPrefix}_${id}`;
} else {
return id.substring(SpecialAttribute.ComparisonGroupPrefix.length + 1); // +1 for the underscore
}
}
export function makeComparisonGroupClinicalAttributes(
comparisonGroups: Group[]
): (ClinicalAttribute & { comparisonGroup: Group })[] {
return comparisonGroups.map(
group =>
({
clinicalAttributeId: convertComparisonGroupClinicalAttribute(
group.id,
true
),
datatype: 'STRING',
description: `Membership in ${group.data.name}`,
displayName: `In group: ${group.data.name}`,
comparisonGroup: group,
patientAttribute: false,
} as ClinicalAttribute & { comparisonGroup: Group })
);
}
export function makeProfiledInClinicalAttributes(
coverageInformation: CoverageInformation['samples'],
molecularProfileIdToMolecularProfile: {
[molecularProfileId: string]: MolecularProfile;
},
selectedMolecularProfiles: MolecularProfile[],
isSingleStudyQuery: boolean
): (ClinicalAttribute & { molecularProfileIds: string[] })[] {
// determine which Profiled In clinical attributes will exist in this query.
// A Profiled In attribute only exists if theres a sample in the query
// which is not profiled in any selected profiles for that type.
// If its a single study query (isSingleStudyQuery), and there is only one profile of a particular alteration type
// in the study, then an attribute will be generated specifically for that profile. Otherwise, the attribute
// will represent an entire alteration type.
const groupedSelectedMolecularProfiles: {
[alterationType: string]: MolecularProfile[];
} = _.groupBy(selectedMolecularProfiles, 'molecularAlterationType');
const selectedMolecularProfilesMap = _.keyBy(
selectedMolecularProfiles,
p => p.molecularProfileId
);
// Start each computation by assuming unprofiled for every queried alteration type.
// This is because of the following example: suppose we have a multiple study query, and
// theres a copy number profile for study A, but not for study B, and every sample in
// study A is profiled for copy number. Since the study B samples aren't notated with
// "not profiled for copy number", since that profile isnt in study B, then unless
// we keep track of the fact that there is a queried copy number profile for SOME
// study, then we'll never see that there are unprofiled samples for copy number,
// and thus we should show a "Profiled In Copy Number" track.
const initIsUnprofiled = _.mapValues(
groupedSelectedMolecularProfiles,
p => true
);
const existsUnprofiledCount: {
[alterationType: string]: number;
} = _.reduce(
coverageInformation,
(map, sampleCoverage) => {
const isUnprofiled: { [alterationType: string]: boolean } = _.clone(
initIsUnprofiled
);
// if a sample is not profiled in all genes, its certainly unprofiled for this profile
for (const gpData of sampleCoverage.notProfiledAllGenes) {
if (gpData.molecularProfileId in selectedMolecularProfilesMap) {
// mark isUnprofiled for this type because this is a selected profile
isUnprofiled[
molecularProfileIdToMolecularProfile[
gpData.molecularProfileId
].molecularAlterationType
] = true;
}
}
// if a sample is not profiled in some gene, then it is maybe unprofiled
_.forEach(sampleCoverage.notProfiledByGene, geneInfo => {
for (const gpData of geneInfo) {
if (
gpData.molecularProfileId in
selectedMolecularProfilesMap
) {
// mark isUnprofiled for this type because this is a selected profile
isUnprofiled[
molecularProfileIdToMolecularProfile[
gpData.molecularProfileId
].molecularAlterationType
] = true;
}
}
});
// if a sample is profiled for all genes, then it is not unprofiled
for (const gpData of sampleCoverage.allGenes) {
if (gpData.molecularProfileId in selectedMolecularProfilesMap) {
// unmark isUnprofiled
isUnprofiled[
molecularProfileIdToMolecularProfile[
gpData.molecularProfileId
].molecularAlterationType
] = false;
}
}
// if a sample is profiled in some gene, then it is not unprofiled
_.forEach(sampleCoverage.byGene, geneInfo => {
for (const gpData of geneInfo) {
if (
gpData.molecularProfileId in
selectedMolecularProfilesMap
) {
// unmark isUnprofiled
isUnprofiled[
molecularProfileIdToMolecularProfile[
gpData.molecularProfileId
].molecularAlterationType
] = false;
}
}
});
// increment counts
_.forEach(isUnprofiled, (_isUnprofiled, alterationType) => {
if (_isUnprofiled) {
map[alterationType] = map[alterationType] || 0;
map[alterationType] += 1;
}
});
return map;
},
{} as { [alterationType: string]: number }
);
// make a clinical attribute for each profile type which not every sample is profiled in
const existsUnprofiled = Object.keys(existsUnprofiledCount).filter(
alterationType => {
return existsUnprofiledCount[alterationType] > 0;
}
);
const attributes: (ClinicalAttribute & {
molecularProfileIds: string[];
})[] = existsUnprofiled
.map(alterationType => {
const group = groupedSelectedMolecularProfiles[alterationType];
if (!group) {
// No selected profiles of that type, skip it
return null;
} else if (group.length === 1 && isSingleStudyQuery) {
// If only one profile of type, and its a single study query, then it gets its own attribute
const profile = group[0];
return {
clinicalAttributeId: `${SpecialAttribute.ProfiledInPrefix}_${profile.molecularProfileId}`,
datatype: 'STRING',
description: `Profiled in ${profile.name}: ${profile.description}`,
displayName: `Profiled in ${profile.name}`,
molecularProfileIds: [profile.molecularProfileId],
patientAttribute: false,
} as ClinicalAttribute & { molecularProfileIds: string[] };
} else {
// If more than one, or its multiple study query, make one attribute for the entire alteration type
return {
clinicalAttributeId: `${SpecialAttribute.ProfiledInPrefix}_${alterationType}`,
datatype: 'STRING',
description: '',
displayName: `Profiled for ${
AlterationTypeText[
alterationType as keyof typeof AlterationTypeText
]
}`,
molecularProfileIds: group.map(p => p.molecularProfileId),
patientAttribute: false,
} as ClinicalAttribute & { molecularProfileIds: string[] };
}
})
.filter(x => !!x) as (ClinicalAttribute & {
molecularProfileIds: string[];
})[]; // filter out null
attributes.sort((a, b) => naturalSort(a.displayName, b.displayName));
return attributes;
}
export function genericAssayEntitiesToSelectOptionsGroupedByGenericAssayType(genericAssayEntitiesGroupedByGenericAssayType: {
[genericAssayType: string]: GenericAssayMeta[];
}): { [genericAssayType: string]: ISelectOption[] } {
return _.mapValues(
genericAssayEntitiesGroupedByGenericAssayType,
genericAssayEntities => {
return _.map(genericAssayEntities, makeGenericAssayOption);
}
);
}
export function getGenericAssayTrackCacheQueries(
groups: AdditionalTrackGroupRecord[],
molecularProfileIdToMolecularProfile: { [id: string]: MolecularProfile },
oncoprint: ResultsViewOncoprint
) {
return _.flatten(
groups.map(entry => {
const type =
molecularProfileIdToMolecularProfile[entry.molecularProfileId]
.genericAssayType;
const genericAssayEntitiesByEntityId = _.keyBy(
oncoprint.props.store
.genericAssayEntitiesGroupedByGenericAssayType.result![
type
],
t => t.stableId
);
return _.keys(entry.entities).map(entityId => {
const entity = genericAssayEntitiesByEntityId[entityId];
// Override name and description based on GenericAssayConfig
const entityName = GENERIC_ASSAY_CONFIG
.genericAssayConfigByType[type]?.oncoprintTrackConfig
?.formatNameUsingCompactLabel
? formatGenericAssayCompactLabelByNameAndId(
entityId,
getGenericAssayMetaPropertyOrDefault(
entity,
COMMON_GENERIC_ASSAY_PROPERTY.NAME,
entityId
)
)
: getGenericAssayMetaPropertyOrDefault(
entity,
COMMON_GENERIC_ASSAY_PROPERTY.NAME,
entityId
);
const description = GENERIC_ASSAY_CONFIG
.genericAssayConfigByType[type]?.oncoprintTrackConfig
?.formatDescriptionUsingCommonLabel
? formatGenericAssayCommonLabel(entity)
: getGenericAssayMetaPropertyOrDefault(
entity,
COMMON_GENERIC_ASSAY_PROPERTY.DESCRIPTION,
entityName
);
return {
molecularProfileId: entry.molecularProfileId,
stableId: entityId,
entityName,
description,
};
});
})
);
}
export function isGenericAssayCategoricalProfile(m: MolecularProfile) {
return (
m.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY &&
m.datatype !== GenericAssayDataType.LIMIT_VALUE
);
}
export function isGenericAssayHeatmapProfile(m: MolecularProfile) {
return (
m.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY &&
m.datatype === GenericAssayDataType.LIMIT_VALUE
);
}
export function makeTrackGroupHeaders(
molecularProfileIdToMolecularProfile: { [p: string]: MolecularProfile },
molecularProfileIdToAdditionalTracks: {
[p: string]: AdditionalTrackGroupRecord;
},
genesetHeatmapTrackGroupIndex: number | undefined,
getClusteredTrackGroupIndex: () => number | undefined,
onClickClusterCallback: (index: TrackGroupIndex) => void,
onClickDontClusterCallback: () => void,
onClickDeleteCallback: (index: TrackGroupIndex) => void
): { [trackGroupIndex: number]: TrackGroupHeader } {
var headers = _.reduce(
molecularProfileIdToAdditionalTracks,
(headerMap, nextEntry) => {
let type: 'categorical' | 'heatmap';
const profile =
molecularProfileIdToMolecularProfile[
nextEntry.molecularProfileId
];
if (isGenericAssayCategoricalProfile(profile)) {
type = 'categorical';
} else {
type = 'heatmap';
}
headerMap[nextEntry.trackGroupIndex] = makeTrackGroupHeader(
type,
molecularProfileIdToMolecularProfile[
nextEntry.molecularProfileId
].name,
nextEntry.trackGroupIndex,
onClickDeleteCallback,
getClusteredTrackGroupIndex,
onClickClusterCallback,
onClickDontClusterCallback
);
return headerMap;
},
{} as { [trackGroupIndex: number]: TrackGroupHeader }
);
if (genesetHeatmapTrackGroupIndex !== undefined) {
headers[genesetHeatmapTrackGroupIndex] = makeTrackGroupHeader(
'geneset',
'GSVA Scores',
genesetHeatmapTrackGroupIndex!,
onClickDeleteCallback,
getClusteredTrackGroupIndex,
onClickClusterCallback,
onClickDontClusterCallback
);
}
return headers;
}
function makeTrackGroupHeader(
type: 'heatmap' | 'geneset' | 'categorical',
text: string,
trackGroupIndex: number,
onClickDeleteCallback: (index: TrackGroupIndex) => void,
getClusteredTrackGroupIndex: () => number | undefined,
onClickClusterCallback?: (index: TrackGroupIndex) => void,
onClickDontClusterCallback?: () => void
): TrackGroupHeader {
const header = {
label: { text: text },
options: [],
} as TrackGroupHeader;
if (type !== 'categorical') {
header.options.push(
{
label: 'Cluster',
onClick: onClickClusterCallback,
weight: () => {
if (getClusteredTrackGroupIndex() === trackGroupIndex) {
return 'bold';
} else {
return 'normal';
}
},
},
{
label: "Don't cluster",
onClick: () => {
if (getClusteredTrackGroupIndex() === trackGroupIndex) {
onClickDontClusterCallback!();
}
},
weight: () => {
if (getClusteredTrackGroupIndex() === trackGroupIndex) {
return 'normal';
} else {
return 'bold';
}
},
}
);
}
if (type !== 'geneset') {
if (type !== 'categorical') {
header.options.push({
separator: true,
});
}
header.options.push({
label: 'Delete',
onClick: onClickDeleteCallback,
});
}
return header;
}