diff --git a/src/components/common/MvPublicationsSection.vue b/src/components/common/MvPublicationsSection.vue index 32480c9a..21a37fb3 100644 --- a/src/components/common/MvPublicationsSection.vue +++ b/src/components/common/MvPublicationsSection.vue @@ -9,9 +9,16 @@ {{ pub.identifier }} - @@ -758,7 +815,7 @@ import { } from '@/lib/mavemd' import {getTargetGeneName} from '@/lib/target-genes' import {components} from '@/schema/openapi' -import {getScoreSetShortName} from '@/lib/score-sets' +import {getScoreSetShortName, getPublicationUrl} from '@/lib/score-sets' import {type GenomeAssembly, GENOME_ASSEMBLY_NAMES, gnomadIdToHgvs, otherAssembly} from '@/lib/gnomad' import {clinVarHgvsSearchStringRegex, hgvsSearchStringRegex} from '@/lib/mave-hgvs' import {SEARCH_COLORS, SEARCH_PLACEHOLDERS} from '@/data/search' @@ -786,6 +843,22 @@ import MvLoader from '@/components/common/MvLoader.vue' const SCORE_SETS_TO_SHOW = 5 +/** + * Legend for the calibration-status column header, rendered as HTML so the states read as a scannable bullet list. + * Passed to the tooltip with `escape: false`; the markup is static, so there is no injection surface. + */ +const CALIBRATION_STATUS_LEGEND_HTML = ` +
+
ACMG/AMP calibration status
+ +
+` + type ScoreSet = components['schemas']['ScoreSet'] type TargetGene = components['schemas']['TargetGene'] @@ -810,7 +883,17 @@ export default defineComponent({ const router = useRouter() const toast = useToast() const {getEntity} = useEntityCache() - return {route, router, toast, getEntity, getScoreSetShortName, scoreSetUrnFromVariantUrn, AVE_CLINICAL_APPLICATION} + return { + route, + router, + toast, + getEntity, + getScoreSetShortName, + getPublicationUrl, + scoreSetUrnFromVariantUrn, + AVE_CLINICAL_APPLICATION, + CALIBRATION_STATUS_LEGEND_HTML + } }, data: function () { @@ -847,6 +930,7 @@ export default defineComponent({ associatedNucleotideScoreSetListIsExpanded: [] as Array, defaultNumScoreSetsToShow: SCORE_SETS_TO_SHOW, guideExpanded: false, + filterGene: '', maveMdScoreSetUrns: [] as string[], maveMdScoreSets: {} as {[urn: string]: ScoreSet | undefined}, maveMdScoreSetsError: false, @@ -855,8 +939,9 @@ export default defineComponent({ }, computed: { - maveMdScoreSetsGroupedByGene: function () { - const groups = _(this.maveMdScoreSetUrns) + /** All score sets grouped by gene name, sorted alphabetically — the unfiltered, unsliced source list. */ + allScoreSetsGroupedByGene: function (): Array<{gene: string; urns: string[]}> { + return _(this.maveMdScoreSetUrns) .groupBy((urn) => { const scoreSet = this.maveMdScoreSets[urn] if (!scoreSet) return 'Unknown' @@ -866,7 +951,14 @@ export default defineComponent({ .map(([gene, urns]) => ({gene, urns})) .sortBy(({gene}) => gene.toLowerCase()) .value() - return this.guideExpanded ? groups : groups.slice(0, 8) + }, + maveMdScoreSetsGroupedByGene: function (): Array<{gene: string; urns: string[]}> { + // A gene filter searches the whole collection, so it bypasses the eight-gene preview and returns every match. + const filter = this.filterGene.trim().toLowerCase() + if (filter) { + return this.allScoreSetsGroupedByGene.filter(({gene}) => gene.toLowerCase().includes(filter)) + } + return this.guideExpanded ? this.allScoreSetsGroupedByGene : this.allScoreSetsGroupedByGene.slice(0, 8) }, searchIsClearable: function () { return ( @@ -1616,24 +1708,100 @@ export default defineComponent({ } }, - calibrationCountWithEvidence(urn: string): number { - const scoreSet = this.maveMdScoreSets[urn] - if (!scoreSet?.scoreCalibrations) return 0 - return scoreSet.scoreCalibrations.filter( - (calibration: components['schemas']['ScoreCalibration']) => - Array.isArray(calibration.functionalClassifications) && - calibration.functionalClassifications.filter((range) => range.acmgClassification).length > 0 - ).length + /** A calibration carries evidence when at least one of its functional classifications assigns an ACMG strength. */ + calibrationHasEvidence(calibration: components['schemas']['ScoreCalibration']): boolean { + return ( + Array.isArray(calibration.functionalClassifications) && + calibration.functionalClassifications.some((range) => range.acmgClassification) + ) + }, + + /** + * Derive a labeled ACMG calibration status for a score set, replacing the opaque "with-evidence / total" badge. + * + * A single calibration that assigns ACMG evidence strengths is enough to call a score set calibrated — the status + * does not require every calibration to carry evidence. The one qualification is research-use-only (RUO): when the + * only evidence-bearing calibrations are RUO, the score set is flagged as such rather than shown as clinically + * calibrated, since RUO calibrations are not intended for clinical interpretation. + */ + calibrationStatus(urn: string): {label: string; badgeClass: string; tooltip: string} { + const calibrations = this.maveMdScoreSets[urn]?.scoreCalibrations ?? [] + const withEvidence = calibrations.filter((c) => this.calibrationHasEvidence(c)) + const clinicalEvidence = withEvidence.filter((c) => !c.researchUseOnly) + const muted = 'border-gray-200 bg-gray-50 text-gray-500' + + if (calibrations.length === 0) { + return { + label: 'None', + badgeClass: muted, + tooltip: 'This score set has no clinical evidence calibrations.' + } + } + if (withEvidence.length === 0) { + return { + label: 'Uncalibrated', + badgeClass: muted, + tooltip: 'Calibrations exist but none assign ACMG/AMP evidence strengths.' + } + } + if (clinicalEvidence.length === 0) { + return { + label: 'Research use only', + badgeClass: 'border-orange-border bg-orange-light text-orange-cta-dark', + tooltip: + 'The only calibrations assigning ACMG/AMP evidence strengths are marked research-use-only, so they are not intended for clinical variant interpretation.' + } + } + return { + label: 'Calibrated', + badgeClass: 'border-published-dot bg-published-light text-published', + tooltip: 'At least one calibration assigns ACMG/AMP evidence strengths for clinical variant interpretation.' + } }, - calibrationCountTotal(urn: string): number { - return this.maveMdScoreSets[urn]?.scoreCalibrations?.length || 0 + /** Format a variant count with a thousands separator, or an em dash when the count is unavailable. */ + formatVariantCount(scoreSet: ScoreSet | undefined): string { + return typeof scoreSet?.numVariants === 'number' ? scoreSet.numVariants.toLocaleString() : '—' } } })