Skip to content

Commit

Permalink
feat: Retrieval of affected genes in SV from mehari
Browse files Browse the repository at this point in the history
  • Loading branch information
gromdimon committed Oct 4, 2023
1 parent 2ce7de2 commit c6b72f8
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 156 deletions.
13 changes: 10 additions & 3 deletions frontend/src/api/__tests__/mehari.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ describe.concurrent('Mehari Client', () => {
fetchMocker.mockResponseOnce(JSON.stringify(BRCA1TxInfo))

const client = new MehariClient()
const result = await client.retrieveTxCsq('grch37', 'chr17', 43044295, 'A', 'G', 'HGNC:1100')
const result = await client.retrieveSeqvarsCsq(
'grch37',
'chr17',
43044295,
'A',
'G',
'HGNC:1100'
)
expect(JSON.stringify(result)).toEqual(JSON.stringify(BRCA1TxInfo))
})

it('fetches TxCsq info correctly without HGNC id', async () => {
fetchMocker.mockResponseOnce(JSON.stringify(BRCA1TxInfo))

const client = new MehariClient()
const result = await client.retrieveTxCsq('grch37', 'chr17', 43044295, 'A', 'G')
const result = await client.retrieveSeqvarsCsq('grch37', 'chr17', 43044295, 'A', 'G')
expect(JSON.stringify(result)).toEqual(JSON.stringify(BRCA1TxInfo))
})

Expand All @@ -37,7 +44,7 @@ describe.concurrent('Mehari Client', () => {
})

const client = new MehariClient()
const result = await client.retrieveTxCsq('grch37', 'chr17', 43044295, 'A', 'T')
const result = await client.retrieveSeqvarsCsq('grch37', 'chr17', 43044295, 'A', 'T')
expect(JSON.stringify(result)).toEqual(JSON.stringify({ status: 400 }))
})
})
18 changes: 17 additions & 1 deletion frontend/src/api/mehari.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class MehariClient {
this.csrfToken = csrfToken ?? null
}

async retrieveTxCsq(
async retrieveSeqvarsCsq(
genomeRelease: string,
chromosome: string,
pos: number,
Expand All @@ -30,4 +30,20 @@ export class MehariClient {
})
return await response.json()
}

async retrieveStrucvarsCsq(
genomeRelease: string,
chromosome: string,
start: number,
stop: number,
sv_type: string
): Promise<any> {
const url =
`${this.apiBaseUrl}strucvars/csq?genome_release=${genomeRelease}&` +
`chromosome=${chromosome}&start=${start}&stop=${stop}&sv_type=${sv_type}`
const response = await fetch(url, {
method: 'GET'
})
return await response.json()
}
}
1 change: 1 addition & 0 deletions frontend/src/components/SvDetails/SvDetailsClinvar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const vcvUrl = (vcv: string): string => {

<template>
<div>
{{ svInfoStore.currentSvRecord }}
<template v-if="svInfoStore.currentSvRecord?.payload?.clinvar_ovl_vcvs?.length">
<p>The following overlapping SVs are flagged as (likely) pathogenic in ClinVar.</p>
Expand Down
193 changes: 63 additions & 130 deletions frontend/src/components/SvDetails/SvGenes.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
<script setup lang="ts">
import { type ComputedRef, type Ref, computed, ref } from 'vue'
import EasyDataTable from 'vue3-easy-data-table'
import type { ClickRowArgument, Header } from 'vue3-easy-data-table'
import 'vue3-easy-data-table/dist/style.css'
import VariantDetailsGene from '@/components/VariantDetails/VariantGene.vue'
import { roundIt } from '@/lib/utils'
import { useSvInfoStore } from '@/stores/svInfo'
/** `GeneInfo` is a type alias for easier future interface definition. */
type GeneInfo = any
Expand All @@ -16,136 +12,77 @@ const props = defineProps<{
}>()
const currentGeneInfos: Ref<any> = ref(null)
const itemsPerPage = ref(10)
const headers: Header[] = [
const headers = [
{
text: 'symbol',
value: 'dbnsfp.gene_name',
title: 'symbol',
key: 'dbnsfp.gene_name',
width: 150,
sortable: true
},
{
text: 'name',
value: 'dbnsfp.gene_full_name',
title: 'name',
key: 'dbnsfp.gene_full_name',
width: 200
},
{
text: 'OMIM',
value: 'omim',
title: 'OMIM',
key: 'omim',
sortable: true
},
{
text: 'Orphanet',
value: 'orpha',
title: 'Orphanet',
key: 'orpha',
sortable: true
},
{
text: 'pLI',
value: 'gnomad_constraints.pli',
title: 'pLI',
key: 'gnomad_constraints.pli',
width: 50,
sortable: true
},
{
text: 'o/e LoF (upper)',
value: 'gnomad_constraints.oe_lof_upper',
title: 'o/e LoF (upper)',
key: 'gnomad_constraints.oe_lof_upper',
width: 100,
sortable: true
},
{
text: 'P(HI)',
title: 'P(HI)',
width: 50,
value: 'dbnsfp.haploinsufficiency'
key: 'dbnsfp.haploinsufficiency'
},
{
text: 'sHet',
title: 'sHet',
width: 100,
value: 'shet.s_het',
key: 'shet.s_het',
sortable: true
},
{
text: 'pHaplo',
title: 'pHaplo',
width: 100,
value: 'rcnv.p_haplo',
key: 'rcnv.p_haplo',
sortable: true
},
{
text: 'pTriplo',
title: 'pTriplo',
width: 100,
value: 'rcnv.p_triplo',
key: 'rcnv.p_triplo',
sortable: true
},
{
text: 'CG haploin.',
title: 'CG haploin.',
width: 100,
value: 'clingen.haplo_summary'
key: 'clingen.haplo_summary'
},
{
text: 'CG triploin.',
title: 'CG triploin.',
width: 100,
value: 'clingen.triplo_summary'
key: 'clingen.triplo_summary'
}
]
/** The SV details store. */
const svInfoStore = useSvInfoStore()
/** Helper type for `resultsInfos`. */
type ResultsInfo = {
isDiseaseGene: boolean
txEffect?: string
}
/**
* Compute mapping from HGNC gene ID to transcript effect and disease gene property
* so we can display the same information as in the SV results table.
*/
const resultsInfos: ComputedRef<Map<string, ResultsInfo>> = computed(() => {
const result = new Map()
for (const txEffect of svInfoStore.currentSvRecord?.payload?.tx_effects ?? []) {
const value: ResultsInfo = {
isDiseaseGene: txEffect.gene.is_disease_gene,
txEffect: txEffect.transcript_effects[0]
}
result.set(txEffect.gene.hgnc_id, value)
}
return result
})
/** Compute geneInfo's class. */
const geneInfoClass = (geneInfo: any): string | null => {
let resultsInfo = resultsInfos.value.get(geneInfo.hgnc.hgnc_id)
if (resultsInfo?.isDiseaseGene) {
return 'text-danger'
} else {
return null
}
}
/** Compute geneInfo's badge HTML (if any). */
const geneInfoBadge = (geneInfo: any): string | null => {
const badge = (color: string, title: string, text: string): string => {
return `<span class="badge badge-${color}" title="${title}">${text}</span>&nbsp;`
}
let resultsInfo = resultsInfos.value.get(geneInfo.hgnc.hgnc_id)
switch (resultsInfo?.txEffect) {
case 'transcript_variant':
return badge('danger', 'whole transcript is affected', 'tx')
case 'exon_variant':
return badge('danger', 'exonic for gene', 'ex')
case 'splice_region_variant':
return badge('danger', 'affects splice region for gene', 'sr')
case 'intron_variant':
return badge('warning', 'intronic for gene', 'in')
case 'upstream_variant':
return badge('secondary', 'upstream of gene', 'up')
case 'downstream_variant':
return badge('secondary', 'downstream of gene gene', 'dw')
default:
return ''
}
}
/** Compute list of gene infos to protect against empty `props.genesInfos`. */
const items: ComputedRef<GeneInfo[]> = computed(() => {
if (props.genesInfos) {
Expand Down Expand Up @@ -188,32 +125,28 @@ const items: ComputedRef<GeneInfo[]> = computed(() => {
}
})
const onRowClicked = (item: ClickRowArgument) => {
/** Show gene info on click. */
const onRowClicked = (event: Event, { item }: { item: GeneInfo }): void => {
currentGeneInfos.value = item
}
</script>

<template>
<div>
<div>
<EasyDataTable
<v-data-table
v-model:items-per-page="itemsPerPage"
:headers="headers"
:items="items"
:loading="!items"
buttons-pagination
show-index
@click-row="onRowClicked"
item-key="gene_name"
@click:row="onRowClicked"
>
<template v-slot:[`item-dbnsfp.gene_name`]="geneInfo">
<span v-html="geneInfoBadge(geneInfo)" />
<span :class="geneInfoClass(geneInfo)">
{{ geneInfo.dbnsfp?.gene_name }}
</span>
</template>

<template v-slot:[`item-omim`]="{ omim }">
<template v-if="omim?.omim_diseases?.length">
<template v-for="(disease, idx) in omim?.omim_diseases" :key="idx">
<template v-slot:[`item.omim`]="{ value }">
<template v-if="value?.omim_diseases?.length">
<template v-for="(disease, idx) in value?.omim_diseases" :key="idx">
<template v-if="idx > 0">, </template>
<a
:href="`https://www.omim.org/entry/${disease.omim_id.replace('OMIM:', '')}`"
Expand All @@ -226,9 +159,9 @@ const onRowClicked = (item: ClickRowArgument) => {
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-orpha`]="{ orpha }">
<template v-if="orpha?.orpha_diseases?.length">
<template v-for="(disease, idx) in orpha?.orpha_diseases" :key="idx">
<template v-slot:[`item.orpha`]="{ value }">
<template v-if="value?.orpha_diseases?.length">
<template v-for="(disease, idx) in value?.orpha_diseases" :key="idx">
<template v-if="idx > 0">, </template>
<a
:href="`https://www.orpha.net/consor/cgi-bin/OC_Exp.php?Expert=${disease.orpha_id.replace(
Expand All @@ -244,62 +177,62 @@ const onRowClicked = (item: ClickRowArgument) => {
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-gnomad_constraints.pli`]="{ gnomad_constraints }">
<template v-if="gnomad_constraints">
<span v-html="roundIt(gnomad_constraints.pli, 3)" />
<template v-slot:[`item.gnomad_constraints.pli`]="{ value }">
<template v-if="value">
<span v-html="roundIt(value, 3)" />
</template>
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-gnomad_constraints.oe_lof_upper`]="{ gnomad_constraints }">
<template v-if="gnomad_constraints">
<span v-html="roundIt(gnomad_constraints.oe_lof_upper, 3)" />
<template v-slot:[`item.gnomad_constraints.oe_lof_upper`]="{ value }">
<template v-if="value">
<span v-html="roundIt(value, 3)" />
</template>
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-dbnsfp.haploinsufficiency`]="{ dbnsfp }">
<template v-if="dbnsfp?.haploinsufficiency">
<span v-html="roundIt(dbnsfp.haploinsufficiency, 3)" />
<template v-slot:[`item.dbnsfp.haploinsufficiency`]="{ value }">
<template v-if="value">
<span v-html="roundIt(value, 3)" />
</template>
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-shet.s_het`]="{ shet }">
<template v-if="shet?.s_het">
<span v-html="roundIt(shet.s_het, 3)" />
<template v-slot:[`item.shet.s_het`]="{ value }">
<template v-if="value">
<span v-html="roundIt(value, 3)" />
</template>
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-rcnv.p_haplo`]="{ rcnv }">
<template v-if="rcnv?.p_haplo">
<span v-html="roundIt(rcnv.p_haplo, 3)" />
<template v-slot:[`item.rcnv.p_haplo`]="{ value }">
<template v-if="value">
<span v-html="roundIt(value, 3)"></span>
</template>
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-rcnv.p_triplo`]="{ rcnv }">
<template v-if="rcnv?.p_triplo">
<span v-html="roundIt(rcnv.p_triplo, 3)" />
<template v-slot:[`item.rcnv.p_triplo`]="{ value }">
<template v-if="value">
<span v-html="roundIt(value, 3)" />
</template>
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-clingen.haplo_summary`]="{ clingen }">
<template v-if="clingen?.haplo_summary">
<abbr :title="clingen.haplo_label">{{ clingen.haplo_summary }}</abbr>
<template v-slot:[`item.clingen.haplo_summary`]="{ value }">
<template v-if="value">
<abbr :title="value">{{ value }}</abbr>
</template>
<template v-else> &mdash; </template>
</template>
<template v-slot:[`item-clingen.triplo_summary`]="{ clingen }">
<template v-if="clingen?.triplo_summary">
<abbr :title="clingen.triplo_label">{{ clingen.triplo_summary }}</abbr>
<template v-slot:[`item.clingen.triplo_summary`]="{ value }">
<template v-if="value">
<abbr :title="value">{{ value }}</abbr>
</template>
<template v-else> &mdash; </template>
</template>
</EasyDataTable>
</v-data-table>
</div>
<div v-if="currentGeneInfos">
Expand Down
Loading

0 comments on commit c6b72f8

Please sign in to comment.