Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/translation handle #254

Merged
merged 13 commits into from
Apr 5, 2024
Merged
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
Loading