Skip to content

Commit

Permalink
Added handle for translation in linear view
Browse files Browse the repository at this point in the history
  • Loading branch information
guzmanvig committed Jan 8, 2024
1 parent ea558f2 commit ff19e08
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 28 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
];
```
Expand Down
8 changes: 4 additions & 4 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 @@ -105,9 +105,9 @@ export default class App extends React.Component<any, AppState> {
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,
Expand Down
4 changes: 2 additions & 2 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
4 changes: 3 additions & 1 deletion src/Linear/SeqBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,9 @@ 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
const translationYDiff = primerRevYDiff + primerRevHeight;
const translationHeight = elementHeight * translationRows.length;
const translationHeight = elementHeight * 2 * translationRows.length;

// height and yDiff of annotations
const annYDiff = translationYDiff + translationHeight;
Expand Down Expand Up @@ -424,6 +425,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
159 changes: 148 additions & 11 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 @@ -43,15 +53,17 @@ export const TranslationRows = ({
key={`i-${firstBase}`}
bpsPerBlock={bpsPerBlock}
charWidth={charWidth}
elementHeight={elementHeight}
findXAndWidth={findXAndWidth}
findXAndWidthElement={findXAndWidthElement}
firstBase={firstBase}
fullSeq={fullSeq}
height={elementHeight * 0.9}
inputRef={inputRef}
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}
/>
))}
Expand All @@ -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;
Expand All @@ -78,16 +92,26 @@ 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}
/>
<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 +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<SingleNamedElementProps> {
class SingleNamedElementAminoacids extends React.PureComponent<SingleNamedElementAminoacidsProps> {
AAs: string[] = [];

// on unmount, clear all AA references.
Expand Down Expand Up @@ -268,3 +292,116 @@ 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 = 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 (
<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}
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={nameHandleMargin}
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
2 changes: 1 addition & 1 deletion src/SeqViewerContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,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
10 changes: 7 additions & 3 deletions src/SeqViz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
PrimerProp,
Range,
SeqType,
TranslationProp,
} from "./elements";
import { isEqual } from "./isEqual";
import search from "./search";
Expand Down Expand Up @@ -150,7 +151,7 @@ export interface SeqVizProps {
style?: Record<string, unknown>;

/** 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";
Expand Down Expand Up @@ -427,7 +428,7 @@ export default class SeqViz extends React.Component<SeqVizProps, SeqVizState> {
// 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.
Expand Down Expand Up @@ -455,10 +456,13 @@ export default class SeqViz extends React.Component<SeqVizProps, SeqVizState> {
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: {
Expand Down
9 changes: 9 additions & 0 deletions src/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/selectionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type SelectionTypeEnum =
| "ANNOTATION"
| "FIND"
| "TRANSLATION"
| "TRANSLATION_HANDLE"
| "ENZYME"
| "SEQ"
| "AMINOACID"
Expand Down
Loading

0 comments on commit ff19e08

Please sign in to comment.