import * as React from 'react';
import _ from 'lodash';
import {
DEFAULT_PROTEIN_IMPACT_TYPE_COLORS,
getColorForProteinImpactType as getDefaultColorForProteinImpactType,
IProteinImpactTypeColors,
ProteinImpactTypeFilter,
NumericalFilter,
NumericalFilterValue,
CategoricalFilter,
CategoricalFilterValue,
} from 'react-mutation-mapper';
import {
Gene,
MolecularProfile,
Mutation,
SampleIdentifier,
} from 'cbioportal-ts-api-client';
import {
stringListToSet,
ProteinImpactType,
getProteinImpactType,
DriverVsVusType,
MUT_DRIVER,
MUT_VUS,
} from 'cbioportal-frontend-commons';
import { extractGenomicLocation } from 'cbioportal-utils';
import { GenomicLocation } from 'genome-nexus-ts-api-client';
import {
MUTATION_STATUS_GERMLINE,
MOLECULAR_PROFILE_UNCALLED_MUTATIONS_SUFFIX,
} from 'shared/constants';
import { toSampleUuid } from './UuidUtils';
import { normalizeMutations } from '../components/mutationMapper/MutationMapperUtils';
import { getSimplifiedMutationType } from './oql/AccessorsForOqlFilter';
export const SELECTOR_VALUE_WITH_VUS = [
ProteinImpactType.MISSENSE_PUTATIVE_DRIVER,
ProteinImpactType.MISSENSE_UNKNOWN_SIGNIFICANCE,
ProteinImpactType.TRUNCATING_PUTATIVE_DRIVER,
ProteinImpactType.TRUNCATING_UNKNOWN_SIGNIFICANCE,
ProteinImpactType.INFRAME_PUTATIVE_DRIVER,
ProteinImpactType.INFRAME_UNKNOWN_SIGNIFICANCE,
ProteinImpactType.SPLICE_PUTATIVE_DRIVER,
ProteinImpactType.SPLICE_UNKNOWN_SIGNIFICANCE,
ProteinImpactType.FUSION_PUTATIVE_DRIVER,
ProteinImpactType.FUSION_UNKNOWN_SIGNIFICANCE,
ProteinImpactType.OTHER,
];
export function getProteinImpactTypeOptionDisplayValueMap(proteinImpactTypeColorMap: {
[proteinImpactType: string]: string;
}): { [proteinImpactType: string]: JSX.Element } {
return {
[ProteinImpactType.MISSENSE_PUTATIVE_DRIVER]: (
Missense
),
[ProteinImpactType.MISSENSE_UNKNOWN_SIGNIFICANCE]: (
Missense
),
[ProteinImpactType.TRUNCATING_PUTATIVE_DRIVER]: (
Truncating
),
[ProteinImpactType.TRUNCATING_UNKNOWN_SIGNIFICANCE]: (
Truncating
),
[ProteinImpactType.INFRAME_PUTATIVE_DRIVER]: (
Inframe
),
[ProteinImpactType.INFRAME_UNKNOWN_SIGNIFICANCE]: (
Inframe
),
[ProteinImpactType.SPLICE_PUTATIVE_DRIVER]: (
Splice
),
[ProteinImpactType.SPLICE_UNKNOWN_SIGNIFICANCE]: (
Splice
),
[ProteinImpactType.FUSION_PUTATIVE_DRIVER]: (
SV/Fusion
),
[ProteinImpactType.FUSION_UNKNOWN_SIGNIFICANCE]: (
SV/Fusion
),
[ProteinImpactType.OTHER]: (
Other
),
[DriverVsVusType.DRIVER]: (
Driver
),
[DriverVsVusType.VUS]: (
VUS
),
};
}
export function getProteinImpactTypeColorMap(
colors: IProteinImpactTypeColors
): { [proteinImpactType: string]: string } {
return {
[ProteinImpactType.MISSENSE_PUTATIVE_DRIVER]: colors.missenseColor,
[ProteinImpactType.MISSENSE_UNKNOWN_SIGNIFICANCE]:
colors.missenseVusColor,
[ProteinImpactType.TRUNCATING_PUTATIVE_DRIVER]: colors.truncatingColor,
[ProteinImpactType.TRUNCATING_UNKNOWN_SIGNIFICANCE]:
colors.truncatingVusColor,
[ProteinImpactType.INFRAME_PUTATIVE_DRIVER]: colors.inframeColor,
[ProteinImpactType.INFRAME_UNKNOWN_SIGNIFICANCE]:
colors.inframeVusColor,
[ProteinImpactType.SPLICE_PUTATIVE_DRIVER]: colors.spliceColor,
[ProteinImpactType.SPLICE_UNKNOWN_SIGNIFICANCE]: colors.spliceVusColor,
[ProteinImpactType.FUSION_PUTATIVE_DRIVER]: colors.fusionColor,
[ProteinImpactType.FUSION_UNKNOWN_SIGNIFICANCE]: colors.fusionVusColor,
[ProteinImpactType.OTHER]: colors.otherColor,
[DriverVsVusType.DRIVER]: MUT_DRIVER,
[DriverVsVusType.VUS]: MUT_VUS,
};
}
export const ANNOTATED_PROTEIN_IMPACT_TYPE_FILTER_ID =
'_cBioPortalAnnotatedProteinImpactTypeFilter_';
export const ANNOTATED_PROTEIN_IMPACT_FILTER_TYPE =
'annotatedProteinImpactType';
export function columnIdToFilterId(columnId: string) {
return '_cBioPortal' + columnId.replace(' ', '') + 'ColumnFilter_';
}
export function isFusion(mutation: Mutation) {
return getSimplifiedMutationType(mutation.mutationType) === 'fusion';
}
export function isUncalled(molecularProfileId: string) {
const r = new RegExp(MOLECULAR_PROFILE_UNCALLED_MUTATIONS_SUFFIX + '$');
return r.test(molecularProfileId);
}
export function getColorForProteinImpactType(
mutations: Mutation[],
colors: IProteinImpactTypeColors = DEFAULT_PROTEIN_IMPACT_TYPE_COLORS,
isPutativeDriver?: (mutation: Partial) => boolean
): string {
return getDefaultColorForProteinImpactType(
normalizeMutations(mutations),
colors,
undefined,
isPutativeDriver
);
}
// TODO remove when done refactoring mutation mapper
export function groupMutationsByProteinStartPos(
mutationData: Mutation[][]
): { [pos: number]: Mutation[] } {
const map: { [pos: number]: Mutation[] } = {};
for (const mutations of mutationData) {
for (const mutation of mutations) {
const codon = mutation.proteinPosStart;
if (codon !== undefined && codon !== null) {
map[codon] = map[codon] || [];
map[codon].push(mutation);
}
}
}
return map;
}
export function groupMutationsByGeneAndPatientAndProteinChange(
mutations: Mutation[]
): { [key: string]: Mutation[] } {
// key = __
const map: { [key: string]: Mutation[] } = {};
for (const mutation of mutations) {
const key = `${mutation.gene.hugoGeneSymbol}_${mutation.patientId}_${mutation.proteinChange}`;
map[key] = map[key] || [];
map[key].push(mutation);
}
return map;
}
export function countDuplicateMutations(groupedMutations: {
[key: string]: Mutation[];
}): number {
// helper to count duplicate mutations
const countMapper = (mutations: Mutation[]) =>
mutations.length > 0 ? mutations.length - 1 : 0;
// helper to get the total sum
const sumReducer = (acc: number, current: number) => acc + current;
return _.values(groupedMutations)
.map(countMapper)
.reduce(sumReducer, 0);
}
export function countUniqueMutations(mutations: Mutation[]): number {
return Object.keys(
groupMutationsByGeneAndPatientAndProteinChange(mutations)
).length;
}
/**
* Protein start positions for the mutations falling between a specific start and end position range
*/
export function getProteinStartPositionsByRange(
data: Mutation[][],
start: number,
end: number
) {
const positions: number[] = [];
data.forEach((mutations: Mutation[]) => {
const mutation = mutations[0];
// only add positions which fall between start & end positions
if (
mutation.proteinPosStart > -1 &&
mutation.proteinPosStart >= start &&
mutation.proteinPosStart <= end
) {
positions.push(mutation.proteinPosStart);
}
if (
mutation.proteinPosEnd > mutation.proteinPosStart &&
mutation.proteinPosEnd >= start &&
mutation.proteinPosEnd <= end
) {
positions.push(mutation.proteinPosEnd);
}
});
return _.uniq(positions);
}
export const GERMLINE_REGEXP = new RegExp(MUTATION_STATUS_GERMLINE, 'i');
/**
* Percentage of cases/samples with a germline mutation in given gene.
* Assumes all given sample ids in the study had germline screening for all
* genes (TODO: use gene panel).
*/
export function germlineMutationRate(
hugoGeneSymbol: string,
mutations: Mutation[],
molecularProfileIdToMolecularProfile: {
[molecularProfileId: string]: MolecularProfile;
},
samples: SampleIdentifier[]
) {
if (mutations.length > 0 && samples.length > 0) {
const sampleIds = stringListToSet(samples.map(toSampleUuid));
const nrCasesGermlineMutation: number = _.chain(mutations)
.filter((m: Mutation) => {
const profile =
molecularProfileIdToMolecularProfile[m.molecularProfileId];
if (profile) {
return (
m.gene.hugoGeneSymbol === hugoGeneSymbol &&
GERMLINE_REGEXP.test(m.mutationStatus) &&
// filter for given sample IDs
!!sampleIds[toSampleUuid(profile.studyId, m.sampleId)]
);
} else {
return false;
}
})
.map(toSampleUuid)
.uniq()
.value().length;
return (nrCasesGermlineMutation * 100.0) / samples.length;
} else {
return 0;
}
}
/**
* Percentage of cases/samples with a somatic mutation in given gene.
*/
export function somaticMutationRate(
hugoGeneSymbol: string,
mutations: Mutation[],
molecularProfileIdToMolecularProfile: {
[molecularProfileId: string]: MolecularProfile;
},
samples: SampleIdentifier[]
) {
if (mutations.length > 0 && samples.length > 0) {
const sampleIds = stringListToSet(samples.map(toSampleUuid));
return (
(_.chain(mutations)
.filter((m: Mutation) => {
const profile =
molecularProfileIdToMolecularProfile[
m.molecularProfileId
];
if (profile) {
return (
m.gene.hugoGeneSymbol === hugoGeneSymbol &&
!GERMLINE_REGEXP.test(m.mutationStatus) &&
// filter for given sample IDs
!!sampleIds[
toSampleUuid(profile.studyId, m.sampleId)
]
);
} else {
return false;
}
})
.map(toSampleUuid)
.uniq()
.value().length *
100.0) /
samples.length
);
} else {
return 0;
}
}
export function isNotGermlineMutation(m: { mutationStatus?: string }) {
return !m.mutationStatus || !GERMLINE_REGEXP.test(m.mutationStatus);
}
export function updateMissingGeneInfo(
mutations: Partial[],
genesByHugoSymbol: { [hugoGeneSymbol: string]: Gene }
) {
mutations.forEach(mutation => {
if (mutation.gene && mutation.gene.hugoGeneSymbol) {
const gene = genesByHugoSymbol[mutation.gene.hugoGeneSymbol];
if (gene) {
// keep the existing "mutation.gene" values: only overwrite missing (undefined) values
mutation.gene = _.merge({}, gene, mutation.gene);
// also update entrezGeneId for the mutation itself
mutation.entrezGeneId =
mutation.entrezGeneId || gene.entrezGeneId;
}
}
});
}
// TODO remove when done refactoring mutation mapper
export function genomicLocationString(genomicLocation: GenomicLocation) {
// mapping chromosome X with 23
let chromosome = genomicLocation.chromosome;
switch (genomicLocation.chromosome.toUpperCase()) {
case 'X':
chromosome = '23';
break;
case 'Y':
chromosome = '24';
break;
default:
}
return `${chromosome},${genomicLocation.start},${genomicLocation.end},${genomicLocation.referenceAllele},${genomicLocation.variantAllele}`;
}
// TODO remove when done refactoring mutation mapper
export function uniqueGenomicLocations(
mutations: Mutation[]
): GenomicLocation[] {
const genomicLocationMap: { [key: string]: GenomicLocation } = {};
mutations.map((mutation: Mutation) => {
const genomicLocation:
| GenomicLocation
| undefined = extractGenomicLocation(mutation);
if (genomicLocation) {
genomicLocationMap[
genomicLocationString(genomicLocation)
] = genomicLocation;
}
});
return _.values(genomicLocationMap);
}
export type VAFReport = {
vaf: number;
variantReadCount: number;
totalReadCount: number;
};
export function getVariantAlleleFrequency(m: Mutation): VAFReport | null {
if (
Number.isInteger(m.tumorRefCount) &&
Number.isInteger(m.tumorAltCount)
) {
const vaf = m.tumorAltCount / (m.tumorAltCount + m.tumorRefCount);
if (isNaN(vaf)) {
return null;
} else {
return {
vaf,
variantReadCount: m.tumorAltCount,
totalReadCount: m.tumorAltCount + m.tumorRefCount,
};
}
} else {
return null;
}
}
export function hasASCNProperty(mutation: Mutation, property: string) {
return (
mutation.alleleSpecificCopyNumber !== undefined &&
(mutation.alleleSpecificCopyNumber as any)[property] !== undefined
);
}
export function getAnnotatedProteinImpactType(
mutation: Partial,
proteinImpactType: string,
isPutativeDriverFun?: (mutation: Partial) => boolean
) {
const isPutativeDriver = !!(
!isPutativeDriverFun || isPutativeDriverFun(mutation)
);
switch (proteinImpactType) {
case ProteinImpactType.MISSENSE:
return isPutativeDriver
? ProteinImpactType.MISSENSE_PUTATIVE_DRIVER
: ProteinImpactType.MISSENSE_UNKNOWN_SIGNIFICANCE;
case ProteinImpactType.TRUNCATING:
return isPutativeDriver
? ProteinImpactType.TRUNCATING_PUTATIVE_DRIVER
: ProteinImpactType.TRUNCATING_UNKNOWN_SIGNIFICANCE;
case ProteinImpactType.INFRAME:
return isPutativeDriver
? ProteinImpactType.INFRAME_PUTATIVE_DRIVER
: ProteinImpactType.INFRAME_UNKNOWN_SIGNIFICANCE;
case ProteinImpactType.FUSION:
return isPutativeDriver
? ProteinImpactType.FUSION_PUTATIVE_DRIVER
: ProteinImpactType.FUSION_UNKNOWN_SIGNIFICANCE;
case ProteinImpactType.SPLICE:
return isPutativeDriver
? ProteinImpactType.SPLICE_PUTATIVE_DRIVER
: ProteinImpactType.SPLICE_UNKNOWN_SIGNIFICANCE;
default:
return ProteinImpactType.OTHER;
}
}
export function createAnnotatedProteinImpactTypeFilter(
isPutativeDriver?: (mutation: Partial) => boolean
) {
const filter = (filter: ProteinImpactTypeFilter, mutation: Mutation) => {
return filter.values.includes(
isPutativeDriver === undefined
? getProteinImpactType(mutation.mutationType || 'other')
: getAnnotatedProteinImpactType(
mutation,
getProteinImpactType(mutation.mutationType || 'other'),
isPutativeDriver
)
);
};
return filter;
}
export function matchNumericalFilter(
value: number | null,
config: NumericalFilterValue
) {
return (
(!config.hideEmptyValues || value !== null) &&
(value === null || value >= config.lowerBound) &&
(value === null || value <= config.upperBound)
);
}
export function createNumericalFilter(getData: (d: Mutation) => number | null) {
const filter = (filter: NumericalFilter, mutation: Mutation) => {
const value = getData(mutation);
return matchNumericalFilter(value, filter.values[0]);
};
return filter;
}
export function matchCategoricalFilterSearch(
value: string,
config: CategoricalFilterValue
) {
const filterCondition = config.filterCondition;
const filterString = config.filterString;
if (filterString === '') {
return true;
}
const valueUpper = value.toUpperCase();
const filterStringUpper = filterString.toUpperCase();
switch (filterCondition) {
case 'contains':
return valueUpper.includes(filterStringUpper);
case 'doesNotContain':
return !valueUpper.includes(filterStringUpper);
case 'equals':
return valueUpper === filterStringUpper;
case 'doesNotEqual':
return valueUpper !== filterStringUpper;
case 'beginsWith':
return valueUpper.startsWith(filterStringUpper);
case 'doesNotBeginWith':
return !valueUpper.startsWith(filterStringUpper);
case 'endsWith':
return valueUpper.endsWith(filterStringUpper);
case 'doesNotEndWith':
return !valueUpper.endsWith(filterStringUpper);
case 'regex':
try {
const regex = new RegExp(filterString);
return regex.test(value);
} catch (e) {
return false;
}
default:
return false;
}
}
export function createCategoricalFilter(getData: (d: Mutation) => string) {
const filter = (filter: CategoricalFilter, mutation: Mutation) => {
const value = getData(mutation) || '(Blanks)';
return (
matchCategoricalFilterSearch(value, filter.values[0]) &&
filter.values[0].selections.has(value)
);
};
return filter;
}