diff --git a/src/component/variantPage/BasicInfo.module.scss b/src/component/variantPage/BasicInfo.module.scss index a2e4284f..38394c7d 100644 --- a/src/component/variantPage/BasicInfo.module.scss +++ b/src/component/variantPage/BasicInfo.module.scss @@ -76,6 +76,15 @@ align-items: center; } +.vue-wrapper { + border: 1px dotted transparent; + border-radius: 5; + + &:hover { + border: 1px dotted #8e7cc3; + } +} + .missense-mutation { color: #008000; font-weight: bold; diff --git a/src/component/variantPage/BasicInfo.tsx b/src/component/variantPage/BasicInfo.tsx index 7c3c5683..9d610c94 100644 --- a/src/component/variantPage/BasicInfo.tsx +++ b/src/component/variantPage/BasicInfo.tsx @@ -20,6 +20,8 @@ import basicInfo from './BasicInfo.module.scss'; import { Link } from 'react-router-dom'; import { annotationQueryFields } from '../../config/configDefaults'; import Toggle from '../Toggle'; +import { RevisedProteinEffectRecord, VUE } from './biologicalFunction/ReVUE'; +import { ReVUEContent } from './biologicalFunction/ReVUE'; interface IBasicInfoProps { annotation: VariantAnnotationSummary | undefined; @@ -31,6 +33,8 @@ interface IBasicInfoProps { isCanonicalTranscriptSelected?: boolean | undefined; allValidTranscripts: string[]; onTranscriptSelect(transcriptId: string): void; + vue: VUE | undefined; + revisedProteinEffectRecord: RevisedProteinEffectRecord | undefined; } type MutationTypeFormat = { @@ -141,7 +145,8 @@ export default class BasicInfo extends React.Component { if (this.props.annotation) { let renderData: BasicInfoData[] | null = this.getDataFromTranscriptConsequenceSummary( - selectedTranscript || canonicalTranscript + selectedTranscript || canonicalTranscript, + this.props.variant ); if (renderData === null) { return null; @@ -149,7 +154,14 @@ export default class BasicInfo extends React.Component { if (renderData) { renderData = renderData.filter((data) => data.value != null); // remove null fields } - let basicInfoList = _.map(renderData, (data) => { + let basicInfoBeforeVUE = _.map(renderData.slice(0, 2), (data) => { + return this.generateBasicInfoPills( + data.value, + data.key, + data.category + ); + }); + let basicInfoAfterVUE = _.map(renderData.slice(2), (data) => { return this.generateBasicInfoPills( data.value, data.key, @@ -160,7 +172,14 @@ export default class BasicInfo extends React.Component { return (
- {basicInfoList} + {basicInfoBeforeVUE} + {this.props.revisedProteinEffectRecord && + this.props.vue && + this.generateBasicInfoReVUE( + this.props.vue, + this.props.revisedProteinEffectRecord + )} + {basicInfoAfterVUE} {this.jsonButton()} {haveTranscriptTable && this.transcriptsButton(this.showAllTranscripts)} @@ -204,7 +223,8 @@ export default class BasicInfo extends React.Component { } public getDataFromTranscriptConsequenceSummary( - transcript: TranscriptConsequenceSummary | undefined + transcript: TranscriptConsequenceSummary | undefined, + variant: string ): BasicInfoData[] | null { // no canonical transcript, return null if (transcript === undefined) { @@ -235,18 +255,23 @@ export default class BasicInfo extends React.Component { key: 'tsg', category: 'tsg', }); - // protein change - parsedData.push({ - value: transcript.hgvspShort, - key: 'hgvsShort', - category: 'default', - }); - // variant classification - parsedData.push({ - value: transcript.variantClassification, - key: 'variantClassification', - category: getMutationTypeClassName(transcript), - }); + + // Check if variant is a VUE + if (!this.props.revisedProteinEffectRecord) { + // protein change + parsedData.push({ + value: transcript.hgvspShort, + key: 'hgvsShort', + category: 'default', + }); + // variant classification + parsedData.push({ + value: transcript.variantClassification, + key: 'variantClassification', + category: getMutationTypeClassName(transcript), + }); + } + // variant type parsedData.push({ value: this.props.annotation!.variantType, @@ -342,6 +367,72 @@ export default class BasicInfo extends React.Component { ); } + public generateBasicInfoReVUE( + vue: VUE, + revisedProteinEffectRecord: RevisedProteinEffectRecord + ) { + return ( + + | Source:{' '} + + reVUE + + + } + > + + + reVUE logo + + VUE + + + {revisedProteinEffectRecord.revisedProteinEffect} + + + {revisedProteinEffectRecord.variantClassification} + + + + + ); + } + public generateBasicInfoPills( value: string | null, key: string, diff --git a/src/component/variantPage/BiologicalFunction.tsx b/src/component/variantPage/BiologicalFunction.tsx index 99eb1d3b..eb77d21f 100644 --- a/src/component/variantPage/BiologicalFunction.tsx +++ b/src/component/variantPage/BiologicalFunction.tsx @@ -7,23 +7,18 @@ import { IndicatorQueryResp } from 'oncokb-ts-api-client'; import Separator from '../Separator'; import Oncokb from './biologicalFunction/Oncokb'; import ClinvarInterpretation from './biologicalFunction/ClinvarInterpretation'; -import CuriousCase from './biologicalFunction/CuriousCase'; -import { CuriousCases } from 'genome-nexus-ts-api-client/dist/generated/GenomeNexusAPIInternal'; +import ReVUE, { VUE } from './biologicalFunction/ReVUE'; interface IBiologicalFunctionProps { oncokb: IndicatorQueryResp | undefined; isCanonicalTranscriptSelected: boolean; clinvar?: Clinvar; - curiousCases?: CuriousCases; + vue?: VUE; } @observer class BiologicalFunction extends React.Component { public render() { - // only show curious case when URL has "curious" - const showCuriousCase = - window.location.search.split('curious').length > 1 ? true : false; - return ( <> { this.props.isCanonicalTranscriptSelected } /> - {showCuriousCase && ( - <> - - - - )} + + ); } diff --git a/src/component/variantPage/FunctionalGroups.tsx b/src/component/variantPage/FunctionalGroups.tsx index 58f40bd9..8f470f9f 100644 --- a/src/component/variantPage/FunctionalGroups.tsx +++ b/src/component/variantPage/FunctionalGroups.tsx @@ -16,7 +16,7 @@ import functionalGroupsStyle from './functionalGroups.module.scss'; import ClinicalImplication from './ClinicalImplication'; import { RemoteData } from 'cbioportal-utils'; import PrevalenceInCancer from './PrevalenceInCancer'; -import { CuriousCases } from 'genome-nexus-ts-api-client/dist/generated/GenomeNexusAPIInternal'; +import { VUE } from './biologicalFunction/ReVUE'; interface IFunctionalGroupsProps { annotationInternal?: VariantAnnotationSummary; @@ -28,8 +28,8 @@ interface IFunctionalGroupsProps { indexAnnotationsByGenomicLocationPromise: RemoteData<{ [genomicLocation: string]: VariantAnnotation; }>; - curiousCases?: CuriousCases; genomeBuild?: string; + vue?: VUE; } @observer @@ -67,7 +67,7 @@ class FunctionalGroups extends React.Component { this.props.isCanonicalTranscriptSelected } clinvar={this.clinvar} - curiousCases={this.props.curiousCases} + vue={this.props.vue} /> diff --git a/src/component/variantPage/biologicalFunction/CuriousCase.tsx b/src/component/variantPage/biologicalFunction/CuriousCase.tsx deleted file mode 100644 index aa8b167a..00000000 --- a/src/component/variantPage/biologicalFunction/CuriousCase.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { DefaultTooltip } from 'cbioportal-frontend-commons'; -import { CuriousCases } from 'genome-nexus-ts-api-client/dist/generated/GenomeNexusAPIInternal'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import functionalGroupsStyle from '../functionalGroups.module.scss'; - -interface ICuriousCaseProps { - curiousCases?: CuriousCases; -} - -const TooltipLinks: React.FunctionComponent<{ pubmedIds: number[] }> = ( - props -) => { - let tooltipLinks: JSX.Element[] = []; - props.pubmedIds.forEach((id) => { - tooltipLinks.push( - - {id} - {`; `} - - ); - }); - return <>{tooltipLinks}; -}; - -const CuriousCaseContent: React.FunctionComponent<{ - curiousCases?: CuriousCases; -}> = (props) => { - return props.curiousCases ? ( - - {props.curiousCases.comment} {`. Pubmed ids: `} - {props.curiousCases.pubmedIds ? ( - - ) : ( - 'NA' - )} - - ) : ( - NA - ); -}; - -@observer -export default class CuriousCase extends React.Component { - public render() { - return ( -
-
- Curated list of splice variants.
} - > - - Curious Case - - -
- -
- ); - } -} diff --git a/src/component/variantPage/biologicalFunction/ReVUE.tsx b/src/component/variantPage/biologicalFunction/ReVUE.tsx new file mode 100644 index 00000000..ae4e047b --- /dev/null +++ b/src/component/variantPage/biologicalFunction/ReVUE.tsx @@ -0,0 +1,70 @@ +import { DefaultTooltip } from 'cbioportal-frontend-commons'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import functionalGroupsStyle from '../functionalGroups.module.scss'; + +export declare type RevisedProteinEffectRecord = { + revisedProteinEffect: string; + variantClassification: string; + variant: string; +}; + +export declare type VUE = { + comment: string; + context: string; + referenceText: string; + pubmedIds: Array; + revisedProteinEffects: Array; +}; + +interface IReVUEProps { + vue?: VUE; +} + +export const ReVUEContent: React.FunctionComponent<{ + vue?: VUE; +}> = (props) => { + return props.vue ? ( + + {props.vue.comment}{' '} + + ({props.vue.referenceText}) + + + ) : ( + NA + ); +}; + +@observer +export default class ReVUE extends React.Component { + public render() { + return ( +
+
+ + Repository for Variants with Unexpected Effects +
+ } + > + + reVUE + + +
+ + + ); + } +} diff --git a/src/data/VUEs.json b/src/data/VUEs.json new file mode 100644 index 00000000..ea583561 --- /dev/null +++ b/src/data/VUEs.json @@ -0,0 +1,155 @@ +[ + { + "hugoGeneSymbol": "APC", + "genomicLocation": "5:g.112151184A>G", + "defaultEffect": "synonymous", + "comment": "Introduces splice acceptor site causing a frameshift", + "pubmedIds": [29316426], + "context": "Recurrent alteration in CRC", + "referenceText": "Yaeger et al, 2018", + "revisedProteinEffects": [ + { + "variant": "5:g.112151184A>G", + "transcriptId": "ENST00000257430", + "revisedProteinEffect": "p.G279Ffs*10", + "variantClassification": "Frame_Shift_Ins" + } + ] + }, + { + "hugoGeneSymbol": "BAP1", + "genomicLocation": "3:g.52439306A>G", + "defaultEffect": "synonymous", + "comment": "Leads to exon 11 skipping", + "context": "Recurrent and prognostic in kidney cancer", + "pubmedIds": [33681728], + "referenceText": "Niersch et al, 2021", + "revisedProteinEffects": [ + { + "variant": "3:g.52439306A>G", + "transcriptId": "ENST00000460680", + "revisedProteinEffect": "p.D311Gfs*23", + "variantClassification": "Frame_Shift_Del" + } + ] + }, + { + "genomicLocation": "17:g.7579883G>A", + "defaultEffect": "synonymous", + "comment": "Affects mRNA structure, reduces HDM2 binding", + "context": "Recurrent in many cancer types", + "pubmedIds": [31671760], + "hugoGeneSymbol": "TP53", + "referenceText": "Swiatkowska et al, 2019", + "revisedProteinEffects": [ + { + "variant": "17:g.7579883G>A", + "transcriptId": "ENST00000269305", + "revisedProteinEffect": "p.V10=", + "variantClassification": "Silent" + } + ] + }, + { + "genomicLocation": "12:g.25380277G>T", + "defaultEffect": "missense", + "comment": "Introduces a splice donor site if Q61K is not co-occurring with G60G", + "context": "Recurrent in many cancer types", + "pubmedIds": [35236983], + "hugoGeneSymbol": "KRAS", + "referenceText": "Kobayashi et al, 2022", + "revisedProteinEffects": [ + { + "variant": "12:g.25380277G>T", + "transcriptId": "ENST00000256078", + "revisedProteinEffect": "p.X61", + "variantClassification": "splice_donor_variant" + } + ] + }, + { + "hugoGeneSymbol": "CTNNB1", + "genomicLocation": "Deletions flanking exon 2-3 (chr3:41265579_41266277del)", + "defaultEffect": "splice", + "comment": "Complete exon 3 skip or inframe deletion in exon 3", + "pubmedIds": [29316426], + "context": "Recurrent alteration in CRC", + "referenceText": "Yaeger et al, 2018", + "revisedProteinEffects": [ + { + "variant": "4:g.55593576_55593606del", + "transcriptId": "ENST00000349496", + "revisedProteinEffect": "p.E9_D81del", + "variantClassification": "In_Frame_Del" + } + ] + }, + { + "hugoGeneSymbol": "KIT", + "genomicLocation": "Deletions flanking intron 10 - exon 11 boundary (4:55593576_55593606del)", + "defaultEffect": "splice", + "comment": "Changes splice donor site, cause inframe deletion in exon 11", + "context": "Actionable in GIST", + "pubmedIds": [15507676], + "referenceText": "Corless et al, 2004", + "revisedProteinEffects": [ + { + "variant": "4:g.55593576_55593606del", + "transcriptId": "ENST00000288135", + "revisedProteinEffect": "p.549_557del", + "variantClassification": "In_Frame_Del" + } + ] + }, + { + "hugoGeneSymbol": "MET", + "genomicLocation": "Indels and substitutions flanking exon 14 (7:116411926_116412083del)", + "defaultEffect": "splice", + "comment": "Skipping of exon 14", + "context": "Actionable in NSCLC", + "pubmedIds": [27343443], + "referenceText": "Schrock et al, 2014", + "revisedProteinEffects": [ + { + "variant": "13:g.28608217_28608218insCCAAACTCTAAATTTTCTCTTGGAAACTCCCATTTGAGATCATATTCATATTCTCTG", + "transcriptId": "ENST00000241453", + "revisedProteinEffect": "p.E598_F612dup", + "variantClassification": "In_Frame_Ins" + } + ] + }, + { + "hugoGeneSymbol": "PIK3R1", + "genomicLocation": "Indels and substitutions flanking exon 11 (5:67589663G>A)", + "defaultEffect": "splice", + "comment": "Skipping of exon 11", + "context": "Recurrent in breast and uterine cancer", + "pubmedIds": [25488983], + "referenceText": "Lucas et al, 2014", + "revisedProteinEffects": [ + { + "variant": "5:g.67589663G>C", + "transcriptId": "ENST00000521381", + "revisedProteinEffect": "p.D434_E476del", + "variantClassification": "In_Frame_Del" + } + ] + }, + { + "hugoGeneSymbol": "FLT3", + "genomicLocation": "Indels around tyrosine kinase (13:28608214_28608215insTT...)", + "defaultEffect": "splice", + "comment": "Internal Tandem Duplication", + "context": "Recurrent alteration in AML", + "pubmedIds": [31285539], + "referenceText": "Zhang et al, 2020", + "revisedProteinEffects": [ + { + "variant": "13:g.28608217_28608218insCCAAACTCTAAATTTTCTCTTGGAAACTCCCATTTGAGATCATATTCATATTCTCTG", + "transcriptId": "ENST00000241453", + "revisedProteinEffect": "p.E598_F612dup", + "variantClassification": "In_Frame_Ins" + } + ] + } +] diff --git a/src/page/Variant.tsx b/src/page/Variant.tsx index 0f535f95..7286b237 100644 --- a/src/page/Variant.tsx +++ b/src/page/Variant.tsx @@ -148,6 +148,7 @@ class Variant extends React.Component { private getMutationMapper() { const mutation = variantToMutation( this.props.store.annotationSummary, + this.props.store.revisedProteinEffect, this.props.store.selectedTranscript ); if ( @@ -185,7 +186,7 @@ class Variant extends React.Component { [TrackName.Exon]: 'visible', [TrackName.UniprotTopology]: 'visible', }} - hugoSymbol={mutation[0].gene.hugoGeneSymbol} + hugoSymbol={mutation[0]?.gene?.hugoGeneSymbol} entrezGeneId={Number( getTranscriptConsequenceSummary( this.props.store.annotationSummary, @@ -364,7 +365,9 @@ class Variant extends React.Component { mutation={ variantToMutation( this.props.store - .annotationSummary + .annotationSummary, + this.props.store + .revisedProteinEffect )[0] } variant={this.props.variant} @@ -391,6 +394,11 @@ class Variant extends React.Component { transcriptId ) } + vue={this.props.store.vue.result} + revisedProteinEffectRecord={ + this.props.store + .revisedProteinEffect + } /> @@ -470,12 +478,10 @@ class Variant extends React.Component { ]: VariantAnnotation; }> } - curiousCases={ - this.props.store.curiousCases.result - } genomeBuild={ this.props.store.genomeBuild.result } + vue={this.props.store.vue.result} /> diff --git a/src/page/VariantStore.ts b/src/page/VariantStore.ts index e52456db..5ddae209 100644 --- a/src/page/VariantStore.ts +++ b/src/page/VariantStore.ts @@ -22,14 +22,15 @@ import { import { annotationQueryFields } from '../config/configDefaults'; import { getTranscriptConsequenceSummary } from '../util/AnnotationSummaryUtil'; import { getDataFetcher } from '../util/ApiUtils'; -import genomeNexusInternalClient from '../util/genomeNexusClientInternalInstance'; +//import genomeNexusInternalClient from '../util/genomeNexusClientInternalInstance'; import genomeNexusClient from '../util/genomeNexusClientInstance'; import oncoKbClient from '../util/oncokbClientInstance'; import { - variantToGenomicLocationString, + //variantToGenomicLocationString, variantToMutation, } from '../util/variantUtils'; import { MainStore } from './MainStore'; +import { default as VUEs } from './../data/VUEs.json'; export interface VariantStoreConfig { variant: string; @@ -176,7 +177,10 @@ export class VariantStore { await: () => [this.civicGenes], invoke: async () => { if (this.civicGenes.result) { - const mutations = variantToMutation(this.annotationSummary); + const mutations = variantToMutation( + this.annotationSummary, + this.revisedProteinEffect + ); mutations.forEach((mutation) => { // fetchCivicVariants cannot handle protein change values starting with 'p.' @@ -198,20 +202,44 @@ export class VariantStore { onError: () => {}, }); - readonly curiousCases = remoteData({ + readonly vue = remoteData({ await: () => [this.annotation], invoke: async () => { - return genomeNexusInternalClient.fetchCuriousCasesGET({ - genomicLocation: encodeURIComponent( - variantToGenomicLocationString(this.annotationSummary) - ), - }); - }, - onError: (err: Error) => { - // fail silently + if (this.annotation.result?.annotation_summary) { + const transcriptSummary = + this.annotationSummary?.transcriptConsequenceSummary; + // Check if variant is a VUE + const vue = VUEs.find( + (x) => + x.hugoGeneSymbol === transcriptSummary?.hugoGeneSymbol + ); + if ( + vue?.revisedProteinEffects?.find( + (x) => x.variant === this.variant + ) + ) { + return vue; + } else { + return undefined; + } + } }, + onError: () => {}, }); + @computed + get revisedProteinEffect() { + return this.vue.result?.revisedProteinEffects?.find( + (x) => + x.variant === this.variant && + (this.selectedTranscript === x.transcriptId || + // selectedTranscript is empty string if canonical is shown + (!this.selectedTranscript && + this.annotationSummary?.canonicalTranscriptId === + x.transcriptId)) + ); + } + @computed get annotationSummary() { return this.annotation.result @@ -223,6 +251,7 @@ export class VariantStore { get getMutationMapperStore() { const mutation = variantToMutation( this.annotationSummary, + this.revisedProteinEffect, this.selectedTranscript ); if ( diff --git a/src/util/variantUtils.ts b/src/util/variantUtils.ts index 5a53b4e2..2cf33da2 100644 --- a/src/util/variantUtils.ts +++ b/src/util/variantUtils.ts @@ -3,10 +3,12 @@ import { Mutation, } from 'cbioportal-utils'; import { VariantAnnotationSummary } from 'genome-nexus-ts-api-client'; +import { RevisedProteinEffectRecord } from '../component/variantPage/biologicalFunction/ReVUE'; import { getTranscriptConsequenceSummary } from './AnnotationSummaryUtil'; export function variantToMutation( data: VariantAnnotationSummary | undefined, + revisedProteinEffectRecord?: RevisedProteinEffectRecord | undefined, transcript?: string ): Mutation[] { let mutations = []; @@ -16,6 +18,28 @@ export function variantToMutation( transcript ); if (data && transcriptConsequence) { + let proteinChange; + let proteinPosStart; + let proteinPosEnd; + let mutationType; + + if (revisedProteinEffectRecord?.revisedProteinEffect) { + proteinChange = revisedProteinEffectRecord.revisedProteinEffect; + proteinPosStart = getProteinPosStart(proteinChange); + proteinPosEnd = + getProteinPositionFromProteinChange(proteinChange)?.end; + mutationType = revisedProteinEffectRecord.variantClassification; + } else { + proteinChange = transcriptConsequence.hgvspShort; + proteinPosStart = transcriptConsequence.proteinPosition?.start + ? transcriptConsequence.proteinPosition.start + : getProteinPosStart(transcriptConsequence.hgvspShort); + proteinPosEnd = transcriptConsequence.proteinPosition + ? transcriptConsequence.proteinPosition.end + : undefined; + mutationType = transcriptConsequence.variantClassification; + } + mutation = { gene: { hugoGeneSymbol: transcriptConsequence.hugoGeneSymbol, @@ -26,14 +50,10 @@ export function variantToMutation( endPosition: data.genomicLocation.end, referenceAllele: data.genomicLocation.referenceAllele, variantAllele: data.genomicLocation.variantAllele, - proteinChange: transcriptConsequence.hgvspShort, - proteinPosStart: transcriptConsequence.proteinPosition?.start - ? transcriptConsequence.proteinPosition.start - : getProteinPosStart(transcriptConsequence.hgvspShort), - proteinPosEnd: transcriptConsequence.proteinPosition - ? transcriptConsequence.proteinPosition.end - : undefined, - mutationType: transcriptConsequence.variantClassification, + proteinChange, + proteinPosStart, + proteinPosEnd, + mutationType, }; mutations.push(mutation); }