From 33394f4067320ca61b391e0c1fcafe37a887a83a Mon Sep 17 00:00:00 2001 From: jjtimmons Date: Sat, 12 Nov 2022 20:21:03 -0500 Subject: [PATCH] Render AA-seqs as translations --- demo/lib/App.tsx | 3 ++- src/Linear/Index.tsx | 10 ++++++-- src/Linear/Linear.tsx | 10 +++++--- src/Linear/SeqBlock.tsx | 22 +++++++++++++---- src/Linear/Translations.tsx | 47 ++++++++++++++++++++++--------------- src/SeqViewer.tsx | 8 +++++-- src/SeqViewerContainer.tsx | 3 ++- src/SeqViz.tsx | 17 +++++++------- src/index.test.tsx | 8 +++---- src/sequence.ts | 18 ++++++++------ 10 files changed, 95 insertions(+), 51 deletions(-) diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index 3af261a39..fbaa3418b 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -68,7 +68,8 @@ export default class App extends React.Component { componentDidMount = async () => { const seq = await seqparse(file); - this.setState({ annotations: seq.annotations, name: seq.name, seq: seq.seq }); + // this.setState({ annotations: seq.annotations, name: seq.name, seq: seq.seq }); + this.setState({ annotations: seq.annotations, name: seq.name, seq: "MSKGEELFTGVVPILVELDGDVNGHKFSVSGEGEGDATYGKLTLKFICTTGKLPVPWPTLVTTFSYGVQCFSRYPDHMKQHDFFKSAMPEGYVQERTIFFKDDGNYKTRAEVKFEGDTLVNRIELKGIDFKEDGNILGHKLEYNYNSHNVYIMADKQKNGIKVNFKIRHNIEDGSVQLADHYQQNTPIGDGPVLLPDNHYLSTQSALSKDPNEKRDHMVLLEFVTAAGITHGMDELYK" }); }; toggleSidebar = () => { diff --git a/src/Linear/Index.tsx b/src/Linear/Index.tsx index 3ea7e72a4..9dc64396f 100644 --- a/src/Linear/Index.tsx +++ b/src/Linear/Index.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { Size } from "../elements"; +import { SeqType, Size } from "../elements"; import { FindXAndWidthType } from "./SeqBlock"; interface IndexProps { @@ -9,6 +9,7 @@ interface IndexProps { firstBase: number; lastBase: number; seq: string; + seqType: SeqType; showIndex: boolean; size: Size; yDiff: number; @@ -23,7 +24,7 @@ export default class Index extends React.PureComponent { // by the number set for tally thresholding and, if it is, 2) add its location to the list // of positions for tickInc genTicks = () => { - const { charWidth, findXAndWidth, firstBase, seq, size, zoom } = this.props; + const { charWidth, findXAndWidth, firstBase, seq, seqType, size, zoom } = this.props; const seqLength = seq.length; // the tally's distance on the x-axis is zoom dependent: @@ -49,6 +50,11 @@ export default class Index extends React.PureComponent { tickInc = 10; } + // if rendering amino acids, double the tick frequency + if (seqType === "aa") { + tickInc = tickInc / 2; + } + // create the array that will hold all the indexes in the array const tickIndexes: number[] = []; if (firstBase === 0) { diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index 4056b7788..12d5175fc 100644 --- a/src/Linear/Linear.tsx +++ b/src/Linear/Linear.tsx @@ -5,7 +5,7 @@ import { createMultiRows, createSingleRows, stackElements } from "../elementsToR import withViewerHOCs from "../handlers"; import { Selection } from "../handlers/selection"; import isEqual from "../isEqual"; -import { createLinearTranslations } from "../sequence"; +import { createTranslations } from "../sequence"; import InfiniteScroll from "./InfiniteScroll"; import SeqBlock from "./SeqBlock"; @@ -124,7 +124,7 @@ class Linear extends React.Component { const highlightRows = createSingleRows(highlights, bpsPerBlock, arrSize); const translationRows = translations.length - ? createSingleRows(createLinearTranslations(translations, seq, seqType), bpsPerBlock, arrSize) + ? createSingleRows(createTranslations(translations, seq, seqType), bpsPerBlock, arrSize) : new Array(arrSize).fill([]); for (let i = 0; i < arrSize; i += 1) { @@ -139,7 +139,10 @@ class Linear extends React.Component { ids[i] = seqs[i] + String(i); // find the line height for the seq block based on how many rows need to be shown - let blockHeight = lineHeight * 2.1; // this is for padding between the SeqBlocks + let blockHeight = lineHeight * 1.1; // this is for padding between the SeqBlocks + if (seqType != "aa") { + blockHeight += lineHeight; // for sequence row + } if (zoomed) { blockHeight += showComplement ? lineHeight : 0; // double for complement + 2px margin } @@ -184,6 +187,7 @@ class Linear extends React.Component { searchRows={searchRows[i]} seq={seqs[i]} seqFontSize={this.props.seqFontSize} + seqType={seqType} showComplement={showComplement} showIndex={showIndex} size={size} diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index 58ce6ee7c..7dac1c812 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -1,6 +1,16 @@ import * as React from "react"; -import { Annotation, CutSite, Highlight, InputRefFunc, NameRange, Range, Size, Translation } from "../elements"; +import { + Annotation, + CutSite, + Highlight, + InputRefFunc, + NameRange, + Range, + SeqType, + Size, + Translation, +} from "../elements"; import AnnotationRows from "./Annotations"; import CutSiteRow from "./CutSites"; import Find from "./Find"; @@ -44,6 +54,7 @@ interface SeqBlockProps { searchRows: Range[]; seq: string; seqFontSize: number; + seqType: SeqType; showComplement: boolean; showIndex: boolean; size: Size; @@ -230,6 +241,7 @@ export default class SeqBlock extends React.PureComponent { searchRows, seq, seqFontSize, + seqType, showComplement, showIndex, size, @@ -264,7 +276,7 @@ export default class SeqBlock extends React.PureComponent { // height and yDiff of the sequence strand const indexYDiff = cutSiteYDiff + cutSiteHeight; - const indexHeight = lineHeight; + const indexHeight = seqType === "aa" ? 0 : lineHeight; // if aa, no seq row is shown // height and yDiff of the complement strand const compYDiff = indexYDiff + indexHeight; @@ -312,6 +324,7 @@ export default class SeqBlock extends React.PureComponent { firstBase={firstBase} lastBase={lastBase} seq={seq} + seqType={seqType} showIndex={showIndex} size={size} yDiff={indexRowYDiff} @@ -371,6 +384,7 @@ export default class SeqBlock extends React.PureComponent { inputRef={inputRef} lastBase={lastBase} seqBlockRef={this} + seqType={seqType} translations={translations} yDiff={translationYDiff} onUnmount={onUnmount} @@ -405,12 +419,12 @@ export default class SeqBlock extends React.PureComponent { zoom={zoom} /> )} - {zoomed ? ( + {zoomed && seqType !== "aa" ? ( {seq.split("").map(this.seqTextSpan)} ) : null} - {compSeq && zoomed && showComplement ? ( + {compSeq && zoomed && showComplement && seqType !== "aa" ? ( {compSeq.split("").map(this.seqTextSpan)} diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 53dcd2a42..f0248991c 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { borderColorByIndex, colorByIndex } from "../colors"; -import { InputRefFunc, Translation } from "../elements"; +import { InputRefFunc, SeqType, Translation } from "../elements"; import randomid from "../randomid"; import { FindXAndWidthType } from "./SeqBlock"; @@ -16,6 +16,7 @@ interface TranslationRowsProps { lastBase: number; onUnmount: (a: unknown) => void; seqBlockRef: unknown; + seqType: SeqType; translations: Translation[]; yDiff: number; } @@ -32,6 +33,7 @@ const TranslationRows = ({ lastBase, onUnmount, seqBlockRef, + seqType, translations, yDiff, }: TranslationRowsProps) => ( @@ -49,6 +51,7 @@ const TranslationRows = ({ inputRef={inputRef} lastBase={lastBase} seqBlockRef={seqBlockRef} + seqType={seqType} translation={t} y={yDiff + elementHeight * i} onUnmount={onUnmount} @@ -69,6 +72,7 @@ interface TranslationRowProps { lastBase: number; onUnmount: (a: unknown) => void; seqBlockRef: unknown; + seqType: SeqType; translation: Translation; y: number; } @@ -88,14 +92,13 @@ class TranslationRow extends React.Component { /** * make the actual path string - * - * c = base pair count - * m = multiplier (FWD or REV) */ genPath = (count: number, multiplier: number) => { const { charWidth, height: h } = this.props; // width adjust + const nW = count * charWidth; const wA = multiplier * 3; + return `M 0 0 L ${nW} 0 L ${nW + wA} ${h / 2} @@ -116,6 +119,7 @@ class TranslationRow extends React.Component { inputRef, lastBase, seqBlockRef: element, + seqType, translation, y, } = this.props; @@ -124,8 +128,11 @@ class TranslationRow extends React.Component { // build up a reference to this whole translation for // selection handler (used only for context clicking right now) - const type = "TRANSLATION"; - const ref = { element, end, name: "translation", start, type }; + const ref = { element, end, name: "translation", start, type: "TRANSLATION" }; + + // if rendering an amino-acid sequence directly, each amino acid block is 1:1 with a "base pair". + // otherwise, each amino-acid covers three bases. + const bpPerBlockCount = seqType === "aa" ? 1 : 3; // substring and split only the amino acids that are relevant to this // particular sequence block @@ -139,8 +146,8 @@ class TranslationRow extends React.Component { // calculate the start and end point of each amino acid // modulo needed here for translations that cross zero index - let AAStart = (start + i * 3) % fullSeq.length; - let AAEnd = start + i * 3 + 3; + let AAStart = (start + i * bpPerBlockCount) % fullSeq.length; + let AAEnd = start + i * bpPerBlockCount + bpPerBlockCount; // build up a reference to this whole translation for // selection handler (used only for context clicking right now) @@ -170,20 +177,20 @@ class TranslationRow extends React.Component { // larger translation // the amino acid doesn't fit within this SeqBlock (even partially) - if (AAStart > lastBase || AAEnd < firstBase) return null; + if (AAStart >= lastBase || AAEnd <= firstBase) return null; - let textShow = true; // whether to show amino acids abbreviation - let bpCount = 3; // start off assuming the full thing is shown + let showAminoAcidLabel = true; // whether to show amino acids abbreviation + let bpCount = bpPerBlockCount; // start off assuming the full thing is shown if (AAStart < firstBase) { - bpCount = Math.min(3, AAEnd - firstBase); - if (bpCount < 2) { + bpCount = Math.min(bpPerBlockCount, AAEnd - firstBase); + if (bpCount < 2 && seqType !== "aa") { // w/ one bp, the amino acid is probably too small for an abbreviation - textShow = false; + showAminoAcidLabel = false; } } else if (AAEnd > lastBase) { - bpCount = Math.min(3, lastBase - AAStart); - if (bpCount < 2) { - textShow = false; + bpCount = Math.min(bpPerBlockCount, lastBase - AAStart); + if (bpCount < 2 && seqType !== "aa") { + showAminoAcidLabel = false; } } @@ -207,9 +214,11 @@ class TranslationRow extends React.Component { strokeWidth: 0.8, }} /> - {textShow && ( + + {showAminoAcidLabel && ( { fontWeight: 400, }} textAnchor="middle" - x={charWidth * 1.5} + x={bpCount * 0.5 * charWidth} y={`${h / 2 + 1}`} > {a} diff --git a/src/SeqViewer.tsx b/src/SeqViewer.tsx index c24b875a6..c301de747 100644 --- a/src/SeqViewer.tsx +++ b/src/SeqViewer.tsx @@ -3,7 +3,7 @@ import { withResizeDetector } from "react-resize-detector"; import Circular from "./Circular/Circular"; import Linear from "./Linear/Linear"; -import { Annotation, CutSite, Highlight, NameRange, Range } from "./elements"; +import { Annotation, CutSite, Highlight, NameRange, Range, SeqType } from "./elements"; import CentralIndexContext from "./handlers/centralIndex"; import { Selection, SelectionContext } from "./handlers/selection"; import isEqual from "./isEqual"; @@ -19,6 +19,7 @@ interface SeqViewerProps { name: string; search: NameRange[]; seq: string; + seqType: SeqType; setSelection: (update: Selection) => void; showComplement: boolean; showIndex: boolean; @@ -48,7 +49,7 @@ class SeqViewer extends React.Component { * on the screen at a given time and what should their size be */ linearProps = () => { - const { seq } = this.props; + const { seq, seqType } = this.props; const size = this.props.testSize || { height: this.props.height, width: this.props.width }; const zoom = this.props.zoom.linear; @@ -57,6 +58,9 @@ class SeqViewer extends React.Component { // otherwise the sequence needs to be cut into smaller subsequences // a sliding scale in width related to the degree of zoom currently active let bpsPerBlock = Math.round((size.width / seqFontSize) * 1.4) || 1; // width / 1 * seqFontSize + if (seqType === "aa") { + bpsPerBlock = Math.round(bpsPerBlock / 3); // more space for each amino acid + } if (zoom <= 5) { bpsPerBlock *= 3; diff --git a/src/SeqViewerContainer.tsx b/src/SeqViewerContainer.tsx index 818592480..d12955d2c 100644 --- a/src/SeqViewerContainer.tsx +++ b/src/SeqViewerContainer.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import SeqViewer from "./SeqViewer"; -import { Annotation, CutSite, Highlight, NameRange, Range } from "./elements"; +import { Annotation, CutSite, Highlight, NameRange, Range, SeqType } from "./elements"; import CentralIndexContext from "./handlers/centralIndex"; import { Selection, SelectionContext, defaultSelection } from "./handlers/selection"; import isEqual from "./isEqual"; @@ -21,6 +21,7 @@ interface SeqViewerContainerProps { start: number; }; seq: string; + seqType: SeqType; showComplement: boolean; showIndex: boolean; translations: Range[]; diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index d7e373479..1dba65e3b 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -212,7 +212,7 @@ export default class SeqViz extends React.Component { const input = await this.parseInput(); this.setState(input); - // this.search(input.seq); + this.search(input.seq); this.cut(input.seq, input.seqType); }; @@ -230,7 +230,7 @@ export default class SeqViz extends React.Component { // previous props { accession = "", annotations, enzymes, enzymesCustom, file, search }: SeqVizProps, // previous state - { seq }: SeqVizState + { seq, seqType }: SeqVizState ) => { // New accession or file provided, fetch and/or parse. if (accession !== this.props.accession || file !== this.props.file || (this.props.seq && this.props.seq !== seq)) { @@ -242,8 +242,8 @@ export default class SeqViz extends React.Component { seq: input.seq, seqType: input.seqType, }); - // this.search(seq); - // this.cut(seq, input.seqType); + this.search(seq); + this.cut(seq, input.seqType); return; } @@ -252,12 +252,12 @@ export default class SeqViz extends React.Component { search && (!this.props.search || search.query !== this.props.search.query || search.mismatch !== this.props.search.mismatch) ) { - // this.search(seq); // new search parameters + this.search(seq); // new search parameters } // New digest parameters. if (!isEqual(enzymes, this.props.enzymes) || !isEqual(enzymesCustom, this.props.enzymesCustom)) { - // this.cut(seq, seqType); + this.cut(seq, seqType); } // New annotations provided. @@ -372,8 +372,9 @@ export default class SeqViz extends React.Component { if (!seq) return
; if (seqType !== "dna" && seqType !== "rna" && translations && translations.length) { - // TODO: this should have a warning, I just don't want to do it in render - translations = []; + // make the entire sequence the "translation" + // TODO: during some grand future refactor, make this cleaner and more transparent to the user + translations = [{ direction: 1, end: seq.length, start: 0 }]; } // Since all the props are optional, we need to parse them to defaults. diff --git a/src/index.test.tsx b/src/index.test.tsx index 9918f5d48..311beff9a 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -77,15 +77,15 @@ describe("SeqViz rendering (React)", () => { const aaSeq = "MSKGEELFTGVVPILVELDGDVNGHKFSVSGEGEGDATYGKLTLKFICTTGKLPVPWPTLVTTFSYGVQCFSRYPDHMKQHDFFKSAMPEGYVQERTIFFKDDGNYKTRAEVKFEGDTLVNRIELKGIDFKEDGNILGHKLEYNYNSHNVYIMADKQKNGIKVNFKIRHNIEDGSVQLADHYQQNTPIGDGPVLLPDNHYLSTQSALSKDPNEKRDHMVLLEFVTAAGITHGMDELYK*"; - const { getAllByTestId, getByTestId } = render(); + const { getAllByTestId, getByTestId } = render(); await waitFor(() => expect(getAllByTestId("la-vz-seqviz-rendered")).toBeTruthy()); expect(getByTestId("la-vz-viewer-linear")).toBeTruthy(); expect(getAllByTestId("la-vz-viewer-linear")).toHaveLength(1); - const seqs = screen.getAllByTestId("la-vz-seq"); - const seq = seqs.map(s => s.textContent).join(""); - expect(seq).toEqual(aaSeq); + // const seqs = getAllByTestId("la-vz-translation"); + // const seq = seqs.map(s => s.textContent).join(""); + // expect(seq).toEqual(aaSeq); cleanup(); }); diff --git a/src/sequence.ts b/src/sequence.ts index 19afdfe33..0213d1e29 100644 --- a/src/sequence.ts +++ b/src/sequence.ts @@ -298,27 +298,31 @@ 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 createLinearTranslations = (translations: Range[], seq: string, seqType: SeqType) => { +export const createTranslations = (translations: Range[], seq: string, seqType: SeqType) => { // elongate the original sequence to account for translations that cross the zero index - const dnaDoubled = seq + seq; + const seqDoubled = seq + seq; + const bpPerBlock = seqType === "aa" ? 1 : 3; + return translations.map(t => { const { direction, start } = t; let { end } = t; if (start > end) end += seq.length; - // get the DNA sub sequence + // TODO: below will fail on an "aa" type sequence if direction = -1. At the time of writing, this won't be reached, anyway + + // get the subsequence const subSeq = - direction === 1 ? dnaDoubled.substring(start, end) : reverseComplement(dnaDoubled.substring(start, end), seqType); + direction === 1 ? seqDoubled.substring(start, end) : reverseComplement(seqDoubled.substring(start, end), seqType); - // translate the DNA sub sequence + // translate the subsequence const aaSeq = direction === 1 ? translate(subSeq, seqType) : translate(subSeq, seqType).split("").reverse().join(""); // translate // the starting point for the translation, reading left to right (regardless of translation // direction). this is later needed to calculate the number of bps needed in the first // and last codons - const tStart = direction === 1 ? start : end - aaSeq.length * 3; - let tEnd = direction === 1 ? (start + aaSeq.length * 3) % seq.length : end % seq.length; + const tStart = direction === 1 ? start : end - aaSeq.length * bpPerBlock; + let tEnd = direction === 1 ? (start + aaSeq.length * bpPerBlock) % seq.length : end % seq.length; // treating one particular edge case where the start at zero doesn't make sense if (tEnd === 0) {