diff --git a/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-cds/TranscriptVariantCDS.tsx b/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-cds/TranscriptVariantCDS.tsx index 7e296365ff..25980b2220 100644 --- a/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-cds/TranscriptVariantCDS.tsx +++ b/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-cds/TranscriptVariantCDS.tsx @@ -71,16 +71,24 @@ const EXON_BLOCK_OFFSET_TOP = (VARIANT_MARKER_HEIGHT - EXON_BLOCK_HEIGHT) / 2; const TranscriptVariantCDS = (props: Props) => { const { exons, allele, cds } = props; - const scale = scaleLinear() + // length's domain is between 0 and the length of the CDS + const lengthScale = scaleLinear() .domain([0, cds.nucleotide_length]) .range([0, DIAGRAM_WIDTH]) .interpolate(interpolateRound) .clamp(true); + // meanwhile, since position starts at 1, the position scale is between 1 and the length of the CDS + const positionScale = scaleLinear() + .domain([1, cds.nucleotide_length]) + .range([0, DIAGRAM_WIDTH]) + .interpolate(interpolateRound) + .clamp(true); + const exonsWithWidths = getExonWidths({ exons, containerWidth: DIAGRAM_WIDTH, - scale + scale: lengthScale }); return ( @@ -95,7 +103,7 @@ const TranscriptVariantCDS = (props: Props) => { diff --git a/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-genomic-sequence/TranscriptVariantGenomicSequence.tsx b/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-genomic-sequence/TranscriptVariantGenomicSequence.tsx index d02677f1d5..e548d115e6 100644 --- a/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-genomic-sequence/TranscriptVariantGenomicSequence.tsx +++ b/src/content/app/entity-viewer/variant-view/transcript-consequences/transcript-variant-genomic-sequence/TranscriptVariantGenomicSequence.tsx @@ -37,6 +37,9 @@ type Props = { variantToTranscriptEndDistance: number; }; +const SEQUENCE_LETTER_WIDTH = 16; +const SEQUENCE_LETTER_RIGHT_MARGIN = 1; + const TranscriptVariantGenomicSequence = (props: Props) => { const { leftFlankingSequence, @@ -49,12 +52,21 @@ const TranscriptVariantGenomicSequence = (props: Props) => { alleleType } = props; + const leftOffsetInLetters = calculateLeftOffset({ + flankingSequence: leftFlankingSequence, + variantSequence + }); + const leftOffsetInPx = leftOffsetInLetters + ? leftOffsetInLetters * SEQUENCE_LETTER_WIDTH + + leftOffsetInLetters * SEQUENCE_LETTER_RIGHT_MARGIN + : undefined; + const leftOffset = leftOffsetInPx ? `${leftOffsetInPx}px` : undefined; + return (
-
+
@@ -75,19 +87,11 @@ const TranscriptVariantGenomicSequence = (props: Props) => { const LeftFlankingSequence = ({ flankingSequence, - variantSequence, variantToTranscriptStartDistance }: { flankingSequence: string; - variantSequence: string; variantToTranscriptStartDistance: number; }) => { - const leftOffset = calculateLeftOffset({ - flankingSequence, - variantSequence - }); - const marginLeft = leftOffset ? `${leftOffset}px` : undefined; - const letters = flankingSequence.split(''); const letterClasses = classNames( @@ -112,7 +116,6 @@ const LeftFlankingSequence = ({ letter={letter} key={index} className={hasEllipsis && index === 0 ? ellipsisClasses : letterClasses} - style={index === 0 && marginLeft ? { marginLeft } : undefined} /> )); }; @@ -345,7 +348,7 @@ const getAltAlleleAnchorPosition = ({ refAlleleSequence: string; variantType: string; }) => { - const letterWidth = 16; + const letterWidth = SEQUENCE_LETTER_WIDTH; const halfGenomicSequenceLength = Math.ceil( DISPLAYED_REFERENCE_SEQUENCE_LENGTH / 2 ); diff --git a/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.test.ts b/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.test.ts index 17ddb01d40..4a1132cfa6 100644 --- a/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.test.ts +++ b/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.test.ts @@ -496,7 +496,7 @@ describe('functions splitting genomic sequence into parts', () => { ).toBe(expectedVariantSeq); }); - test('Insertion, near forward-strand start, forward strand', () => { + test('insertion, near forward-strand start, forward strand', () => { const sequence = 'GCGGGGTCCAGCAGGCAGGCTCCGGCCGTG'; // 30 characters const expectedLeftFlankingSeq = 'GCGGGGTCC'; // first 9 characters @@ -529,7 +529,7 @@ describe('functions splitting genomic sequence into parts', () => { ).toBe(expectedVariantSeq); }); - test('Insertion, near forward-strand start, reverse strand', () => { + test('insertion, near forward-strand start, reverse strand', () => { const sequence = 'GCGGGGTCCAGCAGGCAGGCTCCGGCCGTG'; // 30 characters const expectedLeftFlankingSeq = getReverseComplement( @@ -563,6 +563,39 @@ describe('functions splitting genomic sequence into parts', () => { }) ).toBe(expectedVariantSeq); }); + + test('allele starts at the start of genomic slice, reverse strand', () => { + const sequence = 'TCTCTCACACAGTAATACATG'; // 21 characters + + const expectedLeftFlankingSeq = getReverseComplement(sequence).slice(0, 20); + const expectedVariantSeq = getReverseComplement(sequence)[20]; + const expectedRightFlankingSeq = ''; + + expect( + getLeftFlankingGenomicSequence({ + sequence, + distanceToSliceStart: 0, + distanceToSliceEnd: 20, + strand: 'reverse' + }) + ).toBe(expectedLeftFlankingSeq); + expect( + getRightFlankingGenomicSequence({ + sequence, + distanceToSliceStart: 0, + distanceToSliceEnd: 20, + strand: 'reverse' + }) + ).toBe(expectedRightFlankingSeq); + expect( + getReferenceAlleleGenomicSequence({ + sequence, + distanceToSliceStart: 0, + distanceToSliceEnd: 20, + strand: 'reverse' + }) + ).toBe(expectedVariantSeq); + }); }); describe('getProteinSliceCoordinates', () => { diff --git a/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.ts b/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.ts index 5f9a8617f3..f375724736 100644 --- a/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.ts +++ b/src/content/app/entity-viewer/variant-view/transcript-consequences/useTranscriptDetails.ts @@ -412,9 +412,11 @@ export const getLeftFlankingGenomicSequence = ({ strand: 'forward' | 'reverse'; }) => { if (strand === 'forward') { - return sequence.slice(0, distanceToSliceStart); + return distanceToSliceStart ? sequence.slice(0, distanceToSliceStart) : ''; } else { - return getReverseComplement(sequence).slice(0, distanceToSliceEnd); + return distanceToSliceEnd + ? getReverseComplement(sequence).slice(0, distanceToSliceEnd) + : ''; } }; @@ -430,9 +432,11 @@ export const getRightFlankingGenomicSequence = ({ strand: 'forward' | 'reverse'; }) => { if (strand === 'forward') { - return sequence.slice(-1 * distanceToSliceEnd); + return distanceToSliceEnd ? sequence.slice(-1 * distanceToSliceEnd) : ''; } else { - return getReverseComplement(sequence).slice(-1 * distanceToSliceStart); + return distanceToSliceStart + ? getReverseComplement(sequence).slice(-1 * distanceToSliceStart) + : ''; } }; diff --git a/src/shared/helpers/exon-helpers/exonHelpers.test.ts b/src/shared/helpers/exon-helpers/exonHelpers.test.ts index 9672f321ed..cc4056f2db 100644 --- a/src/shared/helpers/exon-helpers/exonHelpers.test.ts +++ b/src/shared/helpers/exon-helpers/exonHelpers.test.ts @@ -20,6 +20,7 @@ import * as example1 from './fixtures/exons-cds-data-1'; import * as example2 from './fixtures/exons-cds-data-2'; import * as example3 from './fixtures/exons-cds-data-3'; import * as example4 from './fixtures/exons-cds-data-4'; +import * as example5 from './fixtures/exons-cds-data-5'; describe('addRelativeLocationInCDSToExons', () => { test('transcript with a single exon and no UTRs ', () => { @@ -68,4 +69,13 @@ describe('addRelativeLocationInCDSToExons', () => { expect(result).toEqual(example4.exonsWithRelativeLocationInCDS); }); + + test('transcript with two exons and no UTRs', () => { + const result = addRelativeLocationInCDSToExons({ + exons: example5.exons, + cds: example5.cds + }); + + expect(result).toEqual(example5.exonsWithRelativeLocationInCDS); + }); }); diff --git a/src/shared/helpers/exon-helpers/exonHelpers.ts b/src/shared/helpers/exon-helpers/exonHelpers.ts index 58a858b36c..e12d73b43d 100644 --- a/src/shared/helpers/exon-helpers/exonHelpers.ts +++ b/src/shared/helpers/exon-helpers/exonHelpers.ts @@ -47,7 +47,7 @@ export const addRelativeLocationInCDSToExons = < const cds = params.cds; const exons = structuredClone(params.exons); - let lastPositionInCDS = 1; + let lastPositionInCDS = 0; for (const exon of exons) { if (doesExonIncludeCDS({ exon, cds })) { @@ -86,7 +86,7 @@ export const addRelativeLocationInCDSToExons = < !isExonEndWithinCDS({ exon, cds }) ) { // last exon in CDS - const relativeStart = lastPositionInCDS; + const relativeStart = lastPositionInCDS + 1; const remainingExonLength = cds.relative_end - exon.relative_location.start + 1; const relativeEnd = relativeStart + remainingExonLength - 1; @@ -98,7 +98,7 @@ export const addRelativeLocationInCDSToExons = < }; } else { // exon fully within CDS - const relativeStart = lastPositionInCDS; + const relativeStart = lastPositionInCDS + 1; const exonLength = exon.relative_location.end - exon.relative_location.start + 1; const relativeEnd = relativeStart + exonLength - 1; diff --git a/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-3.ts b/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-3.ts index be1864eee3..a78cf59fbe 100644 --- a/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-3.ts +++ b/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-3.ts @@ -52,6 +52,6 @@ export const exonsWithRelativeLocationInCDS = [ { index: 2, relative_location: { start: 4257, end: 6314 }, - relative_location_in_cds: { start: 406, end: 851, length: 446 } + relative_location_in_cds: { start: 407, end: 852, length: 446 } } ]; diff --git a/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-4.ts b/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-4.ts index 9e7682de7c..1e2df5f341 100644 --- a/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-4.ts +++ b/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-4.ts @@ -244,97 +244,97 @@ export const exonsWithRelativeLocationInCDS = [ { index: 3, relative_location: { start: 3570, end: 3818 }, - relative_location_in_cds: { start: 67, end: 315, length: 249 } + relative_location_in_cds: { start: 68, end: 316, length: 249 } }, { index: 4, relative_location: { start: 9569, end: 9677 }, - relative_location_in_cds: { start: 315, end: 423, length: 109 } + relative_location_in_cds: { start: 317, end: 425, length: 109 } }, { index: 5, relative_location: { start: 10594, end: 10643 }, - relative_location_in_cds: { start: 423, end: 472, length: 50 } + relative_location_in_cds: { start: 426, end: 475, length: 50 } }, { index: 6, relative_location: { start: 10735, end: 10775 }, - relative_location_in_cds: { start: 472, end: 512, length: 41 } + relative_location_in_cds: { start: 476, end: 516, length: 41 } }, { index: 7, relative_location: { start: 10992, end: 11106 }, - relative_location_in_cds: { start: 512, end: 626, length: 115 } + relative_location_in_cds: { start: 517, end: 631, length: 115 } }, { index: 8, relative_location: { start: 13936, end: 13985 }, - relative_location_in_cds: { start: 626, end: 675, length: 50 } + relative_location_in_cds: { start: 632, end: 681, length: 50 } }, { index: 9, relative_location: { start: 15412, end: 15523 }, - relative_location_in_cds: { start: 675, end: 786, length: 112 } + relative_location_in_cds: { start: 682, end: 793, length: 112 } }, { index: 10, relative_location: { start: 16765, end: 17880 }, - relative_location_in_cds: { start: 786, end: 1901, length: 1116 } + relative_location_in_cds: { start: 794, end: 1909, length: 1116 } }, { index: 11, relative_location: { start: 20758, end: 25689 }, - relative_location_in_cds: { start: 1901, end: 6832, length: 4932 } + relative_location_in_cds: { start: 1910, end: 6841, length: 4932 } }, { index: 12, relative_location: { start: 29051, end: 29146 }, - relative_location_in_cds: { start: 6832, end: 6927, length: 96 } + relative_location_in_cds: { start: 6842, end: 6937, length: 96 } }, { index: 13, relative_location: { start: 31320, end: 31389 }, - relative_location_in_cds: { start: 6927, end: 6996, length: 70 } + relative_location_in_cds: { start: 6938, end: 7007, length: 70 } }, { index: 14, relative_location: { start: 39354, end: 39781 }, - relative_location_in_cds: { start: 6996, end: 7423, length: 428 } + relative_location_in_cds: { start: 7008, end: 7435, length: 428 } }, { index: 15, relative_location: { start: 40921, end: 41102 }, - relative_location_in_cds: { start: 7423, end: 7604, length: 182 } + relative_location_in_cds: { start: 7436, end: 7617, length: 182 } }, { index: 16, relative_location: { start: 42235, end: 42422 }, - relative_location_in_cds: { start: 7604, end: 7791, length: 188 } + relative_location_in_cds: { start: 7618, end: 7805, length: 188 } }, { index: 17, relative_location: { start: 47016, end: 47186 }, - relative_location_in_cds: { start: 7791, end: 7961, length: 171 } + relative_location_in_cds: { start: 7806, end: 7976, length: 171 } }, { index: 18, relative_location: { start: 47672, end: 48026 }, - relative_location_in_cds: { start: 7961, end: 8315, length: 355 } + relative_location_in_cds: { start: 7977, end: 8331, length: 355 } }, { index: 19, relative_location: { start: 54895, end: 55050 }, - relative_location_in_cds: { start: 8315, end: 8470, length: 156 } + relative_location_in_cds: { start: 8332, end: 8487, length: 156 } }, { index: 20, relative_location: { start: 55449, end: 55593 }, - relative_location_in_cds: { start: 8470, end: 8614, length: 145 } + relative_location_in_cds: { start: 8488, end: 8632, length: 145 } }, { index: 21, relative_location: { start: 59455, end: 59703 }, - relative_location_in_cds: { start: 8614, end: 8732, length: 119 } + relative_location_in_cds: { start: 8633, end: 8751, length: 119 } }, { index: 22, diff --git a/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-5.ts b/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-5.ts new file mode 100644 index 0000000000..cabaa68ac9 --- /dev/null +++ b/src/shared/helpers/exon-helpers/fixtures/exons-cds-data-5.ts @@ -0,0 +1,73 @@ +/** + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Exons of human transcript ENST00000622028.1 +// It is a protein-coding transcript with no UTRs + +export const exons = [ + { + index: 1, + relative_location: { + start: 1, + end: 46, + length: 46 + } + }, + { + index: 2, + relative_location: { + start: 130, + end: 436, + length: 307 + } + } +]; + +export const cds = { + relative_start: 1, + relative_end: 436, + nucleotide_length: 353 +}; + +// The expected result +export const exonsWithRelativeLocationInCDS = [ + { + index: 1, + relative_location: { + start: 1, + end: 46, + length: 46 + }, + relative_location_in_cds: { + start: 1, + end: 46, + length: 46 + } + }, + { + index: 2, + relative_location: { + start: 130, + end: 436, + length: 307 + }, + relative_location_in_cds: { + start: 47, + end: 353, + length: 307 + } + } +];