Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/content/finding-data/external-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ MaveDB displays ClinVar significance classifications and **star** status alongsi

Mapped variants in MaveDB are cross-referenced with the [gnomAD database](https://gnomad.broadinstitute.org/) to retrieve population allele frequency data. This integration provides context about the prevalence of variants in diverse human populations, which is an important factor in clinical variant interpretation alongside functional evidence.

A variant's frequency is displayed on its [variant page](../mavemd/variant-page.md), and is available in bulk through the `gnomad` namespace of the variant data download. Each frequency is matched by ClinGen allele ID, so it is a direct assertion about that variant rather than an aggregate over related variants. Variants absent from gnomAD show no frequency.

## Ensembl VEP

MaveDB uses the [Ensembl Variant Effect Predictor (VEP)](https://www.ensembl.org/info/docs/tools/vep/index.html) to annotate mapped variants with predicted functional consequences, including effects on protein coding sequences, splicing, and regulatory regions. These VEP annotations are displayed alongside variant effect scores on score set pages, providing additional context for interpreting the functional impact of each variant.
Expand Down
11 changes: 11 additions & 0 deletions docs/content/mavemd/variant-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ Each dataset on the variant page includes an [assay fact](../reference/assay-fac

For the full list of assay fact properties and their definitions, see the [assay facts reference](../reference/assay-facts.md).

## Annotations

Beneath the assay details, an annotations card gathers evidence about the variant itself, independent of any one assay. **Classification** reports the selected measurement's functional score, ACMG code, and OddsPath ratio.

**Population frequency** reports the variant's frequency in [gnomAD](../finding-data/external-integrations.md#gnomad), where it is present:

- **AF** -- The allele frequency, followed by the allele count and allele number it was computed from.
- **FAF95** -- The filtering allele frequency at 95% confidence, a conservative sampling-adjusted estimate, with the genetic ancestry group that attains it. A variant whose FAF95 exceeds a disease's maximum credible allele frequency is too common to be pathogenic (ACMG BA1/BS1).

The gnomAD release the frequencies were drawn from is shown alongside them, with a link to the variant's gnomAD page. Variants absent from gnomAD are reported as having no record rather than as having zero frequency.

## Interactive histogram

The variant page includes the same interactive score histogram shown on score set pages, but with the selected variant's position highlighted within the distribution. This visualization helps you see where the variant falls relative to all other measured variants in the assay.
Expand Down
19 changes: 16 additions & 3 deletions src/api/mavedb/score-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ type ScoreSetSearch = components['schemas']['ScoreSetsSearch']
type ScoreSetsSearchResponse = components['schemas']['ScoreSetsSearchResponse']
export type ScoreSetsSearchFilterOptionsResponse = components['schemas']['ScoreSetsSearchFilterOptionsResponse']

const HISTOGRAM_VARIANT_DATA_NAMESPACES = ['vep', 'scores', 'clingen', 'mavedb']
// Both screens read a score set's whole variant table from the same endpoint, but they render different
// things from it, so each names the namespaces it actually consumes. Keeping these separate matters at
// scale: a saturation-mutagenesis score set is 100k+ rows, and an unused namespace is 100k+ wasted cells.
const SCORE_SET_CHART_NAMESPACES = ['vep', 'scores', 'clingen', 'mavedb']

// The variant page additionally reads the selected measurement's gnomAD frequency out of its row; this
// request is the only source of it. See `gnomadFromVariantRow`.
const VARIANT_PAGE_NAMESPACES = [...SCORE_SET_CHART_NAMESPACES, 'gnomad']

function scoreSetVariantDataParams(options: {namespaces?: string[]} = {}): URLSearchParams {
const params = new URLSearchParams()
Expand All @@ -21,8 +28,14 @@ function scoreSetVariantDataUrl(urn: string, params: URLSearchParams = new URLSe
return query ? `${baseUrl}?${query}` : baseUrl
}

export function histogramScoreSetVariantDataUrl(urn: string): string {
return scoreSetVariantDataUrl(urn, scoreSetVariantDataParams({namespaces: HISTOGRAM_VARIANT_DATA_NAMESPACES}))
/** Variant data for a score set page's histogram and heatmap. */
export function scoreSetChartVariantDataUrl(urn: string): string {
return scoreSetVariantDataUrl(urn, scoreSetVariantDataParams({namespaces: SCORE_SET_CHART_NAMESPACES}))
}

/** Variant data for the variant page: the score distribution chart plus the selected row's annotations. */
export function variantPageVariantDataUrl(urn: string): string {
return scoreSetVariantDataUrl(urn, scoreSetVariantDataParams({namespaces: VARIANT_PAGE_NAMESPACES}))
}

// ---------------------------------------------------------------------------
Expand Down
7 changes: 4 additions & 3 deletions src/api/mavedb/variants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from 'axios'

import config from '@/config'
import {histogramScoreSetVariantDataUrl} from '@/api/mavedb/score-sets'
import {variantPageVariantDataUrl} from '@/api/mavedb/score-sets'
import {components} from '@/schema/openapi'

type ScoreSet = components['schemas']['ScoreSet']
Expand Down Expand Up @@ -31,8 +31,9 @@ export async function getVariantDetail(urn: string): Promise<VariantEffectMeasur
return response.data
}

export async function getHistogramVariantData(scoreSetUrn: string): Promise<string> {
const response = await axios.get(histogramScoreSetVariantDataUrl(scoreSetUrn))
/** The containing score set's variant table, as read by the variant page. */
export async function getVariantPageScoreSetData(scoreSetUrn: string): Promise<string> {
const response = await axios.get(variantPageVariantDataUrl(scoreSetUrn))
return response.data
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/screens/ScoreSetView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ import {
deleteScoreSet,
publishScoreSet,
getScoreSetClinicalControlOptions,
histogramScoreSetVariantDataUrl
scoreSetChartVariantDataUrl
} from '@/api/mavedb'
import {components} from '@/schema/openapi'
import MvLoader from '@/components/common/MvLoader.vue'
Expand Down Expand Up @@ -719,7 +719,7 @@ export default {
this.setItemId(newValue)
let scoresUrl = null
if (this.itemType?.restCollectionName && this.itemId) {
scoresUrl = histogramScoreSetVariantDataUrl(this.itemId)
scoresUrl = scoreSetChartVariantDataUrl(this.itemId)
}
this.setScoresDataUrl(scoresUrl)
this.ensureScoresDataLoaded()
Expand Down
92 changes: 63 additions & 29 deletions src/components/screens/VariantScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
:model="tableDownloadMenu"
severity="secondary"
size="small"
@click="primaryTableDownload.command()">
@click="primaryTableDownload.command()"
>
<template #default>
<i class="pi pi-table mr-1.5 text-xs" />
Download variant CSV
Expand All @@ -21,7 +22,8 @@
:model="annotationDownloadOptions"
severity="secondary"
size="small"
@click="primaryAnnotationDownload?.command()">
@click="primaryAnnotationDownload?.command()"
>
<template #default>
<i class="pi pi-download mr-1.5 text-xs" />
Download VA-Spec annotations
Expand All @@ -31,7 +33,8 @@
<span
v-if="downloadInProgress"
aria-live="polite"
class="flex items-center gap-1.5 text-xs text-text-muted">
class="flex items-center gap-1.5 text-xs text-text-muted"
>
<i class="pi pi-spin pi-spinner text-xs" />
Preparing {{ lookup.downloadInProgressLabel.value }}…
</span>
Expand Down Expand Up @@ -66,7 +69,8 @@
<MvEmptyState
v-else-if="lookup.variants.value.length === 0"
description="No variants were found for this allele."
title="No variants found" />
title="No variants found"
/>

<template v-else>
<!-- ── MEASUREMENTS SECTION ───────────────────────────── -->
Expand All @@ -84,23 +88,26 @@
active-border="var(--color-nucleotide-border)"
color="var(--color-nucleotide)"
:count="lookup.nucleotideCount.value"
label="Nucleotide level" />
label="Nucleotide level"
/>
<MvBadgeToggle
v-if="lookup.proteinCount.value > 0"
v-model="lookup.showProtein.value"
active-background="var(--color-protein-light)"
active-border="var(--color-protein-border)"
color="var(--color-protein)"
:count="lookup.proteinCount.value"
label="Protein level" />
label="Protein level"
/>
<MvBadgeToggle
v-if="lookup.associatedNucleotideCount.value > 0"
v-model="lookup.showAssociatedNucleotide.value"
active-background="var(--color-synonymous-nucleotide-light)"
active-border="var(--color-synonymous-nucleotide-border)"
color="var(--color-synonymous-nucleotide)"
:count="lookup.associatedNucleotideCount.value"
label="Synonymous nucleotide" />
label="Synonymous nucleotide"
/>
</div>
</div>
<!-- Desktop: horizontal scroll strip -->
Expand All @@ -118,7 +125,8 @@
:normal-odds-path="lookup.getNormalOddsPath(variant.content.urn)"
:study-title="variant.content.scoreSet?.title || 'Untitled score set'"
:type="variant.type"
@select="lookup.selectVariant(variant.content.urn)" />
@select="lookup.selectVariant(variant.content.urn)"
/>
</div>
<!-- Mobile: dropdown selector -->
<div class="tablet:hidden px-4 py-3">
Expand All @@ -128,56 +136,65 @@
option-label="label"
option-value="urn"
:options="measurementOptions"
@update:model-value="lookup.selectVariant($event)" />
@update:model-value="lookup.selectVariant($event)"
/>
</div>
</div>

<!-- ── VARIANT & ASSAY DETAILS ──────────────────────── -->
<template v-if="lookup.selectedVariantDetail.value">
<!-- Desktop: single card with two columns -->
<div
class="mave-gradient-bar relative mt-6 hidden tablet:block rounded-lg border border-border bg-surface px-[18px] py-3.5">
class="mave-gradient-bar relative mt-6 hidden tablet:block rounded-lg border border-border bg-surface px-[18px] py-3.5"
>
<div class="grid grid-cols-2">
<div class="border-r border-border-light pr-[18px]">
<VariantInfoSection
:allele-name="lookup.clingenAllele.alleleName.value"
:classification="lookup.calibrationResolution.classification.value"
:clingen-allele-id="lookup.selectedClingenAlleleId.value"
:clinvar-allele-ids="lookup.clingenAllele.clinvarAlleleIds.value"
:genomic-locations="lookup.clingenAllele.genomicLocations.value" />
:genomic-locations="lookup.clingenAllele.genomicLocations.value"
/>
</div>
<div class="pl-[18px]">
<MvAssayFactsCard
:columns="1"
:score-set="lookup.selectedScoreSet.value ?? undefined"
:variant-urn="lookup.selectedVariantDetail.value?.urn ?? undefined" />
:variant-urn="lookup.selectedVariantDetail.value?.urn ?? undefined"
/>
</div>
</div>
</div>

<!-- Mobile: separate cards -->
<div
class="mt-6 tablet:hidden mave-gradient-bar relative rounded-lg border border-border bg-surface px-4 py-3.5">
class="mt-6 tablet:hidden mave-gradient-bar relative rounded-lg border border-border bg-surface px-4 py-3.5"
>
<VariantInfoSection
:allele-name="lookup.clingenAllele.alleleName.value"
:classification="lookup.calibrationResolution.classification.value"
:clingen-allele-id="lookup.selectedClingenAlleleId.value"
:clinvar-allele-ids="lookup.clingenAllele.clinvarAlleleIds.value"
:genomic-locations="lookup.clingenAllele.genomicLocations.value" />
:genomic-locations="lookup.clingenAllele.genomicLocations.value"
/>
</div>
<div
class="mt-4 tablet:hidden mave-gradient-bar relative rounded-lg border border-border bg-surface px-4 py-3.5">
class="mt-4 tablet:hidden mave-gradient-bar relative rounded-lg border border-border bg-surface px-4 py-3.5"
>
<MvAssayFactsCard
:columns="1"
:score-set="lookup.selectedScoreSet.value ?? undefined"
:variant-urn="lookup.selectedVariantDetail.value?.urn ?? undefined" />
:variant-urn="lookup.selectedVariantDetail.value?.urn ?? undefined"
/>
</div>
</template>

<!-- ── ANNOTATIONS CARD ──────────────────────────────── -->
<div
v-if="lookup.selectedVariantDetail.value && lookup.selectedVariantScore.value != null"
class="mave-gradient-bar relative mt-6 rounded-lg border border-border bg-surface px-[18px] py-3.5">
class="mave-gradient-bar relative mt-6 rounded-lg border border-border bg-surface px-[18px] py-3.5"
>
<div class="annotations-columns grid grid-cols-1 tablet:grid-cols-3">
<!-- Classification -->
<div class="tablet:pr-[18px]">
Expand All @@ -188,26 +205,36 @@
lookup.selectedVariantScore.value !== 'NA'
? Number(lookup.selectedVariantScore.value).toPrecision(4)
: undefined
" />
"
/>
<MvDetailRow label="ACMG code">
<MvEvidenceTag
v-if="lookup.calibrationResolution.formattedEvidenceCode.value"
:code="lookup.calibrationResolution.formattedEvidenceCode.value" />
:code="lookup.calibrationResolution.formattedEvidenceCode.value"
/>
</MvDetailRow>
<MvDetailRow
label="OddsPath ratio"
:value="lookup.calibrationResolution.scoreRange.value?.oddspathsRatio ?? undefined" />
:value="lookup.calibrationResolution.scoreRange.value?.oddspathsRatio ?? undefined"
/>
</div>
<!-- Placeholder columns for future data -->
<!-- Population frequency: gnomAD links to a single mapped variant, so this is a direct
assertion about the measured allele rather than an aggregate over related variants.
TODO(#746) moves this to a reverse translated data model with a notion of related variants.
-->
<div
class="border-t border-border-light pt-4 tablet:border-t-0 tablet:pt-0 tablet:border-l tablet:border-border-light tablet:px-[18px]">
class="flex flex-col border-t border-border-light pt-4 tablet:border-t-0 tablet:pt-0 tablet:border-l tablet:border-border-light tablet:px-[18px]"
>
<div class="mb-1.5 text-xs-minus font-bold uppercase tracking-[0.5px] text-black">
Population Frequency
</div>
<p class="text-xs-plus italic text-text-muted">Data coming soon</p>
<MvGnomadSummary v-if="lookup.selectedVariantGnomad.value" :gnomad="lookup.selectedVariantGnomad.value" />
<p v-else class="text-xs-plus italic text-text-muted">No gnomAD record for this variant</p>
</div>
<!-- Placeholder column for future data -->
<div
class="border-t border-border-light pt-4 tablet:border-t-0 tablet:pt-0 tablet:border-l tablet:border-border-light tablet:pl-[18px]">
class="border-t border-border-light pt-4 tablet:border-t-0 tablet:pt-0 tablet:border-l tablet:border-border-light tablet:pl-[18px]"
>
<div class="mb-1.5 text-xs-minus font-bold uppercase tracking-[0.5px] text-black">
Splicing Predictions
</div>
Expand All @@ -219,15 +246,17 @@
<!-- ── SCORE DISTRIBUTION CHART ──────────────────────── -->
<div v-if="lookup.selectedScoreSet.value" class="mt-6 rounded-lg border border-border bg-surface">
<div
class="flex flex-wrap items-center justify-between gap-3 border-b border-border-light px-4 tablet:px-5 py-3.5">
class="flex flex-wrap items-center justify-between gap-3 border-b border-border-light px-4 tablet:px-5 py-3.5"
>
<div class="min-w-0">
<router-link
class="text-base tablet:text-lg font-bold text-link"
:to="{
name: 'scoreSet',
params: {urn: lookup.selectedScoreSet.value.urn},
query: {variant: lookup.selectedVariantDetail.value?.urn}
}">
}"
>
{{ lookup.selectedScoreSet.value.title }}
</router-link>
</div>
Expand All @@ -244,7 +273,8 @@
:selected-calibration="lookup.selectedCalibration.value || undefined"
:variants="lookup.scores.value"
@calibration-changed="lookup.selectedCalibration.value = $event"
@selection-changed="() => {}" />
@selection-changed="() => {}"
/>
</div>
<div v-else class="flex min-h-[200px] items-center justify-center">
<MvLoader text="Loading variant information..." />
Expand All @@ -253,7 +283,8 @@
<div v-if="lookup.selectedCalibrationObject.value" class="border-t border-border-light p-5">
<CalibrationTable
:highlighted-range-label="lookup.calibrationResolution.scoreRange.value?.label || null"
:score-calibration="lookup.selectedCalibrationObject.value" />
:score-calibration="lookup.selectedCalibrationObject.value"
/>
</div>
</div>
</template>
Expand All @@ -263,7 +294,8 @@
header="Download clinical table"
kind="variant"
:urn="lookup.selectedVariantUrn.value"
@confirm="downloadSelectedCsv" />
@confirm="downloadSelectedCsv"
/>
</MvLayout>
</template>

Expand All @@ -287,6 +319,7 @@ import MvAssayFactsCard from '@/components/common/MvAssayFactsCard.vue'
import MvCsvColumnDialog from '@/components/common/MvCsvColumnDialog.vue'
import MvBadgeToggle from '@/components/common/MvBadgeToggle.vue'
import ScoreSetHistogram from '@/components/score-set/ScoreSetHistogram.vue'
import MvGnomadSummary from '@/components/variant/MvGnomadSummary.vue'
import MvMeasurementCard from '@/components/variant/MvMeasurementCard.vue'
import MvRowActionMenu, {type RowAction} from '@/components/common/MvRowActionMenu.vue'
import VariantInfoSection from '@/components/variant/VariantInfoSection.vue'
Expand All @@ -311,6 +344,7 @@ export default defineComponent({
MvEmptyState,
MvErrorState,
MvEvidenceTag,
MvGnomadSummary,
MvLayout,
MvLoader,
MvMeasurementCard,
Expand Down
Loading
Loading