Skip to content

Commit

Permalink
fix: Display of clinvar variants in variation landscape (#118) (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
gromdimon authored Oct 16, 2023
1 parent 061e03f commit b3ab2b3
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 123 deletions.
16 changes: 16 additions & 0 deletions frontend/src/api/__tests__/dotty.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,20 @@ describe.concurrent('DottyClient', () => {

expect(result).toEqual(mockData)
})

it('should load transcripts successfully', async () => {
const mockData = {
transcripts: {
'HGNC:1100': {
gene: 'info'
}
}
}
fetchMocker.mockResponseOnce(JSON.stringify(mockData), { status: 200 })

const client = new DottyClient()
const result = await client.fetchTranscripts('HGNC:1100', 'GRCh37')

expect(result).toEqual(mockData)
})
})
15 changes: 15 additions & 0 deletions frontend/src/api/dotty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,19 @@ export class DottyClient {
return null
}
}

async fetchTranscripts(
hgnc_id: string,
assembly: 'GRCh37' | 'GRCh38' = 'GRCh38'
): Promise<any | null> {
const url = `${API_INTERNAL_BASE_PREFIX_DOTTY}/api/v1/find-transcripts?hgnc_id=${hgnc_id}&assembly=${assembly}`
const response = await fetch(url, {
method: 'GET'
})
if (response.status == 200) {
return await response.json()
} else {
return null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import VegaPlot from '@/components/VegaPlot.vue'
export interface Props {
/** Gene information from annonars. */
clinvar: any
/** Transctipts information. */
transcripts: any
/** The genome release. */
genomeRelease: string
/** The gene symbol. */
geneSymbol: string
/** The gene HGNC symbol. */
hgnc: string
}
const props = withDefaults(defineProps<Props>(), {
clinvar: null,
transcripts: null,
genomeRelease: 'grch37',
geneSymbol: ''
hgnc: ''
})
const clinvarSignificanceMapping: Record<number, number> = {
Expand Down Expand Up @@ -45,6 +48,54 @@ const convertClinvarSignificance = (input: number): number => {
}
}
const minMax = computed(() => {
if (!props.clinvar) {
return []
}
let min = null
let max = null
for (const item of props.clinvar.variants ?? []) {
if (item.genome_release.toLowerCase() == props.genomeRelease) {
// Go through all item.variants and find the min and max pos.
for (const variant of item.variants) {
if (variant.pos < min || min == null) {
min = variant.pos
}
if (variant.pos > max || max == null) {
max = variant.pos
}
}
}
}
for (const exon of exons.value) {
if (exon.start < min || min == null) {
min = exon.start
}
if (exon.stop > max || max == null) {
max = exon.stop
}
}
return [min, max]
})
const exons = computed(() => {
if (!props.transcripts) {
return []
}
const exons = []
for (const transcript of props.transcripts.transcripts) {
for (const alignment of transcript.alignments) {
for (const exon of alignment.exons) {
exons.push({
start: exon.ref_start,
stop: exon.ref_end
})
}
}
}
return exons
})
const vegaData = computed(() => {
if (!props.clinvar) {
return []
Expand Down Expand Up @@ -100,7 +151,7 @@ const vegaLayer = [
x: {
field: 'pos',
type: 'quantitative',
scale: { domain: [41190000, 41282000] },
scale: { domain: minMax.value },
axis: { grid: false, zindex: 1000 },
title: null
},
Expand Down Expand Up @@ -204,7 +255,7 @@ const vegaLayer = [
x: {
field: 'pos',
type: 'quantitative',
scale: { domain: [41190000, 41282000] },
scale: { domain: minMax.value },
axis: { grid: false },
title: null
},
Expand Down Expand Up @@ -246,13 +297,13 @@ const vegaLayer = [
},
{
description: 'gene - line',
data: { values: [{ pos: 41196312 }, { pos: 41277381 }] },
data: { values: [{ pos: minMax.value[0] }, { pos: minMax.value[1] }] },
mark: { type: 'line', stroke: 'black', size: 1, opacity: 0.5 },
encoding: {
x: {
field: 'pos',
type: 'quantitative',
scale: { domain: [41190000, 41282000] },
scale: { domain: minMax.value },
axis: { grid: false },
title: null
},
Expand All @@ -262,10 +313,7 @@ const vegaLayer = [
{
description: 'gene - exons',
data: {
values: [
{ start: 41196312, stop: 41197819 },
{ start: 41199660, stop: 41199720 }
]
values: exons.value
},
mark: {
type: 'rect',
Expand All @@ -278,7 +326,7 @@ const vegaLayer = [
x: {
field: 'start',
type: 'quantitative',
scale: { domain: [41190000, 41282000] },
scale: { domain: minMax.value },
axis: { grid: false },
title: null
},
Expand All @@ -292,7 +340,7 @@ const vegaLayer = [
<template>
<figure class="figure border rounded pl-2 pt-2 mr-3 w-100 col">
<figcaption class="figure-caption text-center">
Variation Landscape of {{ props.geneSymbol }}
Variation Landscape of {{ props.hgnc }}
</figcaption>
<div style="width: 1100px; height: 350px; overflow: none">
<VegaPlot
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'

import * as BRCA1Clinvar from '@/assets/__tests__/BRCA1ClinVar.json'
import VariationLandscape from '@/components/VariantDetails/VariationLandscape.vue'
import VariationLandscape from '@/components/VariationLandscape.vue'
import { setupMountedComponents } from '@/lib/test-utils'

describe.concurrent('VariationLandscape', async () => {
Expand Down
25 changes: 19 additions & 6 deletions frontend/src/stores/__tests__/geneInfo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ describe.concurrent('geneInfo Store', () => {
expect(store.storeState).toBe(StoreState.Initial)
expect(store.geneSymbol).toBe(null)
expect(store.geneInfo).toBe(null)
expect(store.geneClinvar).toBe(null)
expect(store.transcripts).toBe(null)
})

it('should clear state', () => {
Expand All @@ -33,6 +35,8 @@ describe.concurrent('geneInfo Store', () => {
expect(store.storeState).toBe(StoreState.Initial)
expect(store.geneSymbol).toBe(null)
expect(store.geneInfo).toBe(null)
expect(store.geneClinvar).toBe(null)
expect(store.transcripts).toBe(null)
})

it('should load data', async () => {
Expand All @@ -42,16 +46,19 @@ describe.concurrent('geneInfo Store', () => {
return Promise.resolve(JSON.stringify({ genes: { 'HGNC:1100': { gene: 'info' } } }))
} else if (req.url.includes('clinvar')) {
return Promise.resolve(JSON.stringify({ genes: { 'HGNC:1100': { gene: 'info' } } }))
} else if (req.url.includes('find-transcripts')) {
return Promise.resolve(JSON.stringify({ transcripts: { 'HGNC:1100': { gene: 'info' } } }))
} else {
return Promise.resolve(JSON.stringify({ status: 400 }))
}
})
await store.loadData('HGNC:1100')
await store.loadData('HGNC:1100', 'GRCh37')

expect(store.storeState).toBe(StoreState.Active)
expect(store.geneSymbol).toBe('HGNC:1100')
expect(store.geneInfo).toEqual({ gene: 'info' })
expect(store.geneClinvar).toEqual({ gene: 'info' })
expect(store.transcripts).toEqual({ transcripts: { 'HGNC:1100': { gene: 'info' } } })
})

it('should fail to load data with invalid request to gene info', async () => {
Expand All @@ -63,16 +70,19 @@ describe.concurrent('geneInfo Store', () => {
return Promise.resolve(JSON.stringify({ foo: 'bar' }))
} else if (req.url.includes('clinvar')) {
return Promise.resolve(JSON.stringify({ genes: { 'HGNC:1100': { gene: 'info' } } }))
} else if (req.url.includes('find-transcripts')) {
return Promise.resolve(JSON.stringify({ transcripts: { 'HGNC:1100': { gene: 'info' } } }))
} else {
return Promise.resolve(JSON.stringify({ status: 400 }))
}
})
await store.loadData('invalid')
await store.loadData('invalid', 'invalid')

expect(store.storeState).toBe(StoreState.Error)
expect(store.geneSymbol).toBe(null)
expect(store.geneInfo).toBe(null)
expect(store.geneClinvar).toBe(null)
expect(store.transcripts).toBe(null)
})

it('should fail to load data with invalid request to clinvar info', async () => {
Expand All @@ -84,32 +94,35 @@ describe.concurrent('geneInfo Store', () => {
return Promise.resolve(JSON.stringify({ genes: { 'HGNC:1100': { gene: 'info' } } }))
} else if (req.url.includes('clinvar')) {
return Promise.resolve(JSON.stringify({ foo: 'bar' }))
} else if (req.url.includes('find-transcripts')) {
return Promise.resolve(JSON.stringify({ transcripts: { 'HGNC:1100': { gene: 'info' } } }))
} else {
return Promise.resolve(JSON.stringify({ status: 400 }))
}
})
await store.loadData('invalid')
await store.loadData('invalid', 'invalid')

expect(store.storeState).toBe(StoreState.Error)
expect(store.geneSymbol).toBe(null)
expect(store.geneInfo).toBe(null)
expect(store.geneClinvar).toBe(null)
expect(store.transcripts).toBe(null)
})

it('should not load data if gene symbol is the same', async () => {
const store = useGeneInfoStore()
fetchMocker.mockResponse(JSON.stringify({ genes: { 'HGNC:1100': { gene: 'info' } } }))

await store.loadData('HGNC:1100')
await store.loadData('HGNC:1100', 'GRCh37')

expect(store.storeState).toBe(StoreState.Active)
expect(store.geneSymbol).toBe('HGNC:1100')
expect(store.geneInfo).toEqual({ gene: 'info' })
expect(store.geneClinvar).toEqual({ gene: 'info' })
expect(store.geneSymbol).toBe('HGNC:1100')

await store.loadData('HGNC:1100')
await store.loadData('HGNC:1100', 'GRCh37')

expect(fetchMocker.mock.calls.length).toBe(2)
expect(fetchMocker.mock.calls.length).toBe(3)
})
})
15 changes: 14 additions & 1 deletion frontend/src/stores/geneInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'

import { AnnonarsClient } from '@/api/annonars'
import { DottyClient } from '@/api/dotty'
import { StoreState } from '@/stores/misc'

export const useGeneInfoStore = defineStore('geneInfo', () => {
Expand All @@ -22,14 +23,18 @@ export const useGeneInfoStore = defineStore('geneInfo', () => {
/** ClinVar gene-related information from annoars. */
const geneClinvar = ref<any | null>(null)

/** Transcript information from dotty. */
const transcripts = ref<any | null>(null)

function clearData() {
storeState.value = StoreState.Initial
geneSymbol.value = null
geneInfo.value = null
geneClinvar.value = null
transcripts.value = null
}

const loadData = async (geneSymbolQuery: string) => {
const loadData = async (geneSymbolQuery: string, genomeRelease: string) => {
// Do not re-load data if the gene symbol is the same
if (geneSymbolQuery === geneSymbol.value) {
return
Expand All @@ -55,6 +60,13 @@ export const useGeneInfoStore = defineStore('geneInfo', () => {
}
geneClinvar.value = geneClinvarData['genes'][hgncId]

const dottyClient = new DottyClient()
const transcriptsData = await dottyClient.fetchTranscripts(
hgncId,
genomeRelease === 'grch37' ? 'GRCh37' : 'GRCh38'
)
transcripts.value = transcriptsData

geneSymbol.value = geneSymbolQuery
storeState.value = StoreState.Active
} catch (e) {
Expand All @@ -69,6 +81,7 @@ export const useGeneInfoStore = defineStore('geneInfo', () => {
geneSymbol,
geneInfo,
geneClinvar,
transcripts,
loadData,
clearData
}
Expand Down
Loading

0 comments on commit b3ab2b3

Please sign in to comment.