From 4130102acba7fef8eb17442ed9f2cf027699cf2e Mon Sep 17 00:00:00 2001 From: Jeremy Stone <74574922+jstone-uw@users.noreply.github.com> Date: Thu, 13 Mar 2025 05:50:43 -0700 Subject: [PATCH 1/2] Heatmaps for multi-target score sets --- src/components/ScoreSetHeatmap.vue | 481 +++++++++++++++++++++-------- src/lib/mave-hgvs.ts | 27 +- 2 files changed, 362 insertions(+), 146 deletions(-) diff --git a/src/components/ScoreSetHeatmap.vue b/src/components/ScoreSetHeatmap.vue index 74166b82..a2dcec06 100644 --- a/src/components/ScoreSetHeatmap.vue +++ b/src/components/ScoreSetHeatmap.vue @@ -1,7 +1,16 @@ @@ -28,13 +37,23 @@ import * as d3 from 'd3' import _ from 'lodash' +import Dropdown from 'primevue/dropdown' import SelectButton from 'primevue/selectbutton' +import {PropType} from 'vue' +import {saveChartAsFile} from '@/lib/chart-export' import geneticCodes from '@/lib/genetic-codes' -import makeHeatmap, {heatmapRowForNucleotideVariant, heatmapRowForProteinVariant, HEATMAP_AMINO_ACID_ROWS, HEATMAP_NUCLEOTIDE_ROWS, HeatmapDatum} from '@/lib/heatmap' -import {parseSimpleProVariant, parseSimpleNtVariant, variantNotNullOrNA} from '@/lib/mave-hgvs' -import { saveChartAsFile } from '@/lib/chart-export' -import { Heatmap } from '@/lib/heatmap' +import makeHeatmap, { + heatmapRowForNucleotideVariant, + heatmapRowForProteinVariant, + HEATMAP_AMINO_ACID_ROWS, + HEATMAP_NUCLEOTIDE_ROWS, + type Heatmap, + type HeatmapDatum, + type HeatmapRowSpecification +} from '@/lib/heatmap' +import {getVariantTarget, parseSimpleProVariant, parseSimpleNtVariant, variantNotNullOrNA} from '@/lib/mave-hgvs' +import {components} from '@/schema/openapi' function stdev(array: number[]) { if (!array || array.length === 0) { @@ -47,14 +66,59 @@ function stdev(array: number[]) { type HeatmapLayout = 'normal' | 'compact' +type ScoreSet = components['schemas']['ScoreSet'] + +/** A variant observation. */ +interface VariantObservation { + hgvs_nt?: string + hgvs_pro?: string + score?: number + [key: string]: any +} + +/** A variant observation, with its target and heatmap coordinates extracted. */ +interface HeatmapVariantObservation { + target: string + x: number + y: number + score?: number + /** The underlying observation details, excluding the score. */ + observation: Omit +} + +/** + * A variant to be plotted on the heatmap. + * + * It may represent + * - One or more observations of the same variant (wild-type or substitution) + * - Or a wild-type variant that has no observations. + */ +interface HeatmapVariant { + target: string + x: number + y: number + /** The mean score, or undefined if no observations have scores. */ + meanScore?: number + /** The scores' standard deviation, 0 if there are no scores. */ + scoreStdev?: number + /** This variant's rank among all variants at this position, sorted by score, where rank 0 has the lowest score. */ + scoreRank?: number + /** The number of observations of this variant. */ + numObservations: number + /** Observations of this variant. This may be empty for wild-type variants. */ + observations: VariantObservation[] + details: (Omit & {wt?: boolean}) +} + export default { name: 'ScoreSetHeatmap', - components: {SelectButton}, - emits: ['variantSelected', 'heatmapVisible', 'exportChart'], + components: {Dropdown, SelectButton}, + emits: ['exportChart', 'heatmapVisible', 'variantSelected'], props: { - margins: { // Margins must accommodate the axis labels - type: Object, + // Margins must accommodate the axis labels. + margins: { + type: Object as PropType<{top: number, right: number, bottom: number, left: number}>, default: () => ({ top: 0, right: 0, @@ -63,11 +127,11 @@ export default { }) }, scores: { - type: Array, + type: Array as PropType, required: true }, scoreSet: { - type: Object, + type: Object as PropType, required: true }, externalSelection: { @@ -94,61 +158,110 @@ export default { }, data: () => ({ + // Status isMounted: false, - simpleVariants: null, - numComplexVariants: 0, + + // Data + simpleVariants: {} as {[target: string]: HeatmapVariant[]}, + numComplexVariantObservations: {} as {[target: string]: number}, + + // Heatmap visualizations heatmap: null as Heatmap | null, stackedHeatmap: null as Heatmap | null, + + // User-selectable options + selectedTargetName: '', layout: 'normal' as HeatmapLayout }), computed: { - simpleAndWtVariants: function() { - return [...this.simpleVariants || [], ...this.wtVariants || []] - }, - isNucleotideHeatmap: function() { - const targetCategory = _.get(this.scoreSet, 'targetGenes[0].category') - const proteinVariantsAreDefined = this.scores.every((elem) => !isNaN(elem.hgvs_pro)) - return !proteinVariantsAreDefined && (targetCategory === 'other_noncoding' || targetCategory == "regulatory") - }, - heatmapRows: function() { - return this.isNucleotideHeatmap ? HEATMAP_NUCLEOTIDE_ROWS : HEATMAP_AMINO_ACID_ROWS + numComplexVariantsForSelectedTarget: function(): number { + return this.numComplexVariantObservations[this.selectedTargetName] || 0 + }, + simpleAndWtVariantsForSelectedTarget: function(): HeatmapVariant[] { + return [...this.simpleVariants[this.selectedTargetName] || [], ...this.wtVariantsForSelectedTarget || []] + }, + allVariantsHaveHgvsPro: function(): boolean { + return this.scores.every((variantObservation) => variantObservation.hgvs_pro != null && variantObservation.hgvs_pro != 'NA') + }, + /** + * Whether to treat the score set as nucleic-acid-based. + * + * Any score set lacking at least one hgvs_pro field is treated as nucleic-acid-based. This does not mean we will + * show a nucleotide heatmap, but it does mean that we will not show amino acid heatmaps. + */ + isNucleicAcidScoreSet: function(): boolean { + return !this.allVariantsHaveHgvsPro + }, + /** + * Whether to show a nucleotide heatmap. + * + * We show the nucleotide heatmap when the score set is nucleic-acid-based and the target category is + * `other-noncoding` or `regulatory`. + */ + isNucleotideHeatmap: function(): boolean { + const targetCategory = this.selectedTargetGene?.category + // return !this.allVariantsHaveHgvsPro && targetCategory != null && ['other_noncoding', 'regulatory'].includes(targetCategory) + return !this.allVariantsHaveHgvsPro && targetCategory != null && ['protein_coding', 'other_noncoding', 'regulatory'].includes(targetCategory) + }, + heatmapRows: function(): HeatmapRowSpecification[] { + return this.allVariantsHaveHgvsPro ? HEATMAP_AMINO_ACID_ROWS : HEATMAP_NUCLEOTIDE_ROWS }, heatmapRowForVariant: function () { - return this.isNucleotideHeatmap ? heatmapRowForNucleotideVariant : heatmapRowForProteinVariant + return this.allVariantsHaveHgvsPro ? heatmapRowForProteinVariant : heatmapRowForNucleotideVariant }, parseSimpleVariant: function () { - return this.isNucleotideHeatmap ? parseSimpleNtVariant : parseSimpleProVariant + return this.allVariantsHaveHgvsPro ? parseSimpleProVariant : parseSimpleNtVariant }, - // TODO: Swapable Targets - heatmapRange: function() { - const wtSequence = _.get(this.scoreSet, 'targetGenes[0].targetSequence.sequence') - const wtSequenceType = _.get(this.scoreSet, 'targetGenes[0].targetSequence.sequenceType') - - if (!wtSequence) { - return [] - } else if (wtSequenceType === "protein") { - return _.toArray(wtSequence) - } else if (this.isNucleotideHeatmap) { - return this.dnaToSingletons(wtSequence) - } else { - return this.translateDnaToAminoAcids1Char(wtSequence) - } + selectedTargetGene: function() { + return this.getTargetGene(this.selectedTargetName) + }, + wtSequenceForSelectedTarget: function() { + return this.getWtSequenceForTarget(this.selectedTargetName) }, - wtVariants: function() { - return this.heatmapRange ? this.prepareWtVariants(this.heatmapRange) : [] + wtVariantsForSelectedTarget: function() { + return this.wtSequenceForSelectedTarget ? + this.prepareWtVariantsForSelectedTarget(this.selectedTargetName, this.wtSequenceForSelectedTarget) : [] }, heatmapVisible: function() { - return this.simpleVariants && this.simpleVariants.length + return this.targetNameOptions.length > 0 && (!this.isNucleicAcidScoreSet || this.isNucleotideHeatmap) }, selectedVariant: function() { - return this.externalSelection ? this.simpleAndWtVariants.filter((variant) => variant.details?.accession == this.externalSelection.accession)[0] : null + return this.externalSelection ? this.simpleAndWtVariantsForSelectedTarget.filter( + (variant) => variant.details?.accession == this.externalSelection.accession + )[0] : null + }, + /** + * List of target names that can be selected in the dropdown. + * + * Target names can be obtained either from the variant MaveHGVS strings or from the target names in the score set's + * list of target genes. MaveDB's validation ensures that these are consistent, except when there is one target gene, + * in which case the MaveHGVS strings need not have a prefix (or, in some legacy score sets, may have a prefix other + * than the target gene's name). + * + * If there is only one target, '' will be used as its target name, and the dropdown will not be shown. + */ + targetNameOptions: function(): string[] { + const targetNames = this.scoreSet?.targetGenes?.map((targetGene) => targetGene.name) || [] + if (targetNames.length == 1) { + return [''] + } + return targetNames + }, + /** + * Whether to ignore reference identifiers that occur as prefixes in variant MaveHGVS strings. + * + * Reference identifiers are ignored when there is only one target, because in this case they are optional in the + * MaveHGVS strings. (And for some legacy score sets, they may be present but inconsistent with the target gene's + * name.) + */ + ignoreHgvsReferences: function(): boolean { + return this.targetNameOptions.length < 2 }, wtScore: function() { if (!this.scoreSet?.scoreRanges) { return null } - return this.scoreSet.scoreRanges.wtScore }, }, @@ -164,19 +277,23 @@ export default { scores: { handler: function() { if (!this.scores) { - this.simpleVariants = null - this.numComplexVariants = 0 + this.simpleVariants = {} + this.numComplexVariantObservations = {} + this.selectedTargetName = '' } else { - const {simpleVariants, numComplexVariants} = this.prepareSimpleVariants(this.scores) + const {simpleVariants, numComplexVariantObservations} = this.prepareVariants(this.scores) this.simpleVariants = simpleVariants - this.numComplexVariants = numComplexVariants + this.numComplexVariantObservations = numComplexVariantObservations + if (!Object.keys(this.simpleVariants).includes(this.selectedTargetName)) { + this.selectedTargetName = this.targetNameOptions[0] + } } this.renderOrRefreshHeatmaps() }, immediate: true }, - simpleAndWtVariants: { + simpleAndWtVariantsForSelectedTarget: { handler: function() { this.renderOrRefreshHeatmaps() }, @@ -227,123 +344,217 @@ export default { saveChartAsFile(this.$refs.heatmapContainer, `${this.scoreSet.urn}-scores-heatmap`, 'mave-heatmap-container') }, - // We assume that there will only be one substitution variant for each target AA at a given position. - prepareSimpleVariantScoreRanks(simpleVariants) { - _.mapValues(_.groupBy(simpleVariants, 'x'), (variantsAtOnePosition) => { - const variantsSortedByScore = _.sortBy(variantsAtOnePosition, 'meanScore') - variantsAtOnePosition.forEach((v) => v.scoreRank = variantsSortedByScore.indexOf(v)) - }) + /** + * Rank the variants at each position by score. + * + * This is necessary for the stacked heatmap, which displays variants at each position sorted by score. + */ + prepareSimpleVariantScoreRanks(simpleVariants: {[target: string]: HeatmapVariant[]}) { + for (const targetName of Object.keys(simpleVariants)) { + _.mapValues(_.groupBy(simpleVariants[targetName], 'x'), (variantsAtOnePosition) => { + const variantsSortedByScore = _.sortBy(variantsAtOnePosition, 'meanScore') + variantsAtOnePosition.forEach((v) => v.scoreRank = variantsSortedByScore.indexOf(v)) + }) + } }, - prepareWtVariants: function(heatmapRange) { - return heatmapRange.map((el, i) => el == null ? null : ({ - x: i + 1, - y: this.heatmapRows.length - 1 - this.heatmapRowForVariant(el), - details: { - wt: true + /** + * Prepare one target's wild-type variants for the heatmap. + * + * The variants returned do not have scores or observations, and they have details.wt set to true. + * + * TODO We currently assume that there are no observations of wild-type variants. + * + * @param targetName The target name, which may be an empty string in the case of a single-target score set. + * @param wtSequence The wild-type sequence for one target. + * @return An array of HeatmapVariant objects describing the wild-type variants. + */ + prepareWtVariantsForSelectedTarget: function(targetName: string, wtSequence: string[]) { + return wtSequence.map((base, i) => { + const row = base ? this.heatmapRowForVariant(base) : null + if (row == null) { + return null } - })) + return { + target: targetName, + x: i + 1, + y: this.heatmapRows.length - 1 - row, + numObservations: 0, + observations: [], + details: { + wt: true + } + } + }) .filter((x) => x != null) }, - prepareSimpleVariantInstances: function(scores) { - let numComplexVariantInstances = 0 + /** + * Prepare variant observations for the heatmap by extracting the target, x and y coordinates, and score of each + * observation. + * + * Complex variants are ignored. The number of complex variants is counted for each target. + * + * Variants are also ignored if they lie outside the target sequence's range of positions or if their alternate + * allele does not correspond to a heatmap row (which means that it is not recognized as a valid substitution). The + * number of ignored variants is counted for each target. + * + * @param variantObservations All variant observations in the score set. + * @return An object containing the simple variant observations, the number of complex variant observations, and the + * number of ignored variant observations. Each property is a map keyed by target name. + */ + prepareVariantObservations: function(variantObservations: VariantObservation[]) { + const numComplexVariantObservations: {[target: string]: number} = {} // Count of variants that do not appear to be complex but are don't have a valid substitution - let numIgnoredVariantInstances = 0 + const numIgnoredVariantObservations: {[target: string]: number} = {} - const distinctAccessions = new Set() - - let simpleVariantInstances = _.filter( - scores.map((score) => { - const vToParse = this.isNucleotideHeatmap ? score.hgvs_nt : score.hgvs_pro - const variant = this.parseSimpleVariant(vToParse) - if (!variant) { - numComplexVariantInstances++ - return null - } - if (variant.target) { - distinctAccessions.add(variant.target) - } - // Don't display variants out of range from the provided sequence. This happens occassionally - // with legacy variant data. - if (variant.position > this.heatmapRange.length) { - numIgnoredVariantInstances++ - return null - } - const row = this.heatmapRowForVariant(variant.substitution) - if (row == null) { - numIgnoredVariantInstances++ - return null - } - const x = variant.position - const y = this.heatmapRows.length - 1 - row - return {x, y, score: score.score, details: _.omit(score, 'score')} - }), - (x) => x != null + const targetSequences = _.fromPairs( + this.targetNameOptions.map( + (targetName) => [targetName, this.getWtSequenceForTarget(targetName)] + ) ) - // TODO(#237) See https://github.com/VariantEffect/mavedb-ui/issues/237. - if (distinctAccessions.size > 1) { - numComplexVariantInstances += simpleVariantInstances.length - simpleVariantInstances = [] - } - - return {simpleVariantInstances, numComplexVariantInstances, numIgnoredVariantInstances} - }, - prepareSimpleVariants: function(scores) { - const {simpleVariantInstances, numComplexVariantInstances} = this.prepareSimpleVariantInstances(scores) + const simpleVariantObservations: {[target: string]: HeatmapVariantObservation[]} = _.groupBy( + _.filter( + variantObservations.map((variantObservation) => { + // We only use the hgvs_pro field if all variants have it. Otherwise we treat this as a nucleic-acid-based + // score set. + const hgvs = this.allVariantsHaveHgvsPro ? variantObservation.hgvs_pro : variantObservation.hgvs_nt + if (!hgvs || hgvs == 'NA') { + return null + } + const variant = this.parseSimpleVariant(hgvs) + + // Get the effective target name. If there is only one target, use an empty string; otherwise get the target + // name from the HGVS string. + const targetName = (this.ignoreHgvsReferences ? '' : (variant ? variant.target : getVariantTarget(hgvs))) || '' + + if (!variant) { + numComplexVariantObservations[targetName] = (numComplexVariantObservations[targetName] || 0) + 1 + return null + } + // Don't display variants out of range from the provided sequence. This happens occassionally + // with legacy variant data. + if (variant.position > this.getWtSequenceForTarget(targetName).length) { + console.log(`WARNING: Variant out of target range: ${hgvs}`) + numIgnoredVariantObservations[targetName] = (numIgnoredVariantObservations[targetName] || 0) + 1 + return null + } + const row = this.heatmapRowForVariant(variant.substitution) + if (row == null) { + console.log(`WARNING: Unrecognized substitution in variant: ${hgvs}`) + numIgnoredVariantObservations[targetName] = (numIgnoredVariantObservations[targetName] || 0) + 1 + return null + } + const x = variant.position + const y = this.heatmapRows.length - 1 - row + return { + target: targetName, + x, + y, + score: variantObservation.score, + observation: _.omit(variantObservation, 'score') + } + }), + (variantData) => variantData != null + ), + 'target' + ) - const simpleVariants = _.flatten( - _.values( - _.mapValues( - _.groupBy(simpleVariantInstances, 'x'), - (instancesAtX) => _.values(_.groupBy(instancesAtX, 'y')) + return {simpleVariantObservations, numComplexVariantObservations, numIgnoredVariantObservations} + }, + + /*** + * Prepare the simple variants for the heatmap. + * + * @param variantObservations All variant observations in the score set. + * @return An object containing the simple variants and the number of complex variant observations. Both properties + * are maps keyed by target name. If there are no simple variants for a target, the target will not appear in the + * simple variants map. + */ + prepareVariants: function(variantObservations: VariantObservation[]) { + const {simpleVariantObservations, numComplexVariantObservations} = this.prepareVariantObservations(variantObservations) + + const simpleVariantObservationsByHeatmapPosition = _.mapValues(simpleVariantObservations, + (observations: HeatmapVariantObservation[], _target) => _.flatten( + _.values( + _.mapValues( + _.groupBy(observations, 'x'), + (observationsAtX) => _.values(_.groupBy(observationsAtX, 'y')) + ) ) ) ) - .map((v) => ({ - ..._.pick(v[0], ['x', 'y']), - instances: v - })) - for (const simpleVariant of simpleVariants) { - const scores = simpleVariant.instances.map((instance) => instance.score).filter((s) => s != null) - simpleVariant.numScores = scores.length - simpleVariant.meanScore = scores.length == 0 ? null : (scores.reduce((a, b) => a ? a:null + b ? b:null, 0) / scores.length) - simpleVariant.scoreStdev = stdev(scores) - - // Assume that aside from score, the details are identical for each instance. - simpleVariant.details = _.omit(simpleVariant.instances[0].details, 'score') - } + const simpleVariants: {[target: string]: HeatmapVariant[]} = _.mapValues(simpleVariantObservationsByHeatmapPosition, + (observationsByHeatmapPosition, _target) => observationsByHeatmapPosition.map( + (observations) => { + const scores = observations.map((observation) => observation.score).filter((score) => score != null) + return { + ..._.pick(observations[0], ['target', 'x', 'y']), + meanScore: scores.length == 0 ? undefined : (scores.reduce((a, b) => a + b, 0) / scores.length), + scoreStdev: stdev(scores), + numObservations: observations.length, + observations, + + // Assume that aside from the score, details are identical for each observation of the variant. + details: _.omit(observations[0].observation, 'score') + } + } + ) + ) this.prepareSimpleVariantScoreRanks(simpleVariants) return { simpleVariants, - // TODO Group these to identify instances of the same variant. - numComplexVariants: numComplexVariantInstances + numComplexVariantObservations } }, - translateDnaToAminoAcids1Char: function(dna) { + getTargetGene: function(targetName: string) { + if (this.ignoreHgvsReferences) { + return this.scoreSet.targetGenes[0] + } + console.log(this.scoreSet.targetGenes) + return this.scoreSet.targetGenes.find((targetGene) => targetGene.name == targetName) + }, + + getWtSequenceForTarget: function(targetName: string) { + const targetGene = this.getTargetGene(targetName) + const wtSequence = targetGene?.targetSequence?.sequence + const wtSequenceType = targetGene?.targetSequence?.sequenceType + + if (!wtSequence) { + return [] + } else if (wtSequenceType === 'protein') { + return _.toArray(wtSequence) + } else if (this.isNucleotideHeatmap) { + return this.dnaToSingletons(wtSequence) + } else { + return this.translateDnaToAminoAcids1Char(wtSequence) + } + }, + + translateDnaToAminoAcids1Char: function(dna: string | string[]): string[] { const triplets = this.dnaToTriplets(dna) return triplets.map((triplet) => this.translateCodon(triplet)) }, - dnaToTriplets: function(dna) { + dnaToTriplets: function(dna: string | string[]): string[] { if (_.isArray(dna)) { dna = dna.join('') } return _.words(dna, /.../g) }, - dnaToSingletons: function(dna) { + dnaToSingletons: function(dna: string | string[]): string[] { if (_.isArray(dna)) { dna = dna.join('') } return _.words(dna, /./g) }, - translateCodon: function(codon) { + translateCodon: function(codon: string): string | undefined { return geneticCodes.standard.dna.codonToAa[codon] }, @@ -356,10 +567,6 @@ export default { }, renderOrRefreshHeatmaps: function() { - if (!this.simpleAndWtVariants) { - return - } - this.heatmap?.destroy() this.stackedHeatmap?.destroy() @@ -388,7 +595,7 @@ export default { .skipXTicks(99) } - this.heatmap.data(this.simpleAndWtVariants) + this.heatmap.data(this.simpleAndWtVariantsForSelectedTarget) .valueField((d) => d.meanScore) .colorClassifier((variant) => variant.details.wt ? d3.color('#ddbb00') : variant.meanScore) .refresh() @@ -408,7 +615,7 @@ export default { .alignViaLegend(true) .excludeDatum((d) => d.details.wt ? true : false) - this.stackedHeatmap.data(this.simpleAndWtVariants) + this.stackedHeatmap.data(this.simpleAndWtVariantsForSelectedTarget) .valueField((d) => d.meanScore) .colorClassifier((variant) => variant.details.wt ? d3.color('#ddbb00') : variant.meanScore) .refresh() @@ -433,7 +640,7 @@ export default { parts.push(`# of observations: ${variant.numScores}`) } if (variant.numScores == 1) { - parts.push(`Score: ${variant.meanScore}`) + parts.push(`Score: ${variant.meanScore}`) } else if (variant.numScores > 1) { parts.push(`Mean score: ${variant.meanScore}`) parts.push(`Score stdev: ${variant.scoreStdev}`) diff --git a/src/lib/mave-hgvs.ts b/src/lib/mave-hgvs.ts index 90bd8090..590ee731 100644 --- a/src/lib/mave-hgvs.ts +++ b/src/lib/mave-hgvs.ts @@ -42,7 +42,18 @@ type VariantLabel = { * properly represented as Ter and del in MaveHGVS. */ const proVariantRegex = /^p\.([A-Za-z]{3})([0-9]+)([A-Za-z]{3}|=|\*|-)$/ -const ntVariantRegex = /^c|g|n\.([0-9]+)([ACGTacgt]{1})(>)([ACGTactg]{1})$/ +const ntVariantRegex = /^(c|g|n)\.([0-9]+)([ACGTacgt]{1})>([ACGTactg]{1})$/ + +/** + * Parse a MaveHGVS variant and return its target name, or null if it has no target prefix. + * + * @param variant A MaveHGVS-pro variant string representing a single variation. + * @returns The target name, or null if the MaveHGVS string has no target prefix. + */ +export function getVariantTarget(variant: string): string | null { + const parts = variant.split(":") + return parts.length == 1 ? null : parts[0] +} /** * Parse a MaveHGVS protein variant representing a variation at one locus. @@ -55,8 +66,8 @@ export function parseSimpleProVariant(variant: string): SimpleProteinVariation | const variation = parts.length == 1 ? parts[0] : parts[1] const target = parts.length == 1 ? null : parts[0] const match = variation.match(proVariantRegex) - if (!match) { - // console.log(`WARNING: Unrecognized pro variant: ${variant}`) + if (!match || !match[1] || !match[2] || !match[3]) { + console.log(`WARNING: Unrecognized pro variant: ${variant}`) return null } return { @@ -78,19 +89,18 @@ export function parseSimpleNtVariant(variant: string): SimpleProteinVariation | const variation = parts.length == 1 ? parts[0] : parts[1] const target = parts.length == 1 ? null : parts[0] const match = variation.match(ntVariantRegex) - if (!match) { - // console.log(`WARNING: Unrecognized pro variant: ${variant}`) + if (!match || !match[2] || !match[3] || !match[4]) { + console.log(`WARNING: Unrecognized nt variant: ${variant}`) return null } return { - position: parseInt(match[1]), - original: match[2], + position: parseInt(match[2]), + original: match[3], substitution: match[4], target: target } } - /** * Checks whether a provided variant is null or na * @@ -101,7 +111,6 @@ export function variantNotNullOrNA(variant: string | null | undefined): boolean return variant ? variant.toLowerCase() !== "na" : false } - /** * Return the preferred variant label for a given variant. Protein variation is preferred * to nucleotide variation, which is preferred to splice variation. From 31dbfe2dcac07a1aa291c1c9d464b6e28f9853b3 Mon Sep 17 00:00:00 2001 From: Jeremy Stone <74574922+jstone-uw@users.noreply.github.com> Date: Fri, 14 Mar 2025 08:09:57 -0700 Subject: [PATCH 2/2] Heatmap support for accession-based score sets Limitation: c. MaveHGVS strings are not yet supported, because mapping c. positions to the target gene reference is nontrivial. --- src/components/ScoreSetHeatmap.vue | 107 +++++++++++++++++++----- src/components/screens/ScoreSetView.vue | 2 +- src/lib/mave-hgvs.ts | 12 ++- 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/components/ScoreSetHeatmap.vue b/src/components/ScoreSetHeatmap.vue index a2dcec06..3ef45bfb 100644 --- a/src/components/ScoreSetHeatmap.vue +++ b/src/components/ScoreSetHeatmap.vue @@ -34,13 +34,14 @@