From ff19e08bbd9be7a91d0fa3b618a232b3ff19e59f Mon Sep 17 00:00:00 2001 From: Guzman Date: Mon, 8 Jan 2024 15:40:44 -0300 Subject: [PATCH 01/11] Added handle for translation in linear view --- README.md | 4 +- demo/lib/App.tsx | 8 +- src/Linear/Linear.tsx | 4 +- src/Linear/SeqBlock.tsx | 4 +- src/Linear/Translations.tsx | 159 +++++++++++++++++++++++++++++++++--- src/SelectionHandler.tsx | 1 + src/SeqViewerContainer.tsx | 2 +- src/SeqViz.tsx | 10 ++- src/elements.ts | 9 ++ src/selectionContext.ts | 1 + src/sequence.ts | 6 +- src/style.ts | 15 ++++ 12 files changed, 195 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 7377fc72a..687d5098c 100644 --- a/README.md +++ b/README.md @@ -168,11 +168,11 @@ In the example above, the forward and reverse primers of LacZ are define by the #### `translations (=[])` -An array of `translations`: sequence ranges to translate and render as amino acids sequences. Requires 0-based `start` (inclusive) and `end` (exclusive) indexes relative the DNA sequence. A direction is required: `1` (FWD) or `-1` (REV). +An array of `translations`: sequence ranges to translate and render as amino acids sequences. Requires 0-based `start` (inclusive) and `end` (exclusive) indexes relative the DNA sequence. A direction is required: `1` (FWD) or `-1` (REV). It will also render a handle to select the entire range. A color and a name are optional for the handle. If no name is provided, start and end indices will be used as the name. ```js translations = [ - { start: 0, end: 90, direction: 1 }, // [0, 90) + { start: 0, end: 90, direction: 1, name: "ORF 1", color: "#FAA887" }, // [0, 90) { start: 191, end: 522, direction: -1 }, ]; ``` diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index c787f118b..b76703ab0 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -18,7 +18,7 @@ import Circular from "../../src/Circular/Circular"; import Linear from "../../src/Linear/Linear"; import SeqViz from "../../src/SeqViz"; import { chooseRandomColor } from "../../src/colors"; -import { AnnotationProp, Primer } from "../../src/elements"; +import { AnnotationProp, Primer, TranslationProp } from "../../src/elements"; import Header from "./Header"; import file from "./file"; @@ -43,7 +43,7 @@ interface AppState { showIndex: boolean; showSelectionMeta: boolean; showSidebar: boolean; - translations: { direction?: 1 | -1; end: number; start: number }[]; + translations: TranslationProp[]; viewer: string; zoom: number; } @@ -105,9 +105,9 @@ export default class App extends React.Component { showSelectionMeta: false, showSidebar: false, translations: [ - { direction: -1, end: 630, start: 6 }, + { color: chooseRandomColor(), direction: -1, end: 630, name: "ORF 1", start: 6 }, { end: 1147, start: 736 }, - { end: 1885, start: 1165 }, + { end: 1885, name: "ORF 2", start: 1165 }, ], viewer: "both", zoom: 50, diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index 41fd28032..64e17dc53 100644 --- a/src/Linear/Linear.tsx +++ b/src/Linear/Linear.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { InputRefFunc } from "../SelectionHandler"; -import { Annotation, CutSite, Highlight, NameRange, Primer, Range, SeqType, Size } from "../elements"; +import { Annotation, CutSite, Highlight, NameRange, Primer, SeqType, Size } from "../elements"; import { createMultiRows, createSingleRows, stackElements } from "../elementsToRows"; import { isEqual } from "../isEqual"; import { createTranslations } from "../sequence"; @@ -29,7 +29,7 @@ export interface LinearProps { showComplement: boolean; showIndex: boolean; size: Size; - translations: Range[]; + translations: NameRange[]; zoom: { linear: number }; } diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index e7fd375b1..a04c2e5b6 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -279,8 +279,9 @@ export class SeqBlock extends React.PureComponent { const primerRevHeight = primerRevRows.length ? elementHeight * primerRevRows.length : 0; // height and yDiff of translations + // elementHeight * 2 is to account for the translation handle const translationYDiff = primerRevYDiff + primerRevHeight; - const translationHeight = elementHeight * translationRows.length; + const translationHeight = elementHeight * 2 * translationRows.length; // height and yDiff of annotations const annYDiff = translationYDiff + translationHeight; @@ -424,6 +425,7 @@ export class SeqBlock extends React.PureComponent { charWidth={charWidth} elementHeight={elementHeight} findXAndWidth={this.findXAndWidth} + findXAndWidthElement={this.findXAndWidthElement} firstBase={firstBase} fullSeq={fullSeq} inputRef={inputRef} diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 384db0675..884ba6ecc 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -2,16 +2,25 @@ import * as React from "react"; import { InputRefFunc } from "../SelectionHandler"; import { borderColorByIndex, colorByIndex } from "../colors"; -import { SeqType, Translation } from "../elements"; +import { NameRange, SeqType, Translation } from "../elements"; import { randomID } from "../sequence"; -import { translationAminoAcidLabel } from "../style"; -import { FindXAndWidthType } from "./SeqBlock"; +import { translationAminoAcidLabel, translationHandle, translationHandleLabel } from "../style"; +import { FindXAndWidthElementType, FindXAndWidthType } from "./SeqBlock"; + +const hoverOtherTranshlationHandleRows = (className: string, opacity: number) => { + if (!document) return; + const elements = document.getElementsByClassName(className) as HTMLCollectionOf; + for (let i = 0; i < elements.length; i += 1) { + elements[i].style.fillOpacity = `${opacity}`; + } +}; interface TranslationRowsProps { bpsPerBlock: number; charWidth: number; elementHeight: number; findXAndWidth: FindXAndWidthType; + findXAndWidthElement: FindXAndWidthElementType; firstBase: number; fullSeq: string; inputRef: InputRefFunc; @@ -28,6 +37,7 @@ export const TranslationRows = ({ charWidth, elementHeight, findXAndWidth, + findXAndWidthElement, firstBase, fullSeq, inputRef, @@ -43,7 +53,9 @@ export const TranslationRows = ({ key={`i-${firstBase}`} bpsPerBlock={bpsPerBlock} charWidth={charWidth} + elementHeight={elementHeight} findXAndWidth={findXAndWidth} + findXAndWidthElement={findXAndWidthElement} firstBase={firstBase} fullSeq={fullSeq} height={elementHeight * 0.9} @@ -51,7 +63,7 @@ export const TranslationRows = ({ lastBase={lastBase} seqType={seqType} translations={translations} - y={yDiff + elementHeight * i} + y={yDiff + elementHeight * 2 * i} // * 2 because we have two elements per row, the aminoacids and the handle onUnmount={onUnmount} /> ))} @@ -65,7 +77,9 @@ export const TranslationRows = ({ const TranslationRow = (props: { bpsPerBlock: number; charWidth: number; + elementHeight: number; findXAndWidth: FindXAndWidthType; + findXAndWidthElement: FindXAndWidthElementType; firstBase: number; fullSeq: string; height: number; @@ -78,16 +92,26 @@ const TranslationRow = (props: { }) => ( <> {props.translations.map((t, i) => ( - + <> + + + + ))} ); -interface SingleNamedElementProps { +interface SingleNamedElementAminoacidsProps { bpsPerBlock: number; charWidth: number; findXAndWidth: FindXAndWidthType; @@ -106,7 +130,7 @@ interface SingleNamedElementProps { * A single row for translations of DNA into Amino Acid sequences so a user can * see the resulting protein or peptide sequence in the viewer */ -class SingleNamedElement extends React.PureComponent { +class SingleNamedElementAminoacids extends React.PureComponent { AAs: string[] = []; // on unmount, clear all AA references. @@ -268,3 +292,116 @@ class SingleNamedElement extends React.PureComponent { ); } } + + +/** + * SingleNamedElement is a single rectangular element in the SeqBlock. + * It does a bunch of stuff to avoid edge-cases from wrapping around the 0-index, edge of blocks, etc. + */ +const SingleNamedElementHandle = (props: { + element: NameRange; + elementHeight: number; + elements: NameRange[]; + findXAndWidthElement: FindXAndWidthElementType; + height: number; + index: number; + inputRef: InputRefFunc; + y: number; +}) => { + const { element, elementHeight, elements, findXAndWidthElement, index, inputRef, y } = props; + + const { color, end, name, start } = element; + const { width, x: origX } = findXAndWidthElement(index, element, elements); + + + // 0.591 is our best approximation of Roboto Mono's aspect ratio (width / height). + const fontSize = 12; + const characterWidth = 0.591 * fontSize; + // Use at most 1/4 of the width for the name handle. + const availableCharacters = Math.floor((width / 4) / characterWidth); + + let displayName = name; + if (name.length > availableCharacters) { + const charactersToShow = availableCharacters - 1; + if (charactersToShow < 3) { + // If we can't show at least three characters, don't show any. + displayName = ""; + } else { + displayName = `${name.slice(0, charactersToShow)}…`; + } + } + + + + // What's needed for the display + margin at the start + margin at the end + const nameHandleMargin = 10 + const nameHandleWidth = displayName.length * characterWidth + nameHandleMargin * 2 + + const x = origX; + const w = width; + const height = props.height; + + + let linePath = "" + // First rectangle that contains the name and has the whole height + linePath += `M 0 0 L ${nameHandleWidth} 0 L ${nameHandleWidth} ${height} L 0 ${height}`; + // Second rectangle with half the height and centered + linePath += `M ${nameHandleWidth} ${height / 4} L ${w} ${height / 4} L ${w} ${3 * height / 4} L ${nameHandleWidth} ${3 * height / 4}`; + + return ( + + + {/* provides a hover tooltip on most browsers */} + <title>{name} + { + // do nothing + }} + onFocus={() => { + // do nothing + }} + onMouseOut={() => hoverOtherTranshlationHandleRows(element.id, 0.7)} + onMouseOver={() => hoverOtherTranshlationHandleRows(element.id, 1.0)} + /> + { + // do nothing + }} + onFocus={() => { + // do nothing + }} + onMouseOut={() => hoverOtherTranshlationHandleRows(element.id, 0.7)} + onMouseOver={() => hoverOtherTranshlationHandleRows(element.id, 1.0)} + > + {displayName} + + + + ); +}; + diff --git a/src/SelectionHandler.tsx b/src/SelectionHandler.tsx index b1d53f553..b2bb238a1 100644 --- a/src/SelectionHandler.tsx +++ b/src/SelectionHandler.tsx @@ -134,6 +134,7 @@ export default class SelectionHandler extends React.PureComponent; /** testSize is a forced height/width that overwrites anything from sizeMe. For testing */ testSize?: { height: number; width: number }; - translations: Range[]; + translations: NameRange[]; viewer: "linear" | "circular" | "both" | "both_flip"; width: number; zoom: { circular: number; linear: number }; diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index 78e2034e1..b798f238d 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -15,6 +15,7 @@ import { PrimerProp, Range, SeqType, + TranslationProp, } from "./elements"; import { isEqual } from "./isEqual"; import search from "./search"; @@ -150,7 +151,7 @@ export interface SeqVizProps { style?: Record; /** ranges of sequence that should have amino acid translations shown */ - translations?: { direction?: number; end: number; start: number }[]; + translations?: TranslationProp[]; /** the orientation of the viewer(s). "both", the default, has a circular viewer on left and a linear viewer on right. */ viewer?: "linear" | "circular" | "both" | "both_flip"; @@ -427,7 +428,7 @@ export default class SeqViz extends React.Component { // If the seqType is aa, make the entire sequence the "translation" if (seqType === "aa") { // TODO: during some grand future refactor, make this cleaner and more transparent to the user - translations = [{ direction: 1, end: seq.length, start: 0 }]; + translations = [{ direction: 1, end: seq.length, start: 0, name: "translation" }]; } // Since all the props are optional, we need to parse them to defaults. @@ -455,10 +456,13 @@ export default class SeqViz extends React.Component { rotateOnScroll: !!this.props.rotateOnScroll, showComplement: (!!compSeq && (typeof showComplement !== "undefined" ? showComplement : true)) || false, showIndex: !!showIndex, - translations: (translations || []).map((t): { direction: 1 | -1; end: number; start: number } => ({ + translations: (translations || []).map((t, i): { direction: 1 | -1; end: number; start: number, color: string, id: string, name: string } => ({ direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, end: seqType === "aa" ? t.end : t.start + Math.floor((t.end - t.start) / 3) * 3, start: t.start % seq.length, + color: t.color || colorByIndex(i, COLORS), + id: `translation${t.name}${i}${t.start}${t.end}`, + name: t.name || `${t.start}-${t.end}` })), viewer: this.props.viewer || "both", zoom: { diff --git a/src/elements.ts b/src/elements.ts index c81b327e9..2b53c4cc1 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -21,6 +21,15 @@ export interface AnnotationProp { start: number; } +/** AnnotationProp is an annotation provided to SeqViz via the annotations prop. */ +export interface TranslationProp { + color?: string; + direction?: number; + end: number; + name?: string; + start: number; +} + /** Annotation is an annotation after parsing. */ export interface Annotation extends NameRange { color: string; diff --git a/src/selectionContext.ts b/src/selectionContext.ts index 0c7e10dd3..8626f94bd 100644 --- a/src/selectionContext.ts +++ b/src/selectionContext.ts @@ -4,6 +4,7 @@ type SelectionTypeEnum = | "ANNOTATION" | "FIND" | "TRANSLATION" + | "TRANSLATION_HANDLE" | "ENZYME" | "SEQ" | "AMINOACID" diff --git a/src/sequence.ts b/src/sequence.ts index 26749063b..f5147db88 100644 --- a/src/sequence.ts +++ b/src/sequence.ts @@ -1,4 +1,4 @@ -import { Range, SeqType } from "./elements"; +import { NameRange, SeqType } from "./elements"; /** * Map of nucleotide bases @@ -297,7 +297,7 @@ export const translate = (seqInput: string, seqType: SeqType): string => { /** * for each translation (range + direction) and the input sequence, convert it to a translation and amino acid sequence */ -export const createTranslations = (translations: Range[], seq: string, seqType: SeqType) => { +export const createTranslations = (translations: NameRange[], seq: string, seqType: SeqType) => { // elongate the original sequence to account for translations that cross the zero index const seqDoubled = seq + seq; const bpPerBlock = seqType === "aa" ? 1 : 3; @@ -329,8 +329,6 @@ export const createTranslations = (translations: Range[], seq: string, seqType: } return { - id: randomID(), - name: "translation", ...t, AAseq: aaSeq, end: tEnd, diff --git a/src/style.ts b/src/style.ts index 797dd2fe1..02fc0945f 100644 --- a/src/style.ts +++ b/src/style.ts @@ -105,6 +105,21 @@ export const annotationLabel: CSS.Properties = { textRendering: "optimizeLegibility", }; +export const translationHandle: CSS.Properties = { + fillOpacity: "0.7", + shapeRendering: "geometricPrecision", + strokeWidth: "0.5", +}; + +export const translationHandleLabel: CSS.Properties = { + ...svgText, + color: "rgb(42, 42, 42)", + fontWeight: 400, + shapeRendering: "geometricPrecision", + strokeLinejoin: "round", + textRendering: "optimizeLegibility", +}; + export const translationAminoAcidLabel: CSS.Properties = { ...svgText, color: "rgb(42, 42, 42)", From f5a53eb9cff6c840f1fbff3ad27cc9e5df4029d7 Mon Sep 17 00:00:00 2001 From: Guzman Date: Mon, 8 Jan 2024 15:43:22 -0300 Subject: [PATCH 02/11] Ran linter --- src/Linear/Translations.tsx | 19 +++++++------------ src/SeqViz.tsx | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 884ba6ecc..00b732c00 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -106,7 +106,6 @@ const TranslationRow = (props: { index={i} /> - ))} ); @@ -293,7 +292,6 @@ class SingleNamedElementAminoacids extends React.PureComponent availableCharacters) { @@ -330,23 +327,22 @@ const SingleNamedElementHandle = (props: { displayName = `${name.slice(0, charactersToShow)}…`; } } - - // What's needed for the display + margin at the start + margin at the end - const nameHandleMargin = 10 - const nameHandleWidth = displayName.length * characterWidth + nameHandleMargin * 2 + const nameHandleMargin = 10; + const nameHandleWidth = displayName.length * characterWidth + nameHandleMargin * 2; const x = origX; const w = width; const height = props.height; - - let linePath = "" + let linePath = ""; // First rectangle that contains the name and has the whole height linePath += `M 0 0 L ${nameHandleWidth} 0 L ${nameHandleWidth} ${height} L 0 ${height}`; // Second rectangle with half the height and centered - linePath += `M ${nameHandleWidth} ${height / 4} L ${w} ${height / 4} L ${w} ${3 * height / 4} L ${nameHandleWidth} ${3 * height / 4}`; + linePath += `M ${nameHandleWidth} ${height / 4} L ${w} ${height / 4} L ${w} ${ + (3 * height) / 4 + } L ${nameHandleWidth} ${(3 * height) / 4}`; return ( ); }; - diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index b798f238d..30067a188 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -456,14 +456,16 @@ export default class SeqViz extends React.Component { rotateOnScroll: !!this.props.rotateOnScroll, showComplement: (!!compSeq && (typeof showComplement !== "undefined" ? showComplement : true)) || false, showIndex: !!showIndex, - translations: (translations || []).map((t, i): { direction: 1 | -1; end: number; start: number, color: string, id: string, name: string } => ({ - direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, - end: seqType === "aa" ? t.end : t.start + Math.floor((t.end - t.start) / 3) * 3, - start: t.start % seq.length, - color: t.color || colorByIndex(i, COLORS), - id: `translation${t.name}${i}${t.start}${t.end}`, - name: t.name || `${t.start}-${t.end}` - })), + translations: (translations || []).map( + (t, i): { direction: 1 | -1; end: number; start: number; color: string; id: string; name: string } => ({ + direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, + end: seqType === "aa" ? t.end : t.start + Math.floor((t.end - t.start) / 3) * 3, + start: t.start % seq.length, + color: t.color || colorByIndex(i, COLORS), + id: `translation${t.name}${i}${t.start}${t.end}`, + name: t.name || `${t.start}-${t.end}`, + }) + ), viewer: this.props.viewer || "both", zoom: { circular: typeof zoom?.circular == "number" ? Math.min(Math.max(zoom.circular, 0), 100) : 0, From 61414e85ae68b6a4739da30de8554758b4122d15 Mon Sep 17 00:00:00 2001 From: Guzman Date: Mon, 8 Jan 2024 15:45:38 -0300 Subject: [PATCH 03/11] Remove unused import --- src/SeqViewerContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeqViewerContainer.tsx b/src/SeqViewerContainer.tsx index ada32903c..4209da565 100644 --- a/src/SeqViewerContainer.tsx +++ b/src/SeqViewerContainer.tsx @@ -6,7 +6,7 @@ import { EventHandler } from "./EventHandler"; import Linear, { LinearProps } from "./Linear/Linear"; import SelectionHandler, { InputRefFunc } from "./SelectionHandler"; import CentralIndexContext from "./centralIndexContext"; -import { Annotation, CutSite, Highlight, NameRange, Primer, Range, SeqType } from "./elements"; +import { Annotation, CutSite, Highlight, NameRange, Primer, SeqType } from "./elements"; import { isEqual } from "./isEqual"; import SelectionContext, { Selection, defaultSelection } from "./selectionContext"; From 28bcb6b0631284d6f425e47ff402a82c440678cf Mon Sep 17 00:00:00 2001 From: Guzman Date: Mon, 8 Jan 2024 16:06:00 -0300 Subject: [PATCH 04/11] Fixed translation tests --- src/Linear/Translations.tsx | 2 ++ src/index.test.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 00b732c00..4644544f0 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -190,6 +190,8 @@ class SingleNamedElementAminoacids extends React.PureComponent diff --git a/src/index.test.tsx b/src/index.test.tsx index 9b7e35261..fa891b9bc 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -70,7 +70,7 @@ describe("SeqViz rendering (React)", () => { expect(getByTestId("la-vz-viewer-linear")).toBeTruthy(); expect(getAllByTestId("la-vz-viewer-linear")).toHaveLength(1); - const seqs = getAllByTestId("la-vz-linear-translation"); + const seqs = getAllByTestId("la-vz-linear-aa-translation"); const seq = seqs.map(s => s.textContent).join(""); expect(seq.length).toBeGreaterThan(0); expect(aaSeq).toContain(seq); From a681701127aefde672a7d5ddafa7108ba7c2247f Mon Sep 17 00:00:00 2001 From: Guzman Date: Mon, 8 Jan 2024 19:30:23 -0300 Subject: [PATCH 05/11] Making translation row take the whole height so there's no gap between AAs and handle --- src/Linear/Translations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 4644544f0..d0590bc62 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -58,7 +58,7 @@ export const TranslationRows = ({ findXAndWidthElement={findXAndWidthElement} firstBase={firstBase} fullSeq={fullSeq} - height={elementHeight * 0.9} + height={elementHeight} inputRef={inputRef} lastBase={lastBase} seqType={seqType} From 70d5f787ff5aedb3fd2cf437c2bc894762890e84 Mon Sep 17 00:00:00 2001 From: Guzman Date: Tue, 9 Jan 2024 13:17:56 -0300 Subject: [PATCH 06/11] Increasing block height to account for handle --- src/Linear/Linear.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index 64e17dc53..64c6193e1 100644 --- a/src/Linear/Linear.tsx +++ b/src/Linear/Linear.tsx @@ -165,7 +165,7 @@ export default class Linear extends React.Component { blockHeight += lineHeight; // another for index row } if (translationRows[i].length) { - blockHeight += translationRows[i].length * elementHeight; + blockHeight += translationRows[i].length * elementHeight * 2; // * 2 to account for the translation handle } if (annotationRows[i].length) { blockHeight += annotationRows[i].length * elementHeight; From 5cd4ec0d40f0ed5b770262ad420bec25932f72fd Mon Sep 17 00:00:00 2001 From: Guzman Date: Tue, 9 Jan 2024 13:51:17 -0300 Subject: [PATCH 07/11] Added margin bottom to the translations --- src/Linear/Translations.tsx | 28 ++++++++++++++++++---------- src/style.ts | 1 + 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index d0590bc62..46964f1ca 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -314,7 +314,7 @@ const SingleNamedElementHandle = (props: { const { width, x: origX } = findXAndWidthElement(index, element, elements); // 0.591 is our best approximation of Roboto Mono's aspect ratio (width / height). - const fontSize = 12; + const fontSize = 10; const characterWidth = 0.591 * fontSize; // Use at most 1/4 of the width for the name handle. const availableCharacters = Math.floor(width / 4 / characterWidth); @@ -331,20 +331,28 @@ const SingleNamedElementHandle = (props: { } // What's needed for the display + margin at the start + margin at the end - const nameHandleMargin = 10; - const nameHandleWidth = displayName.length * characterWidth + nameHandleMargin * 2; + const nameHandleLeftMargin = 10; + const nameHandleWidth = displayName.length * characterWidth + nameHandleLeftMargin * 2; const x = origX; const w = width; const height = props.height; + const marginBottom = 3; let linePath = ""; // First rectangle that contains the name and has the whole height - linePath += `M 0 0 L ${nameHandleWidth} 0 L ${nameHandleWidth} ${height} L 0 ${height}`; - // Second rectangle with half the height and centered - linePath += `M ${nameHandleWidth} ${height / 4} L ${w} ${height / 4} L ${w} ${ - (3 * height) / 4 - } L ${nameHandleWidth} ${(3 * height) / 4}`; + linePath += `M 0 0 + L ${nameHandleWidth} 0 + L ${nameHandleWidth} ${height - marginBottom} + L 0 ${height - marginBottom} + Z`; + + // Second rectangle with half the height and centered (minus half the margin at the bottom and half on top) + linePath += `M ${nameHandleWidth} ${height / 4 - marginBottom / 2} + L ${w} ${height / 4 - marginBottom / 2} + L ${w} ${(3 * height) / 4 - marginBottom / 2} + L ${nameHandleWidth} ${(3 * height) / 4 - marginBottom / 2} + Z`; return ( { // do nothing }} diff --git a/src/style.ts b/src/style.ts index 02fc0945f..77951644d 100644 --- a/src/style.ts +++ b/src/style.ts @@ -114,6 +114,7 @@ export const translationHandle: CSS.Properties = { export const translationHandleLabel: CSS.Properties = { ...svgText, color: "rgb(42, 42, 42)", + fontSize: "10", fontWeight: 400, shapeRendering: "geometricPrecision", strokeLinejoin: "round", From 8f62da23fa2045218befdb3d09e0e3f91f48e189 Mon Sep 17 00:00:00 2001 From: Guzman Date: Thu, 22 Feb 2024 12:06:34 -0300 Subject: [PATCH 08/11] Added margin and border to handle --- src/Linear/Translations.tsx | 20 +++++++++++--------- src/style.ts | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 46964f1ca..f275dad04 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -314,7 +314,7 @@ const SingleNamedElementHandle = (props: { const { width, x: origX } = findXAndWidthElement(index, element, elements); // 0.591 is our best approximation of Roboto Mono's aspect ratio (width / height). - const fontSize = 10; + const fontSize = 9; const characterWidth = 0.591 * fontSize; // Use at most 1/4 of the width for the name handle. const availableCharacters = Math.floor(width / 4 / characterWidth); @@ -337,21 +337,22 @@ const SingleNamedElementHandle = (props: { const x = origX; const w = width; const height = props.height; - const marginBottom = 3; + const marginBottom = 2; + const marginTop = 2; let linePath = ""; // First rectangle that contains the name and has the whole height - linePath += `M 0 0 - L ${nameHandleWidth} 0 + linePath += `M 0 ${marginTop} + L ${nameHandleWidth} ${marginTop} L ${nameHandleWidth} ${height - marginBottom} L 0 ${height - marginBottom} Z`; // Second rectangle with half the height and centered (minus half the margin at the bottom and half on top) - linePath += `M ${nameHandleWidth} ${height / 4 - marginBottom / 2} - L ${w} ${height / 4 - marginBottom / 2} - L ${w} ${(3 * height) / 4 - marginBottom / 2} - L ${nameHandleWidth} ${(3 * height) / 4 - marginBottom / 2} + linePath += `M ${nameHandleWidth} ${height / 4 - marginBottom / 2 + marginTop / 2} + L ${w} ${height / 4 - marginBottom / 2 + marginTop / 2} + L ${w} ${(3 * height) / 4 - marginBottom / 2 + marginTop / 2} + L ${nameHandleWidth} ${(3 * height) / 4 - marginBottom / 2 + marginTop / 2} Z`; return ( @@ -375,6 +376,7 @@ const SingleNamedElementHandle = (props: { d={linePath} fill={color} id={element.id} + stroke={color} style={translationHandle} onBlur={() => { // do nothing @@ -394,7 +396,7 @@ const SingleNamedElementHandle = (props: { style={translationHandleLabel} textAnchor="start" x={nameHandleLeftMargin} - y={height / 2 - 1} + y={height / 2 + 1} onBlur={() => { // do nothing }} diff --git a/src/style.ts b/src/style.ts index 77951644d..5968ca6c3 100644 --- a/src/style.ts +++ b/src/style.ts @@ -114,7 +114,7 @@ export const translationHandle: CSS.Properties = { export const translationHandleLabel: CSS.Properties = { ...svgText, color: "rgb(42, 42, 42)", - fontSize: "10", + fontSize: "9", fontWeight: 400, shapeRendering: "geometricPrecision", strokeLinejoin: "round", From 958bfdd93b3dbad0c1a4ec87ecd0db48f9bcd476 Mon Sep 17 00:00:00 2001 From: Guzman Date: Thu, 22 Feb 2024 12:43:20 -0300 Subject: [PATCH 09/11] Making it so if there's no name for the translation, we don't show the handle --- README.md | 2 +- src/Linear/SeqBlock.tsx | 8 +++-- src/Linear/Translations.tsx | 66 +++++++++++++++++++++---------------- src/SeqViz.tsx | 7 ++-- src/elements.ts | 2 +- 5 files changed, 51 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 1c65498e2..b197291ae 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ In the example above, the forward and reverse primers of LacZ are define by the #### `translations (=[])` -An array of `translations`: sequence ranges to translate and render as amino acids sequences. Requires 0-based `start` (inclusive) and `end` (exclusive) indexes relative the DNA sequence. A direction is required: `1` (FWD) or `-1` (REV). It will also render a handle to select the entire range. A color and a name are optional for the handle. If no name is provided, start and end indices will be used as the name. +An array of `translations`: sequence ranges to translate and render as amino acids sequences. Requires 0-based `start` (inclusive) and `end` (exclusive) indexes relative the DNA sequence. A direction is required: `1` (FWD) or `-1` (REV). It will also render a handle to select the entire range. A color and a name are optional for the handle. If no name is provided, the handle will not be rendered. ```js translations = [ diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index a04c2e5b6..8b179d169 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -279,9 +279,13 @@ export class SeqBlock extends React.PureComponent { const primerRevHeight = primerRevRows.length ? elementHeight * primerRevRows.length : 0; // height and yDiff of translations - // elementHeight * 2 is to account for the translation handle + // elementHeight * 2 is to account for the translation handle. If no name, don't show the handle const translationYDiff = primerRevYDiff + primerRevHeight; - const translationHeight = elementHeight * 2 * translationRows.length; + let translationHeight = 0; + for (let i = 0; i < translationRows.length; i++) { + const multiplier = translationRows[i][0]["name"] ? 2 : 1; + translationHeight += elementHeight * multiplier; + } // height and yDiff of annotations const annYDiff = translationYDiff + translationHeight; diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index f275dad04..729b4214d 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -48,25 +48,33 @@ export const TranslationRows = ({ yDiff, }: TranslationRowsProps) => ( - {translationRows.map((translations, i) => ( - - ))} + {translationRows.map((translations, i) => { + // Add up the previous translation heights, taking into account if they have a handle or not + let currentElementY = yDiff; + for (let j = 0; j < i; j += 1) { + const multiplier = translationRows[j][0]["name"] ? 2 : 1; + currentElementY += elementHeight * multiplier; + } + return ( + + ); + })} ); @@ -98,13 +106,15 @@ const TranslationRow = (props: { key={`translation-linear-${t.id}-${i}-${props.firstBase}-${props.lastBase}`} translation={t} /> - + {t.name && ( + + )} ))} @@ -319,8 +329,8 @@ const SingleNamedElementHandle = (props: { // Use at most 1/4 of the width for the name handle. const availableCharacters = Math.floor(width / 4 / characterWidth); - let displayName = name; - if (name.length > availableCharacters) { + let displayName = name ?? ""; + if (name && name.length > availableCharacters) { const charactersToShow = availableCharacters - 1; if (charactersToShow < 3) { // If we can't show at least three characters, don't show any. diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index 70246909c..1dac53961 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -453,13 +453,16 @@ export default class SeqViz extends React.Component { showComplement: (!!compSeq && (typeof showComplement !== "undefined" ? showComplement : true)) || false, showIndex: !!showIndex, translations: (translations || []).map( - (t, i): { direction: 1 | -1; end: number; start: number; color: string; id: string; name: string } => ({ + ( + t, + i + ): { direction: 1 | -1; end: number; start: number; color: string; id: string; name: string | undefined } => ({ direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, end: seqType === "aa" ? t.end : t.start + Math.floor((t.end - t.start) / 3) * 3, start: t.start % seq.length, color: t.color || colorByIndex(i, COLORS), id: `translation${t.name}${i}${t.start}${t.end}`, - name: t.name || `${t.start}-${t.end}`, + name: t.name, }) ), viewer: this.props.viewer || "both", diff --git a/src/elements.ts b/src/elements.ts index 2b53c4cc1..569b841b6 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -9,7 +9,7 @@ export interface Range { export interface NameRange extends Range { color?: string; id: string; - name: string; + name?: string; } /** AnnotationProp is an annotation provided to SeqViz via the annotations prop. */ From 07e251406a97bebfda179e7cc71610369fef3798 Mon Sep 17 00:00:00 2001 From: Guzman Date: Mon, 26 Feb 2024 19:56:21 -0300 Subject: [PATCH 10/11] Combined both elements of the handle --- src/Linear/Translations.tsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 729b4214d..334325f11 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -351,19 +351,15 @@ const SingleNamedElementHandle = (props: { const marginTop = 2; let linePath = ""; - // First rectangle that contains the name and has the whole height linePath += `M 0 ${marginTop} - L ${nameHandleWidth} ${marginTop} - L ${nameHandleWidth} ${height - marginBottom} - L 0 ${height - marginBottom} - Z`; - - // Second rectangle with half the height and centered (minus half the margin at the bottom and half on top) - linePath += `M ${nameHandleWidth} ${height / 4 - marginBottom / 2 + marginTop / 2} - L ${w} ${height / 4 - marginBottom / 2 + marginTop / 2} - L ${w} ${(3 * height) / 4 - marginBottom / 2 + marginTop / 2} - L ${nameHandleWidth} ${(3 * height) / 4 - marginBottom / 2 + marginTop / 2} - Z`; + L ${nameHandleWidth} ${marginTop} + L ${nameHandleWidth} ${height / 4 - marginBottom / 2 + marginTop / 2} + L ${w} ${height / 4 - marginBottom / 2 + marginTop / 2} + L ${w} ${(3 * height) / 4 - marginBottom / 2 + marginTop / 2} + L ${nameHandleWidth} ${(3 * height) / 4 - marginBottom / 2 + marginTop / 2} + L ${nameHandleWidth} ${height - marginBottom} + L 0 ${height - marginBottom} + Z`; return ( Date: Mon, 26 Feb 2024 20:19:36 -0300 Subject: [PATCH 11/11] Translation have now a mandatory name so they can use the NameRange interface as the other elements --- README.md | 4 ++-- demo/lib/App.tsx | 2 +- src/SeqViz.tsx | 5 +---- src/elements.ts | 6 +++--- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b197291ae..f1db306b7 100644 --- a/README.md +++ b/README.md @@ -170,12 +170,12 @@ In the example above, the forward and reverse primers of LacZ are define by the #### `translations (=[])` -An array of `translations`: sequence ranges to translate and render as amino acids sequences. Requires 0-based `start` (inclusive) and `end` (exclusive) indexes relative the DNA sequence. A direction is required: `1` (FWD) or `-1` (REV). It will also render a handle to select the entire range. A color and a name are optional for the handle. If no name is provided, the handle will not be rendered. +An array of `translations`: sequence ranges to translate and render as amino acids sequences. Requires 0-based `start` (inclusive) and `end` (exclusive) indexes relative the DNA sequence. A direction is required: `1` (FWD) or `-1` (REV). It will also render a handle to select the entire range. A color is optional for the handle. If the empry string ("") is provided as the name, the handle will not be rendered. ```js translations = [ { start: 0, end: 90, direction: 1, name: "ORF 1", color: "#FAA887" }, // [0, 90) - { start: 191, end: 522, direction: -1 }, + { start: 191, end: 522, direction: -1, name: "" }, ]; ``` diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index cecd03619..f9013723e 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -98,7 +98,7 @@ export default class App extends React.Component { showSidebar: false, translations: [ { color: chooseRandomColor(), direction: -1, end: 630, name: "ORF 1", start: 6 }, - { end: 1147, start: 736 }, + { end: 1147, name: "", start: 736 }, { end: 1885, name: "ORF 2", start: 1165 }, ], viewer: "both", diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index 1dac53961..35f1cfa70 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -453,10 +453,7 @@ export default class SeqViz extends React.Component { showComplement: (!!compSeq && (typeof showComplement !== "undefined" ? showComplement : true)) || false, showIndex: !!showIndex, translations: (translations || []).map( - ( - t, - i - ): { direction: 1 | -1; end: number; start: number; color: string; id: string; name: string | undefined } => ({ + (t, i): { direction: 1 | -1; end: number; start: number; color: string; id: string; name: string } => ({ direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, end: seqType === "aa" ? t.end : t.start + Math.floor((t.end - t.start) / 3) * 3, start: t.start % seq.length, diff --git a/src/elements.ts b/src/elements.ts index 569b841b6..93943dcc6 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -9,7 +9,7 @@ export interface Range { export interface NameRange extends Range { color?: string; id: string; - name?: string; + name: string; } /** AnnotationProp is an annotation provided to SeqViz via the annotations prop. */ @@ -21,12 +21,12 @@ export interface AnnotationProp { start: number; } -/** AnnotationProp is an annotation provided to SeqViz via the annotations prop. */ +/** TranslationProp is an translation provided to SeqViz via the translation prop. */ export interface TranslationProp { color?: string; direction?: number; end: number; - name?: string; + name: string; start: number; }