From 54a2d882bd09396595381594d47ea49d3c84f5ae Mon Sep 17 00:00:00 2001 From: Dzmitry Hramyka Date: Mon, 25 Sep 2023 11:31:32 +0200 Subject: [PATCH] feat: Adapt ACMG Rating for multiple sources of info (#65) (#66) --- .gitignore | 2 +- frontend/src/components/HeaderDetailPage.vue | 2 +- .../components/VariantDetails/AcmgRating.vue | 820 ++++-------------- .../VariantDetails/FreqsAutosomal.vue | 2 +- .../VariantDetails/FreqsMitochondrial.vue | 2 +- .../VariantDetails/VariantConservation.vue | 2 +- .../VariantDetails/VariantFreqs.vue | 2 +- .../VariantDetails/VariantTools.vue | 2 +- .../components/__tests__/AcmgRating.spec.ts | 44 +- frontend/src/lib/__tests__/acmgSeqVar.spec.ts | 621 +++++++++++++ .../src/{api => lib}/__tests__/utils.spec.ts | 0 frontend/src/lib/acmgSeqVar.ts | 697 +++++++++++++++ frontend/src/{api => lib}/utils.ts | 2 +- frontend/src/stores/__tests__/misc.spec.ts | 4 +- .../__tests__/variantAcmgRating.spec.ts | 79 +- frontend/src/stores/variantAcmgRating.ts | 65 +- frontend/src/stores/variantInfo.ts | 2 +- frontend/src/views/ACMGCriteriaDocs.vue | 19 +- frontend/src/views/GeneDetailView.vue | 2 +- frontend/src/views/HomeView.vue | 2 +- .../views/__tests__/VariantDetailView.spec.ts | 22 +- 21 files changed, 1636 insertions(+), 757 deletions(-) create mode 100644 frontend/src/lib/__tests__/acmgSeqVar.spec.ts rename frontend/src/{api => lib}/__tests__/utils.spec.ts (100%) create mode 100644 frontend/src/lib/acmgSeqVar.ts rename frontend/src/{api => lib}/utils.ts (98%) diff --git a/.gitignore b/.gitignore index 98019b4e..c2f3f751 100644 --- a/.gitignore +++ b/.gitignore @@ -203,7 +203,7 @@ tags ### VisualStudioCode ### .vscode/* -!.vscode/settings.json +.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json diff --git a/frontend/src/components/HeaderDetailPage.vue b/frontend/src/components/HeaderDetailPage.vue index 166b8188..e339d149 100644 --- a/frontend/src/components/HeaderDetailPage.vue +++ b/frontend/src/components/HeaderDetailPage.vue @@ -3,7 +3,7 @@ import { watch, ref } from 'vue' import { useRouter } from 'vue-router' import SearchBar from '@/components/SearchBar.vue' -import { search } from '@/api/utils' +import { search } from '@/lib/utils' export interface Props { searchTerm?: string diff --git a/frontend/src/components/VariantDetails/AcmgRating.vue b/frontend/src/components/VariantDetails/AcmgRating.vue index 628252e2..e926facc 100644 --- a/frontend/src/components/VariantDetails/AcmgRating.vue +++ b/frontend/src/components/VariantDetails/AcmgRating.vue @@ -3,8 +3,16 @@ import { computed, onMounted, ref, watch } from 'vue' import { StoreState } from '@/stores/misc' import { useVariantAcmgRatingStore } from '@/stores/variantAcmgRating' - -// Defining props and store +import { + AcmgCriteria, + StateSource, + Presence, + ALL_ACMG_CRITERIA, + ACMG_EVIDENCE_LEVELS_PATHOGENIC, + ACMG_EVIDENCE_LEVELS_BENIGN, + ACMG_CRITERIA_DEFS, + AcmgEvidenceLevel +} from '@/lib/acmgSeqVar' const props = defineProps({ smallVariant: Object @@ -12,445 +20,67 @@ const props = defineProps({ const acmgRatingStore = useVariantAcmgRatingStore() -// Defining data - -const ScoreExplanation = { - 1: 'Benign', - 2: 'Likely benign', - 3: 'Uncertain significance', - 4: 'Likely pathogenic', - 5: 'Pathogenic' -} - -const ACMGRankingScores = { - PVS: { - name: 'Very Strong', - score: 8 - }, - PS: { - name: 'Strong', - score: 4 - }, - PM: { - name: 'Moderate', - score: 2 - }, - PP: { - name: 'Supporting', - score: 1 - }, - BA: { - name: 'Stand alone', - score: -8 - }, - BS: { - name: 'Strong', - score: -4 - }, - BP: { - name: 'Supporting', - score: -2 - } -} - -const acmgRating = { - pathogenic: { - pvs1: { - name: 'PVS1', - id: 'pvs1', - description: - 'Null variant (nonsense, frameshift, canonical ±1 or 2 splice sites, initiation codon, single or multi-exon deletion) in a gene where LOF is a known mechanism of disease', - hint: 'null variant', - score: ACMGRankingScores.PVS, - scoreCustom: null, - active: false - }, - ps1: { - name: 'PS1', - id: 'ps1', - description: - 'Same amino acid change as a previously established pathogenic variant regardless of nucleotide change', - hint: 'literature: this AA exchange', - score: ACMGRankingScores.PS, - scoreCustom: null, - active: false - }, - ps2: { - name: 'PS2', - id: 'ps2', - description: - 'De novo (both maternity and paternity confirmed) in a patient with the disease and no family history', - hint: 'confirmed de novo', - score: ACMGRankingScores.PS, - scoreCustom: null, - active: false - }, - ps3: { - name: 'PS3', - id: 'ps3', - description: - 'Well-established in vitro or in vivo functional studies supportive of a damaging effect on the gene or gene product', - hint: 'supported by functional studies', - score: ACMGRankingScores.PS, - scoreCustom: null, - active: false - }, - ps4: { - name: 'PS4', - id: 'ps4', - description: - 'The prevalence of the variant in affected individuals is significantly increased compared with the prevalence in controls', - hint: 'prevalende in disease controls', - score: ACMGRankingScores.PS, - scoreCustom: null, - active: false - }, - pm1: { - name: 'PM1', - id: 'pm1', - description: - 'Located in a mutational hot spot and/or critical and well-established functional domain (e.g., active site of an enzyme) without benign variation', - hint: 'variant in hotspot (missense)', - score: ACMGRankingScores.PM, - scoreCustom: null, - active: false - }, - pm2: { - name: 'PM2', - id: 'pm2', - description: - 'Absent from controls (or at extremely low frequency if recessive) in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium', - hint: 'rare; < 1:20.000 in ExAC', - score: ACMGRankingScores.PM, - scoreCustom: null, - active: false - }, - pm3: { - name: 'PM3', - id: 'pm3', - description: 'For recessive disorders, detected in trans with a pathogenic variant', - hint: 'AR: trans with known pathogenic', - score: ACMGRankingScores.PM, - scoreCustom: null, - active: false - }, - pm4: { - name: 'PM4', - id: 'pm4', - description: - 'Protein length changes as a result of in-frame deletions/insertions in a nonrepeat region or stop-loss variants', - hint: 'protein length change', - score: ACMGRankingScores.PM, - scoreCustom: null, - active: false - }, - pm5: { - name: 'PM5', - id: 'pm5', - description: - 'Novel missense change at an amino acid residue where a different missense change determined to be pathogenic has been seen before', - hint: 'literature: AA exchange same pos', - score: ACMGRankingScores.PM, - scoreCustom: null, - active: false - }, - pm6: { - name: 'PM6', - id: 'pm6', - description: 'Assumed de novo, but without confirmation of paternity and maternity', - hint: 'assumed de novo', - score: ACMGRankingScores.PM, - scoreCustom: null, - active: false - }, - pp1: { - name: 'PP1', - id: 'pp1', - description: - 'Cosegregation with disease in multiple affected family members in a gene definitively known to cause the disease', - hint: 'cosegregates in family', - score: ACMGRankingScores.PP, - scoreCustom: null, - active: false - }, - pp2: { - name: 'PP2', - id: 'pp2', - description: - 'Missense variant in a gene that has a low rate of benign missense variation and in which missense variants are a common mechanism of disease', - hint: 'few missense in gene', - score: ACMGRankingScores.PP, - scoreCustom: null, - active: false - }, - pp3: { - name: 'PP3', - id: 'pp3', - description: - 'Multiple lines of computational evidence support a deleterious effect on the gene or gene product (conservation, evolutionary, splicing impact, etc.)', - hint: 'predicted pathogenic >= 2', - score: ACMGRankingScores.PP, - scoreCustom: null, - active: false - }, - pp4: { - name: 'PP4', - id: 'pp4', - description: - "Patient's phenotype or family history is highly specific for a disease with a single genetic etiology", - hint: 'phenotype/pedigree match gene', - score: ACMGRankingScores.PP, - scoreCustom: null, - active: false - }, - pp5: { - name: 'PP5', - id: 'pp5', - description: - 'Reputable source recently reports variant as pathogenic, but the evidence is not available to the laboratoryto perform an independent evaluation', - hint: 'reliable source: pathogenic', - score: ACMGRankingScores.PP, - scoreCustom: null, - active: false - } - }, - benign: { - ba1: { - name: 'BA1', - id: 'ba1', - description: - 'Allele frequency is >5% in Exome Sequencing Project, 1000 Genomes Project, or Exome Aggregation Consortium', - hint: 'allele frequency > 5%', - score: ACMGRankingScores.BA, - scoreCustom: null, - active: false - }, - bs1: { - name: 'BS1', - id: 'bs1', - description: 'Allele frequency is greater than expected for disorder', - hint: 'disease: allele freq. too high', - score: ACMGRankingScores.BS, - scoreCustom: null, - active: false - }, - bs2: { - name: 'BS2', - id: 'bs2', - description: - 'Observed in a healthy adult individual for a recessive (homozygous), dominant (heterozygous), or X-linked (hemizygous) disorder, with full penetrance expected at an early age', - hint: 'observed in healthy individual', - score: ACMGRankingScores.BS, - scoreCustom: null, - active: false - }, - bs3: { - name: 'BS3', - id: 'bs3', - description: - 'Well-established in vitro or in vivo functional studies show no damaging effect on protein function or splicing', - hint: 'functional studies: benign', - score: ACMGRankingScores.BS, - scoreCustom: null, - active: false - }, - bs4: { - name: 'BS4', - id: 'bs4', - description: 'Lack of segregation in affected members of a family', - hint: 'lack of segregation', - score: ACMGRankingScores.BS, - scoreCustom: null, - active: false - }, - bp1: { - name: 'BP1', - id: 'bp1', - description: - 'Missense variant in a gene for which primarily truncating variants are known to cause disease', - hint: 'missense in gene with truncating', - score: ACMGRankingScores.BP, - scoreCustom: null, - active: false - }, - bp2: { - name: 'BP2', - id: 'bp2', - description: - 'Observed in trans with a pathogenic variant for a fully penetrant dominant gene/disorder or observed in cis with a pathogenic variant in any inheritance pattern', - hint: 'other variant is causative', - score: ACMGRankingScores.BP, - scoreCustom: null, - active: false - }, - bp3: { - name: 'BP3', - id: 'bp3', - description: 'In-frame deletions/insertions in a repetitive region without a known function', - hint: 'in-frame indel in repeat', - score: ACMGRankingScores.BP, - scoreCustom: null, - active: false - }, - bp4: { - name: 'BP4', - id: 'bp4', - description: - 'Multiple lines of computational evidence suggest no impact on gene or gene product (conservation, evolutionary,splicing impact, etc.)', - hint: 'prediction: benign', - score: ACMGRankingScores.BP, - scoreCustom: null, - active: false - }, - bp5: { - name: 'BP5', - id: 'bp5', - description: 'Variant found in a case with an alternate molecular basis for disease', - hint: 'different gene in other case', - score: ACMGRankingScores.BP, - scoreCustom: null, - active: false - }, - bp6: { - name: 'BP6', - id: 'bp6', - description: - 'Reputable source recently reports variant as benign, but the evidence is not available to the laboratory to perform an independent evaluation', - hint: 'reputable source: benign', - score: ACMGRankingScores.BP, - scoreCustom: null, - active: false - }, - bp7: { - name: 'BP7', - id: 'bp7', - description: - 'A synonymous (silent) variant for which splicing prediction algorithms predict no impact to the splice consensus sequence nor the creation of a new splice site AND the nucleotide is not highly conserved', - hint: 'silent, no splicing/conservation', - score: ACMGRankingScores.BP, - scoreCustom: null, - active: false - } - } -} - -const acmgRatingComputed = ref(JSON.parse(JSON.stringify(acmgRating))) -const acmgRatingCustom = ref(JSON.parse(JSON.stringify(acmgRating))) const acmgRatingConflicting = ref(false) -const acmgRatingScore = ref(0) -const acmgRatingScoreComputed = ref(0) -const acmgRatingPathogenicScore = ref(0) -const acmgRatingBenignScore = ref(0) -const showTooltip = ref(false) const showSwitches = ref(false) const showFailed = ref(false) -// Defining methods - const unsetAcmgRating = () => { - acmgRatingCustom.value = JSON.parse(JSON.stringify(acmgRating)) -} - -const setAcmgRating = () => { - if (acmgRatingStore.acmgRatingComputed) { - for (const [key, value] of Object.entries(acmgRatingStore.acmgRatingComputed)) { - if (value === true) { - for (const [criteriaKey, criteria] of Object.entries( - acmgRatingComputed.value.pathogenic - ) as any) { - if (criteriaKey === key) { - criteria.active = true - } - } - for (const [criteriaKey, criteria] of Object.entries( - acmgRatingComputed.value.benign - ) as any) { - if (criteriaKey === key) { - criteria.active = true - } - } - } - } - acmgRatingCustom.value = JSON.parse(JSON.stringify(acmgRatingComputed.value)) - const acmgScore = calculateAcmgScore(acmgRatingComputed.value) - acmgRatingScoreComputed.value = acmgScore.acmgScore - } else { - acmgRatingComputed.value = JSON.parse(JSON.stringify(acmgRating)) - acmgRatingCustom.value = JSON.parse(JSON.stringify(acmgRating)) - unsetAcmgRating() - } + acmgRatingStore.acmgRating.setUserPresenceAbsent() } const resetAcmgRating = () => { - acmgRatingCustom.value = JSON.parse(JSON.stringify(acmgRatingComputed.value)) + acmgRatingStore.acmgRating.setUserPresenceInterVar() } -const updateAcmgClass = (isConflicting: boolean, pathogenicScore: number, benignScore: number) => { +const updateAcmgConflicting = (isConflicting: boolean) => { acmgRatingConflicting.value = isConflicting - acmgRatingPathogenicScore.value = pathogenicScore - acmgRatingBenignScore.value = -benignScore - acmgRatingScore.value = pathogenicScore + benignScore } -const calculateAcmgScore = (acmgRating: any) => { - let pathogenicScore = 0 - let benignScore = 0 - for (const criteria of Object.values(acmgRating.pathogenic) as any) { - if (criteria.active) { - pathogenicScore += criteria.scoreCustom || criteria.score.score - } - } - for (const criteria of Object.values(acmgRating.benign) as any) { - if (criteria.active) { - benignScore += criteria.scoreCustom || criteria.score.score - } +const calculateAcmgRating = computed((): string => { + let [acmgClass, isConflicting] = acmgRatingStore.acmgRating.getAcmgClass() + if (isConflicting) { + acmgClass = 'Uncertain significance' + updateAcmgConflicting(true) + } else { + updateAcmgConflicting(false) } - const acmgScore = pathogenicScore + benignScore - return { - pathogenicScore, - benignScore, - acmgScore + return acmgClass +}) + +const findSwitchColor = (criteria: AcmgCriteria): string => { + const evidence = acmgRatingStore.acmgRating.getCriteriaState(criteria).evidenceLevel + if (evidence === AcmgEvidenceLevel.PathogenicVeryStrong) { + return 'red-accent-4' + } else if (evidence === AcmgEvidenceLevel.PathogenicStrong) { + return 'orange-darken-4' + } else if (evidence === AcmgEvidenceLevel.PathogenicModerate) { + return 'amber-darken-4' + } else if (evidence === AcmgEvidenceLevel.PathogenicSupporting) { + return 'yellow-darken-3' + } else if (evidence === AcmgEvidenceLevel.BenignStandalone) { + return 'green-darken-4' + } else if (evidence === AcmgEvidenceLevel.BenignStrong) { + return 'light-green' + } else if (evidence === AcmgEvidenceLevel.BenignSupporting) { + return 'lime' + } else { + return 'primary' } } -const calculateAcmgRating = computed(() => { - const acmgScores = calculateAcmgScore(acmgRatingCustom.value) - const acmgScore = acmgScores.acmgScore - const pathogenicScore = acmgScores.pathogenicScore - const benignScore = acmgScores.benignScore - - const isPathogenic = acmgScore >= 10 - const isLikelyPathogenic = acmgScore >= 6 && acmgScore <= 9 - const isLikelyBenign = acmgScore >= -6 && acmgScore <= -1 - const isBenign = acmgScore <= -7 - const isConflicting = pathogenicScore >= 6 && benignScore <= -1 - - var computedClassAuto = 3 - if (isPathogenic) { - computedClassAuto = 5 - } else if (isLikelyPathogenic) { - computedClassAuto = 4 - } else if (isBenign) { - computedClassAuto = 1 - } else if (isLikelyBenign) { - computedClassAuto = 2 - } - if (isConflicting) { - computedClassAuto = 3 - updateAcmgClass(true, pathogenicScore, benignScore) +const switchCriteria = (criteria: AcmgCriteria, presence: Presence) => { + if (presence === Presence.Present) { + acmgRatingStore.acmgRating.setPresence(StateSource.User, criteria, Presence.Absent) } else { - updateAcmgClass(false, pathogenicScore, benignScore) + acmgRatingStore.acmgRating.setPresence(StateSource.User, criteria, Presence.Present) } - return ScoreExplanation[computedClassAuto as 1 | 2 | 3 | 4 | 5] -}) - -// Defining watchers +} watch( () => [props.smallVariant, acmgRatingStore.storeState], async () => { if (props.smallVariant && acmgRatingStore.storeState === StoreState.Active) { - await acmgRatingStore.retrieveAcmgRating(props.smallVariant) + await acmgRatingStore.setAcmgRating(props.smallVariant) resetAcmgRating() } } @@ -458,150 +88,29 @@ watch( onMounted(async () => { if (props.smallVariant) { - await acmgRatingStore.retrieveAcmgRating(props.smallVariant) - setAcmgRating() + await acmgRatingStore.setAcmgRating(props.smallVariant) } })