Skip to content

Commit

Permalink
Merge pull request #254 from Lattice-Automation/feat/translation-handle
Browse files Browse the repository at this point in the history
Feat/translation handle
  • Loading branch information
guzmanvig authored Apr 5, 2024
2 parents 3ca9492 + da0a558 commit a0d2143
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 53 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
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 }, // [0, 90)
{ start: 191, end: 522, direction: -1 },
{ start: 0, end: 90, direction: 1, name: "ORF 1", color: "#FAA887" }, // [0, 90)
{ start: 191, end: 522, direction: -1, name: "" },
];
```

Expand Down
10 changes: 5 additions & 5 deletions demo/lib/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
}
Expand Down Expand Up @@ -97,9 +97,9 @@ export default class App extends React.Component<any, AppState> {
showSelectionMeta: false,
showSidebar: false,
translations: [
{ direction: -1, end: 630, start: 6 },
{ end: 1147, start: 736 },
{ end: 1885, start: 1165 },
{ color: chooseRandomColor(), direction: -1, end: 630, name: "ORF 1", start: 6 },
{ end: 1147, name: "", start: 736 },
{ end: 1885, name: "ORF 2", start: 1165 },
],
viewer: "both",
zoom: 50,
Expand Down
6 changes: 3 additions & 3 deletions src/Linear/Linear.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -29,7 +29,7 @@ export interface LinearProps {
showComplement: boolean;
showIndex: boolean;
size: Size;
translations: Range[];
translations: NameRange[];
zoom: { linear: number };
}

Expand Down Expand Up @@ -165,7 +165,7 @@ export default class Linear extends React.Component<LinearProps> {
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;
Expand Down
8 changes: 7 additions & 1 deletion src/Linear/SeqBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,13 @@ export class SeqBlock extends React.PureComponent<SeqBlockProps> {
const primerRevHeight = primerRevRows.length ? elementHeight * primerRevRows.length : 0;

// height and yDiff of translations
// elementHeight * 2 is to account for the translation handle. If no name, don't show the handle
const translationYDiff = primerRevYDiff + primerRevHeight;
const translationHeight = elementHeight * 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;
Expand Down Expand Up @@ -424,6 +429,7 @@ export class SeqBlock extends React.PureComponent<SeqBlockProps> {
charWidth={charWidth}
elementHeight={elementHeight}
findXAndWidth={this.findXAndWidth}
findXAndWidthElement={this.findXAndWidthElement}
firstBase={firstBase}
fullSeq={fullSeq}
inputRef={inputRef}
Expand Down
204 changes: 177 additions & 27 deletions src/Linear/Translations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>;
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;
Expand All @@ -28,6 +37,7 @@ export const TranslationRows = ({
charWidth,
elementHeight,
findXAndWidth,
findXAndWidthElement,
firstBase,
fullSeq,
inputRef,
Expand All @@ -38,23 +48,33 @@ export const TranslationRows = ({
yDiff,
}: TranslationRowsProps) => (
<g className="la-vz-linear-translation" data-testid="la-vz-linear-translation">
{translationRows.map((translations, i) => (
<TranslationRow
key={`i-${firstBase}`}
bpsPerBlock={bpsPerBlock}
charWidth={charWidth}
findXAndWidth={findXAndWidth}
firstBase={firstBase}
fullSeq={fullSeq}
height={elementHeight * 0.9}
inputRef={inputRef}
lastBase={lastBase}
seqType={seqType}
translations={translations}
y={yDiff + elementHeight * i}
onUnmount={onUnmount}
/>
))}
{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 (
<TranslationRow
key={`i-${firstBase}`}
bpsPerBlock={bpsPerBlock}
charWidth={charWidth}
elementHeight={elementHeight}
findXAndWidth={findXAndWidth}
findXAndWidthElement={findXAndWidthElement}
firstBase={firstBase}
fullSeq={fullSeq}
height={elementHeight}
inputRef={inputRef}
lastBase={lastBase}
seqType={seqType}
translations={translations}
y={currentElementY}
onUnmount={onUnmount}
/>
);
})}
</g>
);

Expand All @@ -65,7 +85,9 @@ export const TranslationRows = ({
const TranslationRow = (props: {
bpsPerBlock: number;
charWidth: number;
elementHeight: number;
findXAndWidth: FindXAndWidthType;
findXAndWidthElement: FindXAndWidthElementType;
firstBase: number;
fullSeq: string;
height: number;
Expand All @@ -78,16 +100,27 @@ const TranslationRow = (props: {
}) => (
<>
{props.translations.map((t, i) => (
<SingleNamedElement
{...props} // include overflowLeft in the key to avoid two split annotations in the same row from sharing a key
key={`translation-linear-${t.id}-${i}-${props.firstBase}-${props.lastBase}`}
translation={t}
/>
<>
<SingleNamedElementAminoacids
{...props}
key={`translation-linear-${t.id}-${i}-${props.firstBase}-${props.lastBase}`}
translation={t}
/>
{t.name && (
<SingleNamedElementHandle
{...props}
key={`translation-handle-linear-${t.id}-${i}-${props.firstBase}-${props.lastBase}`}
element={t}
elements={props.translations}
index={i}
/>
)}
</>
))}
</>
);

interface SingleNamedElementProps {
interface SingleNamedElementAminoacidsProps {
bpsPerBlock: number;
charWidth: number;
findXAndWidth: FindXAndWidthType;
Expand All @@ -106,7 +139,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<SingleNamedElementProps> {
class SingleNamedElementAminoacids extends React.PureComponent<SingleNamedElementAminoacidsProps> {
AAs: string[] = [];

// on unmount, clear all AA references.
Expand Down Expand Up @@ -167,6 +200,8 @@ class SingleNamedElement extends React.PureComponent<SingleNamedElementProps> {
type: "AMINOACID",
viewer: "LINEAR",
})}
className="la-vz-linear-aa-translation"
data-testid="la-vz-linear-aa-translation"
id={id}
transform={`translate(0, ${y})`}
>
Expand Down Expand Up @@ -268,3 +303,118 @@ class SingleNamedElement extends React.PureComponent<SingleNamedElementProps> {
);
}
}

/**
* 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 = 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);

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.
displayName = "";
} else {
displayName = `${name.slice(0, charactersToShow)}…`;
}
}

// What's needed for the display + margin at the start + margin at the end
const nameHandleLeftMargin = 10;
const nameHandleWidth = displayName.length * characterWidth + nameHandleLeftMargin * 2;

const x = origX;
const w = width;
const height = props.height;
const marginBottom = 2;
const marginTop = 2;

let linePath = "";
linePath += `M 0 ${marginTop}
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 (
<g
ref={inputRef(element.id, {
end,
name,
start,
type: "TRANSLATION_HANDLE",
viewer: "LINEAR",
})}
id={element.id}
transform={`translate(0, ${y + elementHeight})`}
>
<g id={element.id} transform={`translate(${x}, 0)`}>
{/* <title> provides a hover tooltip on most browsers */}
<title>{name}</title>
<path
className={`${element.id} la-vz-translation-handle`}
cursor="pointer"
d={linePath}
fill={color}
id={element.id}
stroke={color}
style={translationHandle}
onBlur={() => {
// do nothing
}}
onFocus={() => {
// do nothing
}}
onMouseOut={() => hoverOtherTranshlationHandleRows(element.id, 0.7)}
onMouseOver={() => hoverOtherTranshlationHandleRows(element.id, 1.0)}
/>
<text
className="la-vz-handle-label"
cursor="pointer"
dominantBaseline="middle"
fontSize={fontSize}
id={element.id}
style={translationHandleLabel}
textAnchor="start"
x={nameHandleLeftMargin}
y={height / 2 + 1}
onBlur={() => {
// do nothing
}}
onFocus={() => {
// do nothing
}}
onMouseOut={() => hoverOtherTranshlationHandleRows(element.id, 0.7)}
onMouseOver={() => hoverOtherTranshlationHandleRows(element.id, 1.0)}
>
{displayName}
</text>
</g>
</g>
);
};
1 change: 1 addition & 0 deletions src/SelectionHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export default class SelectionHandler extends React.PureComponent<SelectionHandl
case "ANNOTATION":
case "FIND":
case "TRANSLATION":
case "TRANSLATION_HANDLE":
case "ENZYME":
case "PRIMER":
case "HIGHLIGHT": {
Expand Down
4 changes: 2 additions & 2 deletions src/SeqViewerContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, { ExternalSelection, Selection, defaultSelection } from "./selectionContext";

Expand Down Expand Up @@ -54,7 +54,7 @@ interface SeqViewerContainerProps {
targetRef: React.LegacyRef<HTMLDivElement>;
/** 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 };
Expand Down
Loading

0 comments on commit a0d2143

Please sign in to comment.