From 13d82a88f14e71cb48c21f928d150543aeda3e81 Mon Sep 17 00:00:00 2001 From: Benjamin Capodanno Date: Thu, 2 Jul 2026 13:01:53 -0700 Subject: [PATCH 01/84] feat(score-sets): add lean whole-set variant view channel Wire the UI up to the new GET /score-sets/{urn}/variants endpoint that serves one pre-chewed record per variant (selection key, score, consequence, annotation bridge ids, and parsed DNA/protein HGVS blocks). - add LeanVariant/HgvsField types mirrored from the OpenAPI schema - add getLeanScoreSetVariants and leanScoreSetVariantsUrl to the API client - fetch the lean view alongside the legacy CSV scoresData channel in ScoreSetView, exposing a typed leanVariantRecords handle so consumers can migrate off the CSV channel slice by slice - regenerate openapi.d.ts for the new path and schemas --- src/api/mavedb/score-sets.ts | 26 +++++- src/components/screens/ScoreSetView.vue | 20 ++++- src/lib/variants.ts | 9 ++ src/schema/openapi.d.ts | 114 ++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 4 deletions(-) diff --git a/src/api/mavedb/score-sets.ts b/src/api/mavedb/score-sets.ts index f2211a84..3b0cb554 100644 --- a/src/api/mavedb/score-sets.ts +++ b/src/api/mavedb/score-sets.ts @@ -2,6 +2,7 @@ import axios from 'axios' import config from '@/config' import {components} from '@/schema/openapi' +import type {LeanVariant} from '@/lib/variants' type ScoreSetSearch = components['schemas']['ScoreSetsSearch'] type ScoreSetsSearchResponse = components['schemas']['ScoreSetsSearchResponse'] @@ -31,6 +32,23 @@ export function histogramScoreSetVariantDataUrl(urn: string): string { ) } +// --------------------------------------------------------------------------- +// Lean whole-set variant view (GET /score-sets/{urn}/variants) +// --------------------------------------------------------------------------- + +export function leanScoreSetVariantsUrl(urn: string): string { + return `${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/variants` +} + +/** Fetch the lean whole-set variant view for a score set. */ +export async function getLeanScoreSetVariants(urn: string, signal?: AbortSignal): Promise { + const response = await axios.get(leanScoreSetVariantsUrl(urn), { + headers: {accept: 'application/json'}, + signal + }) + return response.data || [] +} + // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- @@ -103,7 +121,9 @@ export async function publishScoreSet(urn: string) { } export async function getScoreSetClinicalControlOptions(urn: string) { - const response = await axios.get(`${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/clinical-controls/options`) + const response = await axios.get( + `${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/clinical-controls/options` + ) return response.data } @@ -138,7 +158,9 @@ export async function downloadMappedVariants(urn: string) { return response.data } -export async function getRecentlyPublishedScoreSets(signal?: AbortSignal): Promise { +export async function getRecentlyPublishedScoreSets( + signal?: AbortSignal +): Promise { const response = await axios.get(`${config.apiBaseUrl}/score-sets/recently-published`, { headers: {accept: 'application/json'}, signal diff --git a/src/components/screens/ScoreSetView.vue b/src/components/screens/ScoreSetView.vue index 9cd13e8a..992d34f1 100644 --- a/src/components/screens/ScoreSetView.vue +++ b/src/components/screens/ScoreSetView.vue @@ -503,12 +503,13 @@ import config from '@/config' import {hasPathogenicityCalibrations, hasFunctionalCalibrations} from '@/lib/calibrations' import {variantNotNullOrNA} from '@/lib/mave-hgvs' import {getScoreSetShortName} from '@/lib/score-sets' -import {parseScoreSetVariantData, type Variant} from '@/lib/variants' +import {parseScoreSetVariantData, type Variant, type LeanVariant} from '@/lib/variants' import { deleteScoreSet, publishScoreSet, getScoreSetClinicalControlOptions, - histogramScoreSetVariantDataUrl + histogramScoreSetVariantDataUrl, + leanScoreSetVariantsUrl } from '@/api/mavedb' import {components} from '@/schema/openapi' import MvLoader from '@/components/common/MvLoader.vue' @@ -566,6 +567,9 @@ export default { setup(props) { const head = useHead() const scoresRemoteData = useRemoteData() + // Transitional: the lean whole-set view fetched alongside the legacy CSV `scoresData` channel. + // Consumers migrate onto `leanVariants` slice by slice; the CSV channel is removed once they have. + const leanVariantsRemoteData = useRemoteData() const variantSearchSuggestions = ref([]) const selectedCalibrations = ref<(string | null)[]>([null, null]) const urnRef = ref(props.itemId) @@ -593,6 +597,10 @@ export default { scoresDataStatus: scoresRemoteData.remoteDataStatus, setScoresDataUrl: scoresRemoteData.setDataUrl, ensureScoresDataLoaded: scoresRemoteData.ensureDataLoaded, + leanVariants: leanVariantsRemoteData.data, + leanVariantsStatus: leanVariantsRemoteData.remoteDataStatus, + setLeanVariantsUrl: leanVariantsRemoteData.setDataUrl, + ensureLeanVariantsLoaded: leanVariantsRemoteData.ensureDataLoaded, variantSearchSuggestions, distHistogramExportFn, @@ -686,6 +694,10 @@ export default { isMetaDataEmpty() { return Object.keys(this.item?.extraMetadata || {}).length === 0 }, + // Typed handle on the lean whole-set view. Consumers migrate onto this from `variants` slice by slice. + leanVariantRecords(): LeanVariant[] | null { + return (this.leanVariants as LeanVariant[] | null) ?? null + }, selectedCalibrationObjects() { if (this.item?.scoreCalibrations && this.selectedCalibrations) { return this.selectedCalibrations.map( @@ -718,11 +730,15 @@ export default { if (newValue !== oldValue) { this.setItemId(newValue) let scoresUrl = null + let leanUrl = null if (this.itemType?.restCollectionName && this.itemId) { scoresUrl = histogramScoreSetVariantDataUrl(this.itemId) + leanUrl = leanScoreSetVariantsUrl(this.itemId) } this.setScoresDataUrl(scoresUrl) this.ensureScoresDataLoaded() + this.setLeanVariantsUrl(leanUrl) + this.ensureLeanVariantsLoaded() } }, immediate: true diff --git a/src/lib/variants.ts b/src/lib/variants.ts index 1046fd77..7432d5e8 100644 --- a/src/lib/variants.ts +++ b/src/lib/variants.ts @@ -5,8 +5,17 @@ import {DEFAULT_CLNREVSTAT_FIELD, DEFAULT_CLNSIG_FIELD} from '@/lib/clinical-con import geneticCodes from '@/lib/genetic-codes' import {parseSimpleNtVariant, parseSimpleProVariant} from '@/lib/mave-hgvs' import {parseScoresOrCounts} from '@/lib/scores' +import type {components} from '@/schema/openapi' import type {SimpleDnaVariation, SimpleProteinVariation} from '@/lib/mave-hgvs' +/** + * The lean per-variant record served by `GET /score-sets/{urn}/variants`, mirrored from the API's + * OpenAPI schema. This is the target shape for the score-set view reshape; consumers migrate onto it + * slice by slice, at which point the legacy CSV-derived `Variant`/`RawVariant` types below are removed. + */ +export type HgvsField = components['schemas']['HgvsField'] +export type LeanVariant = components['schemas']['LeanVariant'] + export type HgvsReferenceSequenceType = 'c' | 'p' // | 'n' export interface SequenceRange { diff --git a/src/schema/openapi.d.ts b/src/schema/openapi.d.ts index d07f1dbc..43af2041 100644 --- a/src/schema/openapi.d.ts +++ b/src/schema/openapi.d.ts @@ -747,6 +747,20 @@ export interface paths { */ delete: operations["delete_score_set_api_v1_score_sets__urn__delete"]; }; + "/api/v1/score-sets/{urn}/variants": { + /** + * Get the lean whole-set variant view for a score set + * @description Return the lean whole-set view for a score set: one pre-chewed record per variant carrying the + * selection key (variant URN), score, a representative consequence, the bridge identifiers into the + * annotation dimensions (ClinGen allele id, assay-level digest), and the DNA + protein parsed + * position/ref/alt blocks that drive the heatmap's level toggle. + * + * The full set is returned in one payload — the score-set page bins/sorts/filters across every + * variant client-side. as_of time-travels the annotation layer only (scores are immutable); the + * resolved value is echoed in the X-As-Of response header so the content-time is a visible fact. + */ + get: operations["get_score_set_lean_variants_api_v1_score_sets__urn__variants_get"]; + }; "/api/v1/score-sets/{urn}/variants/data": { /** * Get score set variant data in CSV format @@ -3362,6 +3376,23 @@ export interface components { /** Detail */ detail?: components["schemas"]["ValidationError"][]; }; + /** + * HgvsField + * @description An HGVS expression with its parsed substitution block riding alongside when representable. + * + * ``hgvs`` is always present; ``position``/``ref``/``alt`` appear only for a placeable simple + * substitution (the heatmap grid) and are omitted for splice/indels/multivariants. + */ + HgvsField: { + /** Hgvs */ + hgvs: string; + /** Position */ + position?: number | null; + /** Ref */ + ref?: string | null; + /** Alt */ + alt?: string | null; + }; /** * JobRunDetail * @description Single-job-run detail response including the error traceback. @@ -3489,6 +3520,34 @@ export interface components { /** Description */ description?: string | null; }; + /** + * LeanVariant + * @description One pre-chewed per-variant record feeding the score-set table, heatmap, and histograms. + * + * ``variantUrn`` is the universal selection key; ``assayLevelDigest`` bridges into the digest-keyed + * annotation dimensions. The submitted HGVS (``hgvsNt``/``hgvsPro``/``hgvsSplice``, target frame) and + * the mapped ``assayLevelHgvs`` (reference frame) carry both sides of the heatmap's frame toggle, each + * with an optional parsed block for the level toggle. ``proteinLevelHgvs`` is the mapped protein + * representation (distinct from the *submitted* ``hgvsPro``); for a protein assay it coincides with + * ``assayLevelHgvs``. Fields are omitted when null. + */ + LeanVariant: { + /** Varianturn */ + variantUrn: string; + /** Score */ + score?: number | null; + /** Consequence */ + consequence?: string | null; + /** Clingenalleleid */ + clingenAlleleId?: string | null; + /** Assayleveldigest */ + assayLevelDigest?: string | null; + hgvsNt?: components["schemas"]["HgvsField"] | null; + hgvsPro?: components["schemas"]["HgvsField"] | null; + hgvsSplice?: components["schemas"]["HgvsField"] | null; + assayLevelHgvs?: components["schemas"]["HgvsField"] | null; + proteinLevelHgvs?: components["schemas"]["HgvsField"] | null; + }; /** * LengthExpression * @description A sequence expressed only by its length. @@ -10128,6 +10187,61 @@ export interface operations { }; }; }; + /** + * Get the lean whole-set variant view for a score set + * @description Return the lean whole-set view for a score set: one pre-chewed record per variant carrying the + * selection key (variant URN), score, a representative consequence, the bridge identifiers into the + * annotation dimensions (ClinGen allele id, assay-level digest), and the DNA + protein parsed + * position/ref/alt blocks that drive the heatmap's level toggle. + * + * The full set is returned in one payload — the score-set page bins/sorts/filters across every + * variant client-side. as_of time-travels the annotation layer only (scores are immutable); the + * resolved value is echoed in the X-As-Of response header so the content-time is a visible fact. + */ + get_score_set_lean_variants_api_v1_score_sets__urn__variants_get: { + parameters: { + query?: { + /** @description Reconstruct the annotation layer (mapping, allele links, VEP consequence) as it stood at this instant, over the score set's fixed scores. ISO 8601, ideally timezone-aware. This is content valid-time only — it never re-selects a score-set version. Defaults to current. */ + as_of?: string | null; + }; + header?: { + "x-active-roles"?: string | null; + }; + path: { + urn: string; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["LeanVariant"][]; + }; + }; + /** @description Authentication required. */ + 401: { + content: never; + }; + /** @description Forbidden. Insufficient permissions. */ + 403: { + content: never; + }; + /** @description Resource not found. */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + /** @description Internal server error. */ + 500: { + content: never; + }; + }; + }; /** * Get score set variant data in CSV format * @description Return tabular variant data from a score set, identified by URN, in CSV format. From 3678012c930535c2e143604d281ee62fe4461249 Mon Sep 17 00:00:00 2001 From: Benjamin Capodanno Date: Mon, 6 Jul 2026 10:38:54 -0700 Subject: [PATCH 02/84] refactor(score-set): drive visualizations from lean variant records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate the heatmap, histogram, score-set view, visualizer, and variant lookup off the legacy CSV-parsed `Variant` model onto the lean `DisplayVariant`/`LeanVariant` API record. The score-set view now fetches a single lean whole-set channel instead of the dual histogram-CSV + lean channels, and variant identity keys move from `accession` to `variantUrn` throughout. - Rework use-variant-coordinates around `coordinateFor(variant, level, frame)` as the single source of truth for the (level × frame) coordinate grid; add `resolveLevel` and `isUnmapped`, and make the (mapped, dna) cell null for protein assays (mavedb-api#784) - Handle unmapped variants: labels fall back to submitted HGVS rather than the URN, mapped/clinical mode is disabled and annotated when a set has no mapped data, and the search matches both frames - Extract VEP consequence bucketing into lib/consequences.ts, replacing the parsed-HGVS effect classifiers (`variantIsMissense`, etc.) in lib/variants.ts; `isStartOrStopLoss` stays as the block-aware heatmap filter - Extract shared d3 chart tooltip builders into lib/tooltips.ts and rebuild the heatmap/histogram tooltips on top of them - Add `isNucleotideHgvs` to lib/mave-hgvs.ts and `inferReferenceSequenceFromBlocks` to lib/variants.ts - Heatmap UX: deferred redraw with a loading indicator, a not-shown breakdown (no-coordinate vs complex), a note when the selection is off the chart, and viewport-aware hover-tooltip positioning - Add tests for consequences, mave-hgvs, and use-variant-coordinates --- .../state-management.instructions.md | 15 +- src/api/mavedb/score-sets.ts | 2 +- src/components/score-set/ScoreSetHeatmap.vue | 459 +++++++------- .../score-set/ScoreSetHistogram.vue | 577 +++++++----------- .../score-set/ScoreSetVisualizer.vue | 6 +- src/components/screens/ScoreSetView.vue | 206 ++++--- src/components/screens/VariantScreen.vue | 9 +- .../use-variant-coordinates.test.ts | 143 +++++ src/composables/use-variant-coordinates.ts | 153 +++-- src/composables/use-variant-lookup.ts | 41 +- src/lib/consequences.test.ts | 124 ++++ src/lib/consequences.ts | 160 +++++ src/lib/heatmap.ts | 34 +- src/lib/mave-hgvs.test.ts | 25 + src/lib/mave-hgvs.ts | 18 + src/lib/tooltips.ts | 97 +++ src/lib/variants.ts | 207 +++---- 17 files changed, 1374 insertions(+), 902 deletions(-) create mode 100644 src/composables/use-variant-coordinates.test.ts create mode 100644 src/lib/consequences.test.ts create mode 100644 src/lib/consequences.ts create mode 100644 src/lib/mave-hgvs.test.ts create mode 100644 src/lib/tooltips.ts diff --git a/.github/instructions/state-management.instructions.md b/.github/instructions/state-management.instructions.md index a3a7db7d..dc449738 100644 --- a/.github/instructions/state-management.instructions.md +++ b/.github/instructions/state-management.instructions.md @@ -84,12 +84,19 @@ const {downloadFile, downloadMultipleData, customDialogVisible, dataTypeOptions} ## Variant Coordinates (`src/composables/use-variant-coordinates.ts`) -Stateless utilities for resolving variant HGVS coordinates based on display mode (raw vs mapped). Shared between ScoreSetView (variant search, labels, sequence type detection) and ScoreSetHeatmap. +Stateless resolution of a lean variant's HGVS coordinate across two orthogonal axes: sequence +**level** (`dna` ↔ `protein`) and **frame** (`raw` = submitted/target numbering ↔ `mapped` = +reference numbering). `coordinateFor` is the single source of truth — every derivation (heatmap +x/y, axis availability, labels, tooltips) resolves through it, so the (level, frame) → coordinate +mapping lives in one place. The frame axis is load-bearing: `raw` and `mapped` are genuinely +different coordinate systems, so flipping frame reprojects the grid, it does not merely relabel it. +Shared between ScoreSetView (search, labels, level options) and ScoreSetHeatmap (plotting). ```ts -const {getHgvsNt, getHgvsPro, labelForVariant, sequenceTypeOptions} = useVariantCoordinates() -const nt = getHgvsNt(variant, useMapped) // resolved NT coordinate -const options = sequenceTypeOptions(variants, useMapped) // [{title: 'DNA', value: 'dna'}, ...] +const {coordinateFor, sequenceTypeOptions, resolveLevel, labelForVariant} = useVariantCoordinates() +coordinateFor(variant, 'dna', 'mapped') // → HgvsField | null (null for protein assays — no mapped coding, mavedb-api#784) +sequenceTypeOptions(variants, 'mapped') // → [{title: 'DNA', value: 'dna'}, ...] available for that frame +resolveLevel(variants, 'protein', 'mapped') // → the level to display, falling back when the desired one is stranded ``` ## Entity Cache (`src/composables/entity-cache.ts`) diff --git a/src/api/mavedb/score-sets.ts b/src/api/mavedb/score-sets.ts index 3b0cb554..6d97bb28 100644 --- a/src/api/mavedb/score-sets.ts +++ b/src/api/mavedb/score-sets.ts @@ -40,7 +40,7 @@ export function leanScoreSetVariantsUrl(urn: string): string { return `${config.apiBaseUrl}/score-sets/${encodeURIComponent(urn)}/variants` } -/** Fetch the lean whole-set variant view for a score set. */ +/** Fetch the lean whole-set variant view for a score set — one pre-chewed record per variant. */ export async function getLeanScoreSetVariants(urn: string, signal?: AbortSignal): Promise { const response = await axios.get(leanScoreSetVariantsUrl(urn), { headers: {accept: 'application/json'}, diff --git a/src/components/score-set/ScoreSetHeatmap.vue b/src/components/score-set/ScoreSetHeatmap.vue index e7eeed65..34f4aadc 100644 --- a/src/components/score-set/ScoreSetHeatmap.vue +++ b/src/components/score-set/ScoreSetHeatmap.vue @@ -1,5 +1,9 @@
+ +
+
+ MaveDB as of + + +
+ + +
+ @@ -44,8 +100,8 @@ diff --git a/src/components/variant/MvRelatedAlleles.vue b/src/components/variant/MvRelatedAlleles.vue new file mode 100644 index 00000000..d89fda72 --- /dev/null +++ b/src/components/variant/MvRelatedAlleles.vue @@ -0,0 +1,147 @@ + + + diff --git a/src/components/variant/VariantDetailPanel.vue b/src/components/variant/VariantDetailPanel.vue index fd96bcdb..ba550c7c 100644 --- a/src/components/variant/VariantDetailPanel.vue +++ b/src/components/variant/VariantDetailPanel.vue @@ -10,7 +10,10 @@
-
Could not load variant detail.
+
+ Could not load variant detail. + +