From 26f5ccb4c19bed7790a5ef7de39378f73c157948 Mon Sep 17 00:00:00 2001 From: Phil Darnowsky Date: Mon, 31 Jul 2023 10:20:26 -0400 Subject: [PATCH] Catch variants in ClinvarAllVariantsPlot that run past 3' UTR In some cases, such as one variant on BRCA1, the data from VEP gives us an HGVSP for a variant that runs past the end of the 3' UTR as specified by Gencode. These were causing the plot of Clinvar variants to crash. Here we fix that, along with an assortment of off-by-one errors and edge cases. --- .../ClinvarAllVariantsPlot.spec.tsx | 294 +++++ .../ClinvarAllVariantsPlot.tsx | 471 ++++--- .../ClinvarAllVariantsPlot.spec.tsx.snap | 1155 +++++++++++++++++ browser/src/TranscriptPage/TranscriptPage.tsx | 12 +- browser/src/__factories__/ClinvarVariant.ts | 43 + 5 files changed, 1787 insertions(+), 188 deletions(-) create mode 100644 browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.spec.tsx create mode 100644 browser/src/ClinvarVariantsTrack/__snapshots__/ClinvarAllVariantsPlot.spec.tsx.snap create mode 100644 browser/src/__factories__/ClinvarVariant.ts diff --git a/browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.spec.tsx b/browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.spec.tsx new file mode 100644 index 000000000..871d5353a --- /dev/null +++ b/browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.spec.tsx @@ -0,0 +1,294 @@ +import React from 'react' +import { test, describe, expect } from '@jest/globals' +import ClinvarAllVariantsPlot from './ClinvarAllVariantsPlot' +import clinvarVariantFactory from '../__factories__/ClinvarVariant' +import transcriptFactory from '../__factories__/Transcript' +import { render } from '@testing-library/react' +import { Strand } from '../GenePage/GenePage' + +const extractPlotFragment = (tree: DocumentFragment) => tree.querySelector('svg >g:nth-child(2)') + +type PlotLayoutElement = + | ['overlay', number, number] + | ['cds', number, number] + | ['utr', number, number] + | ['stop_marker', number] + +const plotLayoutMatches = (expected: PlotLayoutElement[], plotBody: Element | null) => { + if (plotBody === null) { + throw new Error('plot body missing') + } + + const children = Array.from(plotBody.querySelector('g')!.querySelectorAll('*').values()) + expected.forEach((expectedElement, i) => { + if (i >= children.length) { + throw new Error(`ran out of children while looking for ${expectedElement}`) + } + + const child = children[i] + const elementType = expectedElement[0] + const expectedTag = { + overlay: 'rect', + cds: 'line', + utr: 'line', + stop_marker: 'path', + }[elementType] + + if (child.tagName !== expectedTag) { + throw new Error( + `expected tag "${expectedTag}" for element ${expectedElement} but got "${child.tagName}"` + ) + } + + if (elementType === 'overlay') { + const [, start, width] = expectedElement + const actualStart = child.getAttribute('x') + if (actualStart !== start.toString()) { + throw new Error(`wrong start (${actualStart}) for ${expectedElement}`) + } + + const actualWidth = child.getAttribute('width') + if (actualWidth !== width.toString()) { + throw new Error(`wrong width (${actualWidth}) for ${expectedElement}`) + } + } + + if (elementType === 'cds' || elementType === 'utr') { + const [_, start, stop] = expectedElement + + const actualStart = child.getAttribute('x1') + if (actualStart !== start.toString()) { + throw new Error(`wrong start (${actualStart}) for ${expectedElement}`) + } + + const actualStop = child.getAttribute('x2') + if (actualStop !== stop.toString()) { + throw new Error(`wrong stop (${actualStop}) for ${expectedElement}`) + } + } + + if (elementType === 'cds') { + const actualDashArray = child.getAttribute('stroke-dasharray') + if (actualDashArray !== null) { + throw new Error(`got dashed line where solid expected for ${expectedElement}`) + } + } + + if (elementType === 'utr') { + const actualDashArray = child.getAttribute('stroke-dasharray') + if (actualDashArray !== '2 5') { + throw new Error(`got solid line where dashed expected for ${expectedElement}`) + } + } + + if (elementType === 'stop_marker') { + const [_, position] = expectedElement + const actualTransform = child.getAttribute('transform') || '' + if (!actualTransform.includes(`translate(${position}`)) { + throw new Error(`wrong transform (${actualTransform}) for ${expectedElement}`) + } + } + }) +} + +describe('ClinvarAllVariantsPlot', () => { + describe.each(['+', '-'] as Strand[])('rendering a frameshift on strand %s', (strand) => { + const baseVariant = clinvarVariantFactory.build({ + major_consequence: 'frameshift_variant', + pos: 23, + }) + + const transcript = transcriptFactory.build({ + transcript_id: baseVariant.transcript_id, + strand, + exons: [ + { + feature_type: 'UTR', + start: 123, + stop: 222, + }, + { + feature_type: 'CDS', + start: 223, + stop: 322, + }, + { + feature_type: 'CDS', + start: 423, + stop: 522, + }, + { + feature_type: 'CDS', + start: 623, + stop: 722, + }, + { + feature_type: 'UTR', + start: 723, + stop: 822, + }, + ], + }) + + const scalePosition = (...args: any[]) => args + const onClickVariant = (...args: any[]) => args + const width = 1200 + + test('renders properly when the variant ends in the coding section', () => { + // The HGVSP below specifies a frameshift 50 codons long, starting with + // codon 30. + // + // Hence, counting bases 1-based as usual, the variation begins with the + // first base of codon 30, located at 30 * 3 - 2 = 88 bases into the + // coding section. + // + // Considering codon 30 as the first codon, the end of the variation + // will be the last base of the 50th codon, located at + // (30 + 50 - 1) * 3 = 237 bases into the coding section. + // + // Each of the exons we defined above is 100 bases long, so for a + // + strand variant, we want the plot to render: + // * part of first CDS, from base (223 + 88 - 1) = 310 to its end + // at 322 (13 bases) + // * all of the second CDS, from 423 to 522 (100 bases) + // * part of third CDS, from its start at 623 to base + // (623 + 150 - 13 - 100 - 1) = 659 (37 bases) + // + // In addition, we also want to render the two intervening introns, and + // an "X" marking the 3' end (to the right as we plot it). + // + // By the converse of that logic, on the - strand, we expect the plot + // to render: + // * part of (gene-wise) first CDS, from base + // (722 - 88 + 1) = 635 down to its end at 623 (13 bases) + // * all of the (gene-wise) second CDS, from base 522 down to 423 + // (100 bases) + // * all of the (gene-wise) third CDS, from base 322 down to base + // (322 - 150 + 13 + 100 + 1) = 286 (37 bases) + // + // ...and again, two intervening introns and an "X" at the 3' end (left + // as we plot it). + + const variant = { ...baseVariant, hgvsp: 'p.Tyr30SerfsTer50' } + const tree = render( + + ).asFragment() + const plotBody = extractPlotFragment(tree) + + const plotLayouts: Record = { + '+': [ + ['overlay', 310, 659 - 310], + ['cds', 310, 322], + ['utr', 322, 423], + ['cds', 423, 522], + ['utr', 522, 623], + ['cds', 623, 659], + ['stop_marker', 659], + ], + '-': [ + ['overlay', 286, 635 - 286], + ['cds', 286, 322], + ['utr', 322, 423], + ['cds', 423, 522], + ['utr', 522, 623], + ['cds', 623, 635], + ['stop_marker', 286], + ], + } + const expectedPlotLayout = plotLayouts[strand] + + expect(() => plotLayoutMatches(expectedPlotLayout, plotBody)).not.toThrowError() + expect(tree).toMatchSnapshot() + }) + + test('renders properly when the variant ends in the downstream UTR', () => { + // Same logic as in the test above, but now our variants are 30 codons longer, so the far end of the variant should be 27 bases into the 3' UTR + const variant = { ...baseVariant, hgvsp: 'p.Tyr30SerfsTer80' } + const tree = render( + + ).asFragment() + const plotBody = extractPlotFragment(tree) + + const plotLayouts: Record = { + '+': [ + ['overlay', 310, 749 - 310], + ['cds', 310, 322], + ['utr', 322, 423], + ['cds', 423, 522], + ['utr', 522, 623], + ['cds', 623, 722], + ['utr', 723, 749], + ['stop_marker', 749], + ], + '-': [ + ['overlay', 196, 635 - 196], + ['utr', 196, 222], + ['cds', 223, 322], + ['utr', 322, 423], + ['cds', 423, 522], + ['utr', 522, 623], + ['cds', 623, 635], + ['stop_marker', 196], + ], + } + const expectedPlotLayout = plotLayouts[strand] + + expect(() => plotLayoutMatches(expectedPlotLayout, plotBody)).not.toThrowError() + expect(tree).toMatchSnapshot() + }) + + test('renders clamped to the end of the downstream UTR when the variant overruns the downstream UTR', () => { + // Same logic as in the first test, but now our variants are 60 codons longer, so the far end of the variant should be at the end of the downstream UTR + const variant = { ...baseVariant, hgvsp: 'p.Tyr30SerfsTer110' } + const tree = render( + + ).asFragment() + const plotBody = extractPlotFragment(tree) + + const plotLayouts: Record = { + '+': [ + ['overlay', 310, 822 - 310], + ['cds', 310, 322], + ['utr', 322, 423], + ['cds', 423, 522], + ['utr', 522, 623], + ['cds', 623, 722], + ['utr', 723, 822], + ['stop_marker', 822], + ], + '-': [ + ['overlay', 123, 635 - 123], + ['utr', 123, 222], + ['cds', 223, 322], + ['utr', 322, 423], + ['cds', 423, 522], + ['utr', 522, 623], + ['cds', 623, 635], + ['stop_marker', 123], + ], + } + const expectedPlotLayout = plotLayouts[strand] + + expect(() => plotLayoutMatches(expectedPlotLayout, plotBody)).not.toThrowError() + expect(tree).toMatchSnapshot() + }) + }) +}) diff --git a/browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.tsx b/browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.tsx index 167426750..b5c69a1ca 100644 --- a/browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.tsx +++ b/browser/src/ClinvarVariantsTrack/ClinvarAllVariantsPlot.tsx @@ -1,6 +1,6 @@ import { symbol, symbolCircle, symbolCross, symbolDiamond, symbolTriangle } from 'd3-shape' -import { debounce } from 'lodash-es' -import React, { useCallback, useMemo, useState } from 'react' +import { debounce, sortBy } from 'lodash-es' +import React, { useCallback, useState } from 'react' import { TooltipAnchor } from '@gnomad/ui' @@ -12,78 +12,117 @@ import { } from './clinvarVariantCategories' import { ClinvarVariant } from '../VariantPage/VariantPage' import ClinvarVariantTooltip from './ClinvarVariantTooltip' -import { Transcript } from '../TranscriptPage/TranscriptPage' +import { Transcript, Exon } from '../TranscriptPage/TranscriptPage' +import { Strand } from '../GenePage/GenePage' const symbolColor = (clinical_significance: ClinicalSignificance) => CLINICAL_SIGNIFICANCE_CATEGORY_COLORS[clinical_significance] // For a description of HGVS frameshift notation, see // https://varnomen.hgvs.org/recommendations/protein/variant/frameshift/ -const getFrameshiftTerminationSitePosition = ( +const getGlobalFrameshiftCoordinates = ( variant: ClinvarVariant, transcript?: Transcript -): number => { +): [number, number] => { if (!transcript || !variant.hgvsp) { - return variant.pos + return [variant.pos, variant.pos] } const match = /^p\.[a-z]{3}(\d+)[a-z]{3,}?fsTer(\d+|\?)$/i.exec(variant.hgvsp) // If HGVSp annotation does not match a frameshift, draw the termination site at the variant's position if (!match) { - return variant.pos + return [variant.pos, variant.pos] } - // Codon of the first amino acid changed const position = Number(match[1]) // Codon within the new reading frame of the termination site const terminationSitePosition = match[2] - - const exons = transcript.exons.sort((e1, e2) => e1.start - e2.start) - // Codon numbers in HGVSp notation start from the 5' end for the + strand and the 3' end for the - strand. - if (transcript.strand === '-') { - exons.reverse() - } + const exons: Exon[] = sortBy(transcript.exons, (exon: Exon) => + transcript.strand === '+' ? exon.start : -exon.start + ) // Codon positions extracted from HGVS notation start at the CDS region and may extend into the downstream UTR - const codingAndDownstreamExons = exons.slice(exons.findIndex((e) => e.feature_type === 'CDS')) + const codingAndDownstreamExons = exons.slice( + exons.findIndex((exon: Exon) => exon.feature_type === 'CDS') + ) + const codingRegionStart = + transcript.strand === '+' ? codingAndDownstreamExons[0].start : codingAndDownstreamExons[0].stop // Termination site position may be "?" if the new reading frame does not encounter a stop codon // In this case, place the termination site at the end of the transcript + const lastExon = codingAndDownstreamExons[codingAndDownstreamExons.length - 1] + const transcriptEnd = transcript.strand === '+' ? lastExon.stop : lastExon.start + if (terminationSitePosition === '?') { - return transcript.strand === '-' - ? codingAndDownstreamExons[0].start - : codingAndDownstreamExons[codingAndDownstreamExons.length - 1].stop + return [codingRegionStart, transcriptEnd] } // Offset in bases from the start of the transcript's CDS region to the termination site // Codon numbers are 1 indexed - const baseOffset = (position - 1 + Number(terminationSitePosition) - 1) * 3 - - let remainingOffset = baseOffset - // Termination site should always fall within an exon - const exonContainingTerminationSite = codingAndDownstreamExons.find((e) => { - const exonSize = e.stop - e.start + 1 - if (remainingOffset < exonSize) { - return true - } - remainingOffset -= exonSize - return false - }) + const startOffsetFromCDS = position * 3 - 2 + // The extra "+2" at the end is because we start at the first nucleotide of + // the first codon, and end with the last nucleotide (rather than the first) + // of some downstream codon. + const lengthInNucleotides = (Number(terminationSitePosition) - 1) * 3 + 2 + + // Both ends should always fall within an exon + const { remainingIntervals, globalCoordinate: startCoordinate } = advanceOverIntervals( + codingAndDownstreamExons, + startOffsetFromCDS, + transcript.strand + ) + const { globalCoordinate: endCoordinate } = advanceOverIntervals( + remainingIntervals, + lengthInNucleotides, + transcript.strand + ) + return [startCoordinate || transcriptEnd, endCoordinate || transcriptEnd] +} - return transcript.strand === '-' - ? /* @ts-expect-error */ - exonContainingTerminationSite.stop - remainingOffset - : /* @ts-expect-error */ - exonContainingTerminationSite.start + remainingOffset +interface Interval { + start: number + stop: number } +const advanceOverIntervals = ( + intervals: Interval[], + distance: number, + strand: Strand +): { remainingIntervals: Interval[]; globalCoordinate: number | null } => { + if (intervals.length === 0) { + return { remainingIntervals: [], globalCoordinate: null } + } + + const [interval, ...remainingIntervals] = intervals + const intervalSize = interval.stop - interval.start + 1 + if (intervalSize < distance) { + return advanceOverIntervals(remainingIntervals, distance - intervalSize, strand) + } + + if (intervalSize === distance) { + const intervalDownstreamEnd = strand === '+' ? interval.stop : interval.start + return { remainingIntervals, globalCoordinate: intervalDownstreamEnd } + } + + const globalCoordinate = + strand === '+' ? interval.start + distance - 1 : interval.stop - distance + 1 + const newLeadingInterval = + strand === '+' + ? { start: globalCoordinate + 1, stop: interval.stop } + : { start: interval.start, stop: globalCoordinate - 1 } + return { globalCoordinate, remainingIntervals: [newLeadingInterval, ...remainingIntervals] } +} + +type ScalePositionFn = (...args: any[]) => any +type VariantClickCallback = (...args: any[]) => any + type ClinvarAllVariantsPlotProps = { - scalePosition: (...args: any[]) => any + scalePosition: ScalePositionFn transcripts: Transcript[] variants: ClinvarVariant[] width: number - onClickVariant: (...args: any[]) => any + onClickVariant: VariantClickCallback } type Category = 'frameshift' | 'other_lof' | 'missense' | 'splice_region' | 'other' @@ -96,6 +135,203 @@ type VariantRenderingDetails = { clinicalSignificance: ClinicalSignificance } +type LineSegmentProps = { + x1: number + x2: number + y: number + opacity: number +} + +const UTRLineSegment = ({ x1, x2, y, opacity }: LineSegmentProps) => ( + +) + +const CDSLineSegment = ({ x1, x2, y, opacity }: LineSegmentProps) => ( + +) + +const circle = symbol().size(32).type(symbolCircle)() +const cross = symbol().size(40).type(symbolCross)() +const diamond = symbol().size(32).type(symbolDiamond)() +const triangle = symbol().size(32).type(symbolTriangle)() + +/** + * Render symbol based on variant's consequence + * - LoF / essential splice site: square + * - Frameshift: triangle dash square + * - Missense / in-frame indel: triangle + * - Non-essential splice region: diamond + * - Synonymous / non-coding: circle + * - Other: star + */ +const VariantLine = ({ + point, + highlightedCategory, + transcripts, + scalePosition, + onClickVariant, + plotHeight, +}: { + point: VariantRenderingDetails + highlightedCategory: string | null + transcripts: Transcript[] + scalePosition: ScalePositionFn + onClickVariant: VariantClickCallback + plotHeight: number +}) => { + const { variant, clinicalSignificance } = point + const category = clinvarVariantConsequenceCategory(variant) + const fill = symbolColor(clinicalSignificance) + let opacity = 1 + if ( + highlightedCategory && + !( + highlightedCategory === category || + (category === 'synonymous' && highlightedCategory === 'other') + ) + ) { + opacity = 0.2 + } + + if (category === 'frameshift') { + const transcript = transcripts.find((t) => t.transcript_id === variant.transcript_id) + const [endpoint1, endpoint2] = getGlobalFrameshiftCoordinates(variant, transcript) + const frameshiftMinPos = Math.min(endpoint1, endpoint2) + const frameshiftMaxPos = Math.max(endpoint1, endpoint2) + const terminationPos = + transcript && transcript.strand === '+' ? frameshiftMaxPos : frameshiftMinPos + // @ts-expect-error TS(2532) FIXME: Object is possibly 'undefined'. + const frameshiftExonRegions = transcript.exons + .sort((e1, e2) => e1.start - e2.start) + .filter((e) => e.start <= frameshiftMaxPos && e.stop >= frameshiftMinPos) + .map((e) => ({ + start: Math.max(e.start, frameshiftMinPos), + stop: Math.min(e.stop, frameshiftMaxPos), + feature_type: e.feature_type, + })) + + return ( + + onClickVariant(variant)}> + + {frameshiftExonRegions.map((r, i, regions) => { + const lineY = plotHeight - point.y + return ( + + {i !== 0 && + regions[i - 1].feature_type === 'CDS' && + regions[i].feature_type === 'CDS' && ( + + )} + {r.feature_type === 'CDS' ? ( + + ) : ( + + )} + + ) + })} + + + + ) + } + + let symbolPath = circle + let symbolRotation = 0 + let symbolOffset = 0 + if (category === 'other_lof') { + symbolPath = cross + symbolRotation = 45 + } else if (category === 'missense') { + symbolPath = triangle + symbolOffset = 1 + } else if (category === 'splice_region') { + symbolPath = diamond + } + return ( + + onClickVariant(variant)} + style={{ cursor: 'pointer' }} + /> + + ) +} + const ClinvarAllVariantsPlot = ({ scalePosition, transcripts, @@ -133,15 +369,13 @@ const ClinvarAllVariantsPlot = ({ let xEnd: number if (variant.major_consequence === 'frameshift_variant') { - // For transcripts on the negative strand, the termination site will be at a lower global position - // than the variant's position. - const pos1 = scalePosition(variant.pos) const transcript = transcripts.find((t) => t.transcript_id === variant.transcript_id) - const pos2 = transcript - ? scalePosition(getFrameshiftTerminationSitePosition(variant, transcript)) - : pos1 - xStart = Math.min(pos1, pos2) - xEnd = Math.max(pos1, pos2) + const [endpoint1, endpoint2] = getGlobalFrameshiftCoordinates(variant, transcript) + // The order in which getGlobalFrameshiftCoordinates returns the + // endpoints isn't guaranteed, i.e. either might be smaller than the + // other. + xStart = scalePosition(Math.min(endpoint1, endpoint2)) + xEnd = scalePosition(Math.max(endpoint1, endpoint2)) } else { xStart = scalePosition(variant.pos) xEnd = xStart @@ -169,136 +403,6 @@ const ClinvarAllVariantsPlot = ({ const plotHeight = rows.length * rowHeight - const circle = useMemo(() => symbol().size(32).type(symbolCircle)(), []) - const cross = useMemo(() => symbol().size(40).type(symbolCross)(), []) - const diamond = useMemo(() => symbol().size(32).type(symbolDiamond)(), []) - const triangle = useMemo(() => symbol().size(32).type(symbolTriangle)(), []) - - /** - * Render symbol based on variant's consequence - * - LoF / essential splice site: square - * - Frameshift: triangle dash square - * - Missense / in-frame indel: triangle - * - Non-essential splice region: diamond - * - Synonymous / non-coding: circle - * - Other: star - */ - const renderMarker = (point: VariantRenderingDetails) => { - const { variant, clinicalSignificance } = point - const category = clinvarVariantConsequenceCategory(variant) - const fill = symbolColor(clinicalSignificance) - let opacity = 1 - if ( - highlightedCategory && - !( - highlightedCategory === category || - (category === 'synonymous' && highlightedCategory === 'other') - ) - ) { - opacity = 0.2 - } - - if (category === 'frameshift') { - const transcript = transcripts.find((t) => t.transcript_id === variant.transcript_id) - const terminationSitePosition = getFrameshiftTerminationSitePosition(variant, transcript) - const frameshiftMinPos = Math.min(variant.pos, terminationSitePosition) - const frameshiftMaxPos = Math.max(variant.pos, terminationSitePosition) - // @ts-expect-error TS(2532) FIXME: Object is possibly 'undefined'. - const frameshiftExonRegions = transcript.exons - .sort((e1, e2) => e1.start - e2.start) - .filter((e) => e.start <= frameshiftMaxPos && e.stop >= frameshiftMinPos) - .map((e) => ({ - start: Math.max(e.start, frameshiftMinPos), - stop: Math.min(e.stop, frameshiftMaxPos), - })) - - return ( - onClickVariant(variant)}> - - {frameshiftExonRegions.map((r, i, regions) => { - const lineY = plotHeight - point.y - return ( - - {i !== 0 && ( - - )} - - - ) - })} - - - ) - } - - let symbolPath = circle - let symbolRotation = 0 - let symbolOffset = 0 - if (category === 'other_lof') { - symbolPath = cross - symbolRotation = 45 - } else if (category === 'missense') { - symbolPath = triangle - symbolOffset = 1 - } else if (category === 'splice_region') { - symbolPath = diamond - } - return ( - onClickVariant(variant)} - style={{ cursor: 'pointer' }} - /> - ) - } - return ( @@ -363,14 +467,15 @@ const ClinvarAllVariantsPlot = ({ // eslint-disable-next-line react/no-array-index-key {points.map((point) => ( - - {renderMarker(point)} - + ))} ))} diff --git a/browser/src/ClinvarVariantsTrack/__snapshots__/ClinvarAllVariantsPlot.spec.tsx.snap b/browser/src/ClinvarVariantsTrack/__snapshots__/ClinvarAllVariantsPlot.spec.tsx.snap new file mode 100644 index 000000000..f2f43d880 --- /dev/null +++ b/browser/src/ClinvarVariantsTrack/__snapshots__/ClinvarAllVariantsPlot.spec.tsx.snap @@ -0,0 +1,1155 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClinvarAllVariantsPlot rendering a frameshift on strand + renders clamped to the end of the downstream UTR when the variant overruns the downstream UTR 1`] = ` + + + + + + + + Frameshift + + + + + + Other pLoF + + + + + + Missense / Inframe indel + + + + + + Splice region + + + + + + Synonymous / non-coding + + + + + + + + + + + + + + + + + + +`; + +exports[`ClinvarAllVariantsPlot rendering a frameshift on strand + renders properly when the variant ends in the coding section 1`] = ` + + + + + + + + Frameshift + + + + + + Other pLoF + + + + + + Missense / Inframe indel + + + + + + Splice region + + + + + + Synonymous / non-coding + + + + + + + + + + + + + + + + + +`; + +exports[`ClinvarAllVariantsPlot rendering a frameshift on strand + renders properly when the variant ends in the downstream UTR 1`] = ` + + + + + + + + Frameshift + + + + + + Other pLoF + + + + + + Missense / Inframe indel + + + + + + Splice region + + + + + + Synonymous / non-coding + + + + + + + + + + + + + + + + + + +`; + +exports[`ClinvarAllVariantsPlot rendering a frameshift on strand - renders clamped to the end of the downstream UTR when the variant overruns the downstream UTR 1`] = ` + + + + + + + + Frameshift + + + + + + Other pLoF + + + + + + Missense / Inframe indel + + + + + + Splice region + + + + + + Synonymous / non-coding + + + + + + + + + + + + + + + + + + +`; + +exports[`ClinvarAllVariantsPlot rendering a frameshift on strand - renders properly when the variant ends in the coding section 1`] = ` + + + + + + + + Frameshift + + + + + + Other pLoF + + + + + + Missense / Inframe indel + + + + + + Splice region + + + + + + Synonymous / non-coding + + + + + + + + + + + + + + + + + +`; + +exports[`ClinvarAllVariantsPlot rendering a frameshift on strand - renders properly when the variant ends in the downstream UTR 1`] = ` + + + + + + + + Frameshift + + + + + + Other pLoF + + + + + + Missense / Inframe indel + + + + + + Splice region + + + + + + Synonymous / non-coding + + + + + + + + + + + + + + + + + + +`; diff --git a/browser/src/TranscriptPage/TranscriptPage.tsx b/browser/src/TranscriptPage/TranscriptPage.tsx index ffe8e45a5..d9410bebb 100644 --- a/browser/src/TranscriptPage/TranscriptPage.tsx +++ b/browser/src/TranscriptPage/TranscriptPage.tsx @@ -33,6 +33,12 @@ import { GtexTissueExpression } from '../GenePage/TranscriptsTissueExpression' import { Variant, ClinvarVariant } from '../VariantPage/VariantPage' import { MitochondrialVariant } from '../MitochondrialVariantPage/MitochondrialVariantPage' +export type Exon = { + feature_type: string + start: number + stop: number +} + export type Transcript = { transcript_id: string transcript_version: string @@ -41,11 +47,7 @@ export type Transcript = { strand: Strand start: number stop: number - exons: { - feature_type: string - start: number - stop: number - }[] + exons: Exon[] gnomad_constraint: GnomadConstraint | null exac_constraint: ExacConstraint | null gene: GeneMetadata diff --git a/browser/src/__factories__/ClinvarVariant.ts b/browser/src/__factories__/ClinvarVariant.ts new file mode 100644 index 000000000..69b71e6f3 --- /dev/null +++ b/browser/src/__factories__/ClinvarVariant.ts @@ -0,0 +1,43 @@ +import { Factory } from 'fishery' +import { ClinvarVariant } from '../VariantPage/VariantPage' + +const clinvarVariantFactory = Factory.define( + ({ params, associations, sequence }) => { + const { + clinical_significance = 'benign', + clinvar_variation_id = (123456 + sequence).toString(), + gold_stars = 5, + last_evaluated = null, + release_date = '2023-03-01', + review_status = 'criteria provided, single submitter', + submissions = [], + hgvsc = null, + hgvsp = null, + in_gnomad = false, + major_consequence = null, + pos = 123, + transcript_id = 'transcript-1', + variant_id = `123-${456 + sequence}-A-C`, + } = params + const { gnomad = null } = associations + return { + clinical_significance, + clinvar_variation_id, + gold_stars, + last_evaluated, + release_date, + review_status, + submissions, + gnomad, + hgvsc, + hgvsp, + in_gnomad, + major_consequence, + pos, + transcript_id, + variant_id, + } + } +) + +export default clinvarVariantFactory