From 90082117a625c1ae74a20aaf1a41a1c04f68658f Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Wed, 26 Apr 2023 18:00:38 -0700 Subject: [PATCH 01/12] implement one row linear viewer --- README.md | 2 +- demo/lib/App.tsx | 8 +- src/Linear/InfiniteHorizontalScroll.tsx | 312 ++++++++++++++++++++++++ src/Linear/Linear.tsx | 50 +++- src/Linear/SeqBlock.tsx | 21 +- src/SeqViewerContainer.tsx | 7 +- src/SeqViz.css | 18 ++ src/SeqViz.tsx | 2 +- 8 files changed, 403 insertions(+), 17 deletions(-) create mode 100644 src/Linear/InfiniteHorizontalScroll.tsx diff --git a/README.md b/README.md index 327c39ecd..4aa492b3a 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ export default () => { #### `viewer (='both')` -The type and orientation of the sequence viewers. One of `"linear" | "circular" | "both" | "both_flip"`. `both` means the circular viewer fills the left side of SeqViz, and the linear viewer fills the right. `both_flip` is the opposite: the linear viewer is on the left, and the circular viewer is on the right. +The type and orientation of the sequence viewers. One of `"linear" | "circular" | "both" | "both_flip" | "linear_one_row"`. `both` means the circular viewer fills the left side of SeqViz, and the linear viewer fills the right. `both_flip` is the opposite: the linear viewer is on the left, and the circular viewer is on the right. `linear_one_row` will render the entire linear sequence in a single row. #### `name (='')` diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index a60a37d25..e300cf6d9 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -23,6 +23,8 @@ const viewerTypeOptions = [ { key: "both", text: "Both", value: "both" }, { key: "circular", text: "Circular", value: "circular" }, { key: "linear", text: "Linear", value: "linear" }, + { key: "both_flip", text: "Both Flip", value: "both_flip" }, + { key: "linear_one_row", text: "Linear One Row", value: "linear_one_row" }, ]; interface AppState { @@ -38,7 +40,7 @@ interface AppState { showIndex: boolean; showSelectionMeta: boolean; showSidebar: boolean; - translations: { end: number; start: number; direction?: 1 | -1 }[]; + translations: { direction?: 1 | -1, end: number; start: number; }[]; viewer: string; zoom: number; } @@ -58,7 +60,7 @@ export default class App extends React.Component { showSelectionMeta: false, showSidebar: false, translations: [ - { end: 630, start: 6, direction: -1 }, + { direction: -1, end: 630, start: 6 }, { end: 1147, start: 736 }, { end: 1885, start: 1165 }, ], @@ -153,13 +155,13 @@ export default class App extends React.Component { enzymes={this.state.enzymes} name={this.state.name} search={this.state.search} + selection={this.state.selection} seq={this.state.seq} showComplement={this.state.showComplement} showIndex={this.state.showIndex} translations={this.state.translations} viewer={this.state.viewer as "linear" | "circular"} zoom={{ linear: this.state.zoom }} - selection={this.state.selection} onSelection={selection => this.setState({ selection })} /> )} diff --git a/src/Linear/InfiniteHorizontalScroll.tsx b/src/Linear/InfiniteHorizontalScroll.tsx new file mode 100644 index 000000000..79ffe58fb --- /dev/null +++ b/src/Linear/InfiniteHorizontalScroll.tsx @@ -0,0 +1,312 @@ +import * as React from "react"; + +import CentralIndexContext from "../centralIndexContext"; +import { Size } from "../elements"; +import { isEqual } from "../isEqual"; + +interface InfiniteHorizontalScrollProps { + blockWidths: number[]; + bpsPerBlock: number; + seqBlocks: JSX.Element[]; + size: Size; + totalWidth: number; +} + +interface InfiniteHorizontalScrollState { + centralIndex: number; + visibleBlocks: number[]; +} + +/** + * InfiniteHorizontalScroll is a wrapper around the seqBlocks. Renders only the seqBlocks that are + * within the range of the current dom viewerport + * + * This component should sense scroll events and, during one, recheck which sequences are shown. + */ +export class InfiniteHorizontalScroll extends React.PureComponent< + InfiniteHorizontalScrollProps, + InfiniteHorizontalScrollState +> { + static contextType = CentralIndexContext; + static context: React.ContextType; + declare context: React.ContextType; + + scroller: React.RefObject = React.createRef(); // ref to a div for scrolling + insideDOM: React.RefObject = React.createRef(); // ref to a div inside the scroller div + timeoutID; + + constructor(props: InfiniteHorizontalScrollProps) { + super(props); + + this.state = { + centralIndex: 0, + // start off with first 1 blocks shown + visibleBlocks: new Array(Math.min(1, props.seqBlocks.length)).fill(null).map((_, i) => i), + }; + } + + componentDidMount = () => { + this.handleScrollOrResize(); // ref should now be set + window.addEventListener("resize", this.handleScrollOrResize); + }; + + componentDidUpdate = ( + prevProps: InfiniteHorizontalScrollProps, + prevState: InfiniteHorizontalScrollState, + snapshot: any + ) => { + if (!this.scroller.current) { + // scroller not mounted yet + return; + } + + const { seqBlocks, size } = this.props; + const { centralIndex, visibleBlocks } = this.state; + + if (this.context && centralIndex !== this.context.linear) { + this.scrollToCentralIndex(); + } else if (!isEqual(prevProps.size, size) || seqBlocks.length !== prevProps.seqBlocks.length) { + this.handleScrollOrResize(); // reset + } else if (isEqual(prevState.visibleBlocks, visibleBlocks)) { + this.restoreSnapshot(snapshot); // something, like ORFs or index view, has changed + } + }; + + componentWillUnmount = () => { + window.removeEventListener("resize", this.handleScrollOrResize); + }; + + /** + * more info at: https://reactjs.org/docs/react-component.html#getsnapshotbeforeupdate + */ + getSnapshotBeforeUpdate = (prevProps: InfiniteHorizontalScrollProps) => { + // find the current left block + const left = this.scroller.current ? this.scroller.current.scrollLeft : 0; + + // find out 1) which block this is at the edge of the left + // and 2) how far from the left of that block we are right now + const { blockWidths } = prevProps; + let blockIndex = 0; + let accumulatedX = 0; + do { + accumulatedX += blockWidths[blockIndex]; + blockIndex += 1; + } while (accumulatedX + blockWidths[blockIndex] < left && blockIndex < blockWidths.length); + + const blockX = left - accumulatedX; // last extra distance + return { blockIndex, blockX }; + }; + + /** + * Scroll to centralIndex. Likely from circular clicking on an element + * that should then be scrolled to in linear + */ + scrollToCentralIndex = () => { + if (!this.scroller.current) { + return; + } + + const { + blockWidths, + bpsPerBlock, + seqBlocks, + size: { width }, + totalWidth, + } = this.props; + const { visibleBlocks } = this.state; + const { clientWidth, scrollWidth } = this.scroller.current; + const centralIndex = this.context.linear; + + // find the first block that contains the new central index + const centerBlockIndex = seqBlocks.findIndex( + block => block.props.firstBase <= centralIndex && block.props.firstBase + bpsPerBlock >= centralIndex + ); + + // build up the list of blocks that are visible just after this first block + let newVisibleBlocks: number[] = []; + if (scrollWidth <= clientWidth) { + newVisibleBlocks = visibleBlocks; + } else if (centerBlockIndex > -1) { + const centerBlock = seqBlocks[centerBlockIndex]; + + // create some padding to the left of the new center block + const leftAdjust = centerBlockIndex > 0 ? blockWidths[centerBlockIndex - 1] : 0; + let left = centerBlock.props.x - leftAdjust; + let right = left + width; + if (right > totalWidth) { + right = totalWidth; + left = totalWidth - width; + } + + blockWidths.reduce((total, w, i) => { + if (total >= left && total <= right) { + newVisibleBlocks.push(i); + } + return total + w; + }, 0); + + this.scroller.current.scrollLeft = centerBlock.props.x; + } + + if (!isEqual(newVisibleBlocks, visibleBlocks)) { + this.setState({ + centralIndex: centralIndex, + visibleBlocks: newVisibleBlocks, + }); + } + }; + + /** + * the component has mounted to the DOM or updated, and the window should be scrolled + * so that the central index is visible + */ + restoreSnapshot = snapshot => { + if (!this.scroller.current) { + return; + } + + const { blockWidths } = this.props; + const { blockIndex, blockX } = snapshot; + + const scrollLeft = blockWidths.slice(0, blockIndex).reduce((acc, w) => acc + w, 0) + blockX; + + this.scroller.current.scrollLeft = scrollLeft; + }; + + /** + * check whether the blocks that should be visible have changed from what's in state, + * update if so + */ + handleScrollOrResize = () => { + if (!this.scroller.current || !this.insideDOM.current) { + return; + } + + const { + blockWidths, + size: { width }, + totalWidth, + } = this.props; + const { visibleBlocks } = this.state; + + const newVisibleBlocks: number[] = []; + + let left = 0; + if (this.scroller && this.insideDOM) { + const { left: parentLeft } = this.scroller.current.getBoundingClientRect(); + const { left: childLeft } = this.insideDOM.current.getBoundingClientRect(); + left = childLeft - parentLeft; + } + + left = -left + 35; + left = Math.max(0, left); // don't go too left + left = Math.min(totalWidth - width, left); // don't go too right + const right = left + blockWidths[0]; // width; + left -= blockWidths[0]; // add one block padding on left + blockWidths.reduce((total, w, i) => { + if (total >= left && total <= right) { + newVisibleBlocks.push(i); + } + return total + w; + }, 0); + + if (!isEqual(newVisibleBlocks, visibleBlocks)) { + this.setState({ visibleBlocks: newVisibleBlocks }); + } + }; + + incrementScroller = incAmount => { + this.stopIncrementingScroller(); + this.timeoutID = setTimeout(() => { + if (!this.scroller.current) { + return; + } + + this.scroller.current.scrollLeft += incAmount; + this.incrementScroller(incAmount); + }, 5); + }; + + stopIncrementingScroller = () => { + if (this.timeoutID) { + clearTimeout(this.timeoutID); + this.timeoutID = null; + } + }; + + /** + * handleMouseOver is for detecting when the user is performing a drag event + * at the very left or the very right of DIV. If they are, this starts + * a incrementing the div's scrollLeft (ie a horizontal scroll event) that's + * terminated by the user leaving the scroll area + * + * The rate of the scrollLeft is proportional to how far from the left or the + * bottom the user is (within [-40, 0] for left, and [0, 40] for right) + */ + handleMouseOver = (e: React.MouseEvent) => { + if (!this.scroller.current) { + return; + } + + // not relevant, some other type of event, not a selection drag + if (e.buttons !== 1) { + if (this.timeoutID) { + this.stopIncrementingScroller(); + } + return; + } + + // check whether the current drag position is near the right + // of the viewer and, if it is, try and increment the current + // centralIndex (triggering a right scroll event) + const scrollerBlock = this.scroller.current.getBoundingClientRect(); + let scrollRatio = (e.clientX - scrollerBlock.left) / scrollerBlock.width; + if (scrollRatio > 0.9) { + scrollRatio = Math.min(1, scrollRatio); + let scalingRatio = scrollRatio - 0.9; + scalingRatio *= 10; + const scaledScroll = 15 * scalingRatio; + + this.incrementScroller(scaledScroll); + } else if (scrollRatio < 0.1) { + scrollRatio = 0.1 - Math.max(0, scrollRatio); + const scalingRatio = 10 * scrollRatio; + const scaledScroll = -15 * scalingRatio; + + this.incrementScroller(scaledScroll); + } else { + this.stopIncrementingScroller(); + } + }; + + render() { + const { + blockWidths, + seqBlocks, + size: { height }, + totalWidth: width, + } = this.props; + const { visibleBlocks } = this.state; + + // find the width of the empty div needed to correctly position the rest + const [firstRendered] = visibleBlocks; + const spaceLeft = blockWidths.slice(0, firstRendered).reduce((acc, w) => acc + w, 0); + return ( +
{ + // do nothing + }} + onMouseOver={this.handleMouseOver} + onScroll={this.handleScrollOrResize} + > +
+
+ {visibleBlocks.map(i => seqBlocks[i])} +
+
+ ); + } +} diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index df022dd62..d9ff9e7d1 100644 --- a/src/Linear/Linear.tsx +++ b/src/Linear/Linear.tsx @@ -5,6 +5,7 @@ import { Annotation, CutSite, Highlight, NameRange, Range, SeqType, Size } from import { createMultiRows, createSingleRows, stackElements } from "../elementsToRows"; import { isEqual } from "../isEqual"; import { createTranslations } from "../sequence"; +import { InfiniteHorizontalScroll } from "./InfiniteHorizontalScroll"; import { InfiniteScroll } from "./InfiniteScroll"; import { SeqBlock } from "./SeqBlock"; @@ -21,6 +22,7 @@ export interface LinearProps { inputRef: InputRefFunc; lineHeight: number; onUnmount: (id: string) => void; + oneRow: boolean; search: NameRange[]; seq: string; seqFontSize: number; @@ -62,12 +64,14 @@ export default class Linear extends React.Component { const { annotations, bpsPerBlock, + charWidth, compSeq, cutSites, elementHeight, highlights, lineHeight, onUnmount, + oneRow, search, seq, seqType, @@ -82,7 +86,7 @@ export default class Linear extends React.Component { const zoomed = zoom.linear > 10; // the actual fragmenting of the sequence into subblocks. generates all info that will be needed - // including sequence blocks, complement blocks, annotations, blockHeights + // including sequence blocks, complement blocks, annotations, blockHeights, blockWidths const seqLength = seq.length; let arrSize = Math.round(Math.ceil(seqLength / bpsPerBlock)); if (arrSize === Number.POSITIVE_INFINITY) arrSize = 1; @@ -91,6 +95,7 @@ export default class Linear extends React.Component { const seqs = new Array(arrSize); // arrays for sequences... const compSeqs = new Array(arrSize); // complements... const blockHeights = new Array(arrSize); // block heights... + const blockWidths = new Array(arrSize); // block widths... const cutSiteRows = cutSites.length ? createSingleRows(cutSites, bpsPerBlock, arrSize) @@ -122,6 +127,10 @@ export default class Linear extends React.Component { ? createSingleRows(createTranslations(translations, seq, seqType), bpsPerBlock, arrSize) : new Array(arrSize).fill([]); + let maxBlockHeight = 0; + let maxTranslationRowSize = 0; + let maxAnnotationRowSize = 0; + for (let i = 0; i < arrSize; i += 1) { const firstBase = i * bpsPerBlock; const lastBase = firstBase + bpsPerBlock; @@ -130,6 +139,7 @@ export default class Linear extends React.Component { seqs[i] = seq.substring(firstBase, lastBase); compSeqs[i] = compSeq.substring(firstBase, lastBase); + const blockWidth = seqs[i].length * charWidth; // store a unique id from the block ids[i] = seqs[i] + String(i); @@ -155,10 +165,27 @@ export default class Linear extends React.Component { } blockHeights[i] = blockHeight; + blockWidths[i] = blockWidth; + if (blockHeight > maxBlockHeight) { + maxBlockHeight = blockHeight; + } + if (maxTranslationRowSize < translationRows[i].length) { + maxTranslationRowSize = translationRows[i].length; + } + if (maxAnnotationRowSize < annotationRows[i].length) { + maxAnnotationRowSize = annotationRows[i].length; + } + } + + if (oneRow) { + for (let i = 0; i < arrSize; i += 1) { + blockHeights[i] = maxBlockHeight; + } } const seqBlocks: JSX.Element[] = []; let yDiff = 0; + let xDiff = 0; for (let i = 0; i < arrSize; i += 1) { const firstBase = i * bpsPerBlock; seqBlocks.push( @@ -166,9 +193,10 @@ export default class Linear extends React.Component { key={ids[i]} annotationRows={annotationRows[i]} blockHeight={blockHeights[i]} + blockWidth={blockWidths[i]} bpColors={this.props.bpColors} bpsPerBlock={bpsPerBlock} - charWidth={this.props.charWidth} + charWidth={charWidth} compSeq={compSeqs[i]} cutSiteRows={cutSiteRows[i]} elementHeight={elementHeight} @@ -179,6 +207,9 @@ export default class Linear extends React.Component { id={ids[i]} inputRef={this.props.inputRef} lineHeight={lineHeight} + maxAnnotationRowSize={maxAnnotationRowSize} + maxTranslationRowSize={maxTranslationRowSize} + oneRow={oneRow} searchRows={searchRows[i]} seq={seqs[i]} seqFontSize={this.props.seqFontSize} @@ -187,17 +218,28 @@ export default class Linear extends React.Component { showIndex={showIndex} size={size} translations={translationRows[i]} + x={xDiff} y={yDiff} zoom={zoom} zoomed={zoomed} onUnmount={onUnmount} /> ); + xDiff += blockWidths[i]; yDiff += blockHeights[i]; } return ( - seqBlocks.length && ( + seqBlocks.length > 0 && + (oneRow ? ( + acc + w, 0)} + /> + ) : ( { size={size} totalHeight={blockHeights.reduce((acc, h) => acc + h, 0)} /> - ) + )) ); } } diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index 800b41a98..cc0401a15 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -27,6 +27,7 @@ export type FindXAndWidthElementType = ( interface SeqBlockProps { annotationRows: Annotation[][]; blockHeight: number; + blockWidth: number; bpColors?: { [key: number | string]: string }; bpsPerBlock: number; charWidth: number; @@ -41,7 +42,10 @@ interface SeqBlockProps { inputRef: InputRefFunc; key: string; lineHeight: number; + maxAnnotationRowSize: number; + maxTranslationRowSize: number; onUnmount: (a: string) => void; + oneRow: boolean; searchRows: Range[]; seq: string; seqFontSize: number; @@ -50,6 +54,7 @@ interface SeqBlockProps { showIndex: boolean; size: Size; translations: Translation[]; + x: number; y: number; zoom: { linear: number }; zoomed: boolean; @@ -215,6 +220,7 @@ export class SeqBlock extends React.PureComponent { const { annotationRows, blockHeight, + blockWidth, bpsPerBlock, charWidth, compSeq, @@ -227,7 +233,10 @@ export class SeqBlock extends React.PureComponent { id, inputRef, lineHeight, + maxAnnotationRowSize, + maxTranslationRowSize, onUnmount, + oneRow, searchRows, seq, seqFontSize, @@ -254,7 +263,7 @@ export class SeqBlock extends React.PureComponent { // height and yDiff of cut sites const cutSiteYDiff = 0; // spacing for cutSite names - const cutSiteHeight = zoomed && cutSiteRows.length ? lineHeight : 0; + const cutSiteHeight = zoomed && (cutSiteRows.length || oneRow) ? lineHeight : 0; // height and yDiff of the sequence strand const indexYDiff = cutSiteYDiff + cutSiteHeight; @@ -266,14 +275,14 @@ export class SeqBlock extends React.PureComponent { // height and yDiff of translations const translationYDiff = compYDiff + compHeight; - const translationHeight = elementHeight * translations.length; + const translationHeight = elementHeight * (oneRow ? maxTranslationRowSize : translations.length); // height and yDiff of annotations const annYDiff = translationYDiff + translationHeight; - const annHeight = elementHeight * annotationRows.length; + const annHeight = elementHeight * (oneRow ? maxAnnotationRowSize : annotationRows.length); // height and ydiff of the index row. - const elementGap = annotationRows.length + translations.length ? 3 : 0; + const elementGap = annotationRows.length || translations.length || oneRow ? 3 : 0; const indexRowYDiff = annYDiff + annHeight + elementGap; // calc the height necessary for the sequence selection @@ -295,13 +304,13 @@ export class SeqBlock extends React.PureComponent { type: "SEQ", viewer: "LINEAR", })} - className="la-vz-seqblock" + className={oneRow ? "la-vz-linear-one-row-seqblock" : "la-vz-seqblock"} cursor="text" data-testid="la-vz-seqblock" display="block" height={blockHeight} id={id} - width={size.width >= 0 ? size.width : 0} + width={oneRow ? blockWidth : size.width} onMouseDown={handleMouseEvent} onMouseMove={handleMouseEvent} onMouseUp={handleMouseEvent} diff --git a/src/SeqViewerContainer.tsx b/src/SeqViewerContainer.tsx index a60379a83..2ace278a9 100644 --- a/src/SeqViewerContainer.tsx +++ b/src/SeqViewerContainer.tsx @@ -42,7 +42,7 @@ interface SeqViewerContainerProps { /** testSize is a forced height/width that overwrites anything from sizeMe. For testing */ testSize?: { height: number; width: number }; translations: Range[]; - viewer: "linear" | "circular" | "both" | "both_flip"; + viewer: "linear" | "circular" | "both" | "both_flip" | "linear_one_row"; width: number; zoom: { circular: number; linear: number }; } @@ -134,6 +134,8 @@ class SeqViewerContainer extends React.Component {/* TODO: this sucks, some breaking refactor in future should get rid of it SeqViewer */} - {viewer === "linear" && ( + {(viewer === "linear" || viewer === "linear_one_row") && ( Date: Sat, 6 May 2023 15:51:29 -0400 Subject: [PATCH 02/12] fmt --- demo/lib/App.tsx | 13 +++++++------ src/SeqViz.css | 1 - 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index 1758f7e9d..85c51cefd 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -114,7 +114,8 @@ export default class App extends React.Component {
); - } else if (this.state.viewer === "circular") { + } + else if (this.state.viewer === "circular") { return (
@@ -123,10 +124,10 @@ export default class App extends React.Component { } else if (this.state.viewer === "both") { return (
-
+
-
+
@@ -134,10 +135,10 @@ export default class App extends React.Component { } else if (this.state.viewer === "both_flip") { return (
-
+
-
+
@@ -228,7 +229,7 @@ export default class App extends React.Component { showComplement={this.state.showComplement} showIndex={this.state.showIndex} translations={this.state.translations} - viewer={this.state.viewer as "linear" | "circular"} + viewer={this.state.viewer as "linear" | "linear_one_row" | "circular"} zoom={{ linear: this.state.zoom }} onSelection={selection => this.setState({ selection })} > diff --git a/src/SeqViz.css b/src/SeqViz.css index d984cdf73..2a9f7fd51 100644 --- a/src/SeqViz.css +++ b/src/SeqViz.css @@ -200,4 +200,3 @@ display: flex; flex-direction: row; } - From b6794ffd67cdabd09b522ca93dacd7739457528e Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sat, 6 May 2023 23:55:17 -0700 Subject: [PATCH 03/12] remove jaggered edges --- demo/lib/App.tsx | 2 +- src/Linear/Annotations.tsx | 10 +++++++--- src/Linear/SeqBlock.tsx | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index 85c51cefd..4aa3f77b8 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -68,7 +68,7 @@ export default class App extends React.Component { { end: 1147, start: 736 }, { end: 1885, start: 1165 }, ], - viewer: "", + viewer: "both", zoom: 50, }; linearRef: React.RefObject = React.createRef(); diff --git a/src/Linear/Annotations.tsx b/src/Linear/Annotations.tsx index 111e26587..bf819138d 100644 --- a/src/Linear/Annotations.tsx +++ b/src/Linear/Annotations.tsx @@ -26,6 +26,7 @@ const AnnotationRows = (props: { fullSeq: string; inputRef: InputRefFunc; lastBase: number; + oneRow: boolean; seqBlockRef: unknown; width: number; yDiff: number; @@ -42,6 +43,7 @@ const AnnotationRows = (props: { height={props.elementHeight} inputRef={props.inputRef} lastBase={props.lastBase} + oneRow={props.oneRow} seqBlockRef={props.seqBlockRef} width={props.width} y={props.yDiff + props.elementHeight * i} @@ -65,6 +67,7 @@ const AnnotationRow = (props: { height: number; inputRef: InputRefFunc; lastBase: number; + oneRow: boolean; seqBlockRef: unknown; width: number; y: number; @@ -101,8 +104,9 @@ const SingleNamedElement = (props: { index: number; inputRef: InputRefFunc; lastBase: number; + oneRow: boolean; }) => { - const { element, elements, findXAndWidth, firstBase, index, inputRef, lastBase } = props; + const { element, elements, findXAndWidth, firstBase, index, inputRef, lastBase, oneRow } = props; const { color, direction, end, name, start } = element; const forward = direction === 1; @@ -129,7 +133,7 @@ const SingleNamedElement = (props: { let linePath = ""; let bottomRight = `L ${width} ${height}`; // flat right edge - if ((overflowRight && width > 2 * cW) || crossZero) { + if ((overflowRight && width > 2 * cW && !oneRow) || crossZero) { bottomRight = ` L ${width - cW} ${cH} L ${width} ${2 * cH} @@ -142,7 +146,7 @@ const SingleNamedElement = (props: { } let bottomLeft = `L 0 ${height} L 0 0`; // flat left edge - if (overflowLeft && width > 2 * cW) { + if (overflowLeft && width > 2 * cW && !oneRow) { bottomLeft = ` L 0 ${height} L ${cW} ${3 * cH} diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index cc0401a15..87927f5be 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -394,6 +394,7 @@ export class SeqBlock extends React.PureComponent { fullSeq={fullSeq} inputRef={inputRef} lastBase={lastBase} + oneRow={oneRow} seqBlockRef={this} width={size.width} yDiff={annYDiff} From 8e4f94251c51ca8bb37d7ccbbf094ceaff957cd4 Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 09:28:49 -0700 Subject: [PATCH 04/12] align rows --- src/Circular/Annotations.tsx | 4 +-- src/Circular/Circular.tsx | 2 +- src/EventHandler.tsx | 2 +- src/Linear/Annotations.tsx | 5 ++- src/Linear/InfiniteHorizontalScroll.tsx | 2 +- src/Linear/InfiniteScroll.tsx | 6 +++- src/Linear/Linear.tsx | 43 ++++++++++++++++++------- src/Linear/SeqBlock.tsx | 15 +++++---- src/Linear/Translations.tsx | 6 ++-- src/SelectionHandler.tsx | 2 +- src/SeqViewerContainer.tsx | 6 ++-- src/SeqViz.tsx | 17 ++++++---- src/colors.ts | 2 +- 13 files changed, 74 insertions(+), 38 deletions(-) diff --git a/src/Circular/Annotations.tsx b/src/Circular/Annotations.tsx index 337ff9772..6cfcdf821 100644 --- a/src/Circular/Annotations.tsx +++ b/src/Circular/Annotations.tsx @@ -49,7 +49,7 @@ export class Annotations extends React.PureComponent { {({ circular }) => ( - {annotations.reduce((acc: any[], anns: Annotation[], i) => { + {annotations.reduce((acc: React.ReactNode[], anns: Annotation[], i) => { if (i) { // increment the annRow radii on every loop after first currBRadius -= lineHeight + 3; @@ -86,7 +86,7 @@ export class Annotations extends React.PureComponent { interface SingleAnnotationProps { annotation: Annotation; - calcBorderColor: (c: any) => any; + calcBorderColor: (c: string) => string | null; centralIndex: number; currBRadius: number; currTRadius: number; diff --git a/src/Circular/Circular.tsx b/src/Circular/Circular.tsx index a70de071a..2b6169ed7 100644 --- a/src/Circular/Circular.tsx +++ b/src/Circular/Circular.tsx @@ -42,7 +42,7 @@ export interface CircularProps { center: { x: number; y: number }; compSeq: string; cutSites: CutSite[]; - handleMouseEvent: (e: any) => void; + handleMouseEvent: (e: React.MouseEvent) => void; highlights: Highlight[]; inputRef: InputRefFunc; name: string; diff --git a/src/EventHandler.tsx b/src/EventHandler.tsx index 0922212f5..71ee6ab04 100644 --- a/src/EventHandler.tsx +++ b/src/EventHandler.tsx @@ -8,7 +8,7 @@ export interface EventsHandlerProps { bpsPerBlock: number; children: React.ReactNode; copyEvent: (e: React.KeyboardEvent) => boolean; - handleMouseEvent: (e: any) => void; + handleMouseEvent: (e: React.MouseEvent) => void; selection: Selection; seq: string; setSelection: (selection: Selection) => void; diff --git a/src/Linear/Annotations.tsx b/src/Linear/Annotations.tsx index bf819138d..8af2ab970 100644 --- a/src/Linear/Annotations.tsx +++ b/src/Linear/Annotations.tsx @@ -27,6 +27,7 @@ const AnnotationRows = (props: { inputRef: InputRefFunc; lastBase: number; oneRow: boolean; + positions: Map; seqBlockRef: unknown; width: number; yDiff: number; @@ -44,6 +45,7 @@ const AnnotationRows = (props: { inputRef={props.inputRef} lastBase={props.lastBase} oneRow={props.oneRow} + positions={props.positions} seqBlockRef={props.seqBlockRef} width={props.width} y={props.yDiff + props.elementHeight * i} @@ -68,6 +70,7 @@ const AnnotationRow = (props: { inputRef: InputRefFunc; lastBase: number; oneRow: boolean; + positions: Map; seqBlockRef: unknown; width: number; y: number; @@ -84,7 +87,7 @@ const AnnotationRow = (props: { key={`annotation-linear-${a.id}-${i}-${props.firstBase}-${props.lastBase}`} element={a} elements={props.annotations} - index={i} + index={props.positions.get(a.id) as number} /> ))} diff --git a/src/Linear/InfiniteHorizontalScroll.tsx b/src/Linear/InfiniteHorizontalScroll.tsx index 79ffe58fb..38cc93b96 100644 --- a/src/Linear/InfiniteHorizontalScroll.tsx +++ b/src/Linear/InfiniteHorizontalScroll.tsx @@ -53,7 +53,7 @@ export class InfiniteHorizontalScroll extends React.PureComponent< componentDidUpdate = ( prevProps: InfiniteHorizontalScrollProps, prevState: InfiniteHorizontalScrollState, - snapshot: any + snapshot: { blockIndex: number; blockX: number } ) => { if (!this.scroller.current) { // scroller not mounted yet diff --git a/src/Linear/InfiniteScroll.tsx b/src/Linear/InfiniteScroll.tsx index 471d93c8f..c0bc477a9 100644 --- a/src/Linear/InfiniteScroll.tsx +++ b/src/Linear/InfiniteScroll.tsx @@ -47,7 +47,11 @@ export class InfiniteScroll extends React.PureComponent { + componentDidUpdate = ( + prevProps: InfiniteScrollProps, + prevState: InfiniteScrollState, + snapshot: { blockIndex: number; blockX: number } + ) => { if (!this.scroller.current) { // scroller not mounted yet return; diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index d9ff9e7d1..1dc96bf33 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, Range, SeqType, Size } from "../elements"; +import { Annotation, CutSite, Highlight, NameRange, SeqType, Size } from "../elements"; import { createMultiRows, createSingleRows, stackElements } from "../elementsToRows"; import { isEqual } from "../isEqual"; import { createTranslations } from "../sequence"; @@ -30,7 +30,7 @@ export interface LinearProps { showComplement: boolean; showIndex: boolean; size: Size; - translations: Range[]; + translations: NameRange[]; zoom: { linear: number }; } @@ -112,6 +112,33 @@ export default class Linear extends React.Component { return annotations; }; + const computeRangePositions = (ranges: NameRange[]) => { + const results = new Map(); + const sortedRanges = [...ranges]; + const curIndices = new Map(); + sortedRanges + .sort((a, b) => { + if (a.start === b.start) { + return a.end - b.end; + } + return a.start - b.start; + }) + .forEach(range => { + for (let ind = 0; ; ind++) { + const existingRange = curIndices.get(ind); + if (!existingRange || existingRange.end < range.start - 1) { + curIndices.set(ind, range); + results.set(range.id, ind); + break; + } + } + }); + return results; + }; + + const annotationPositions = computeRangePositions(annotations); + const translationPositions = computeRangePositions(translations); + const annotationRows = createMultiRows( stackElements(vetAnnotations(annotations), seq.length), bpsPerBlock, @@ -128,8 +155,6 @@ export default class Linear extends React.Component { : new Array(arrSize).fill([]); let maxBlockHeight = 0; - let maxTranslationRowSize = 0; - let maxAnnotationRowSize = 0; for (let i = 0; i < arrSize; i += 1) { const firstBase = i * bpsPerBlock; @@ -169,12 +194,6 @@ export default class Linear extends React.Component { if (blockHeight > maxBlockHeight) { maxBlockHeight = blockHeight; } - if (maxTranslationRowSize < translationRows[i].length) { - maxTranslationRowSize = translationRows[i].length; - } - if (maxAnnotationRowSize < annotationRows[i].length) { - maxAnnotationRowSize = annotationRows[i].length; - } } if (oneRow) { @@ -191,6 +210,7 @@ export default class Linear extends React.Component { seqBlocks.push( { id={ids[i]} inputRef={this.props.inputRef} lineHeight={lineHeight} - maxAnnotationRowSize={maxAnnotationRowSize} - maxTranslationRowSize={maxTranslationRowSize} oneRow={oneRow} searchRows={searchRows[i]} seq={seqs[i]} @@ -217,6 +235,7 @@ export default class Linear extends React.Component { showComplement={showComplement} showIndex={showIndex} size={size} + translationPositions={translationPositions} translations={translationRows[i]} x={xDiff} y={yDiff} diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index 87927f5be..ab503be6b 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -25,6 +25,7 @@ export type FindXAndWidthElementType = ( ) => { overflowLeft: boolean; overflowRight: boolean; width: number; x: number }; interface SeqBlockProps { + annotationPositions: Map; annotationRows: Annotation[][]; blockHeight: number; blockWidth: number; @@ -42,8 +43,6 @@ interface SeqBlockProps { inputRef: InputRefFunc; key: string; lineHeight: number; - maxAnnotationRowSize: number; - maxTranslationRowSize: number; onUnmount: (a: string) => void; oneRow: boolean; searchRows: Range[]; @@ -53,6 +52,7 @@ interface SeqBlockProps { showComplement: boolean; showIndex: boolean; size: Size; + translationPositions: Map; translations: Translation[]; x: number; y: number; @@ -218,6 +218,7 @@ export class SeqBlock extends React.PureComponent { render() { const { + annotationPositions, annotationRows, blockHeight, blockWidth, @@ -233,8 +234,6 @@ export class SeqBlock extends React.PureComponent { id, inputRef, lineHeight, - maxAnnotationRowSize, - maxTranslationRowSize, onUnmount, oneRow, searchRows, @@ -244,6 +243,7 @@ export class SeqBlock extends React.PureComponent { showComplement, showIndex, size, + translationPositions, translations, zoom, zoomed, @@ -275,11 +275,12 @@ export class SeqBlock extends React.PureComponent { // height and yDiff of translations const translationYDiff = compYDiff + compHeight; - const translationHeight = elementHeight * (oneRow ? maxTranslationRowSize : translations.length); + const translationHeight = + elementHeight * (oneRow ? Math.max(...annotationPositions.values()) + 1 : translations.length); // height and yDiff of annotations const annYDiff = translationYDiff + translationHeight; - const annHeight = elementHeight * (oneRow ? maxAnnotationRowSize : annotationRows.length); + const annHeight = elementHeight * (oneRow ? Math.max(...translationPositions.values()) + 1 : annotationRows.length); // height and ydiff of the index row. const elementGap = annotationRows.length || translations.length || oneRow ? 3 : 0; @@ -378,6 +379,7 @@ export class SeqBlock extends React.PureComponent { fullSeq={fullSeq} inputRef={inputRef} lastBase={lastBase} + positions={translationPositions} seqType={seqType} translations={translations} yDiff={translationYDiff} @@ -395,6 +397,7 @@ export class SeqBlock extends React.PureComponent { inputRef={inputRef} lastBase={lastBase} oneRow={oneRow} + positions={annotationPositions} seqBlockRef={this} width={size.width} yDiff={annYDiff} diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 620754bc7..bd8cf04b5 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -16,6 +16,7 @@ interface TranslationRowsProps { inputRef: InputRefFunc; lastBase: number; onUnmount: (a: unknown) => void; + positions: Map; seqType: SeqType; translations: Translation[]; yDiff: number; @@ -32,12 +33,13 @@ export const TranslationRows = ({ inputRef, lastBase, onUnmount, + positions, seqType, translations, yDiff, }: TranslationRowsProps) => ( - {translations.map((t, i) => ( + {translations.map((t) => ( ))} diff --git a/src/SelectionHandler.tsx b/src/SelectionHandler.tsx index 6c46f567d..b16b42951 100644 --- a/src/SelectionHandler.tsx +++ b/src/SelectionHandler.tsx @@ -6,7 +6,7 @@ interface RefSelection extends Selection { viewer: "LINEAR" | "CIRCULAR"; } -export type InputRefFunc = (id: string, ref: RefSelection) => any; +export type InputRefFunc = (id: string, ref: RefSelection) => React.LegacyRef | undefined; export type SeqVizMouseEvent = React.MouseEvent & { target: { id: string }; diff --git a/src/SeqViewerContainer.tsx b/src/SeqViewerContainer.tsx index 011cccf65..bb15cb7e7 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, Range, SeqType } from "./elements"; +import { Annotation, CutSite, Highlight, NameRange, SeqType } from "./elements"; import { isEqual } from "./isEqual"; import SelectionContext, { Selection, defaultSelection } from "./selectionContext"; @@ -56,7 +56,7 @@ interface SeqViewerContainerProps { targetRef: React.LegacyRef; /** 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" | "linear_one_row"; width: number; zoom: { circular: number; linear: number }; @@ -90,7 +90,7 @@ class SeqViewerContainer extends React.Component + shouldComponentUpdate = (nextProps: SeqViewerContainerProps, nextState: SeqViewerContainerState) => !isEqual(nextProps, this.props) || !isEqual(nextState, this.state); /** diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index 02c8491ec..b651e3239 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -391,7 +391,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, id: randomID(), name: randomID(), start: 0 }]; } // Since all the props are optional, we need to parse them to defaults. @@ -417,11 +417,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): { direction: 1 | -1; end: number; start: number } => ({ - direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, - end: t.start + Math.floor((t.end - t.start) / 3) * 3, - start: t.start % seq.length, - })), + translations: (translations || []).map( + t => + ({ + direction: t.direction ? (t.direction < 0 ? -1 : 1) : 1, + end: t.start + Math.floor((t.end - t.start) / 3) * 3, + id: randomID(), + name: randomID(), + start: t.start % seq.length, + } as NameRange) + ), viewer: this.props.viewer || "both", zoom: { circular: typeof zoom?.circular == "number" ? Math.min(Math.max(zoom.circular, 0), 100) : 0, diff --git a/src/colors.ts b/src/colors.ts index 73662d5dc..3c3508190 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -74,7 +74,7 @@ export const borderColorByIndex = (i: number) => COLOR_BORDER_MAP[COLORS[i % COL const darkerColorCache = {}; /** darken a HEX color by 50% */ -export const darkerColor = c => { +export const darkerColor = (c: string): string | null => { if (darkerColorCache[c]) { return darkerColorCache[c]; } From cd6e8ce84fc9ce8bda1dbc8aba3b48f2da9afff5 Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 09:41:08 -0700 Subject: [PATCH 05/12] fix tsc errors --- demo/lib/App.tsx | 2 +- src/SelectionHandler.tsx | 3 ++- src/SeqViz.tsx | 2 +- tsconfig.json | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index 4aa3f77b8..cbf9189b8 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -43,7 +43,7 @@ interface AppState { showIndex: boolean; showSelectionMeta: boolean; showSidebar: boolean; - translations: { direction?: 1 | -1, end: number; start: number; }[]; + translations: { direction?: 1 | -1; end: number; start: number; }[]; viewer: string; zoom: number; } diff --git a/src/SelectionHandler.tsx b/src/SelectionHandler.tsx index b16b42951..8d83eea5a 100644 --- a/src/SelectionHandler.tsx +++ b/src/SelectionHandler.tsx @@ -6,7 +6,7 @@ interface RefSelection extends Selection { viewer: "LINEAR" | "CIRCULAR"; } -export type InputRefFunc = (id: string, ref: RefSelection) => React.LegacyRef | undefined; +export type InputRefFunc = (id: string, ref: RefSelection) => React.LegacyRef | undefined; export type SeqVizMouseEvent = React.MouseEvent & { target: { id: string }; @@ -96,6 +96,7 @@ export default class SelectionHandler extends React.PureComponent { this.idToRange.set(ref, { ref, ...selectRange }); + return undefined; }; /** diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index b651e3239..e680c409f 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -391,7 +391,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, id: randomID(), name: randomID(), start: 0 }]; + translations = [{ direction: 1, end: seq.length, start: 0 }]; } // Since all the props are optional, we need to parse them to defaults. diff --git a/tsconfig.json b/tsconfig.json index 0cd5728ab..3207750d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "allowJs": false, // allow a partial TypeScript and JavaScript codebase "allowUnreachableCode": false, "declaration": true, + "downlevelIteration": true, "jsx": "react", // use typescript to transpile jsx to js "lib": ["es2015", "dom"], // https://marcobotto.com/blog/compiling-and-bundling-typescript-libraries-with-webpack/ "module": "commonjs", // specify module code generation From 5e0fe56579b958d2adefc23adb2da3e23d8b196e Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 10:44:34 -0700 Subject: [PATCH 06/12] fix linear heights --- src/Linear/Annotations.tsx | 9 +++++---- src/Linear/Linear.tsx | 11 ----------- src/Linear/SeqBlock.tsx | 10 ++++++---- src/Linear/Translations.tsx | 6 ++++-- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/Linear/Annotations.tsx b/src/Linear/Annotations.tsx index 8af2ab970..7674a2865 100644 --- a/src/Linear/Annotations.tsx +++ b/src/Linear/Annotations.tsx @@ -45,10 +45,12 @@ const AnnotationRows = (props: { inputRef={props.inputRef} lastBase={props.lastBase} oneRow={props.oneRow} - positions={props.positions} seqBlockRef={props.seqBlockRef} width={props.width} - y={props.yDiff + props.elementHeight * i} + y={ + props.yDiff + + props.elementHeight * (props.oneRow ? Math.max(...anns.map(ann => props.positions.get(ann.id) as number)) : i) + } /> ))} @@ -70,7 +72,6 @@ const AnnotationRow = (props: { inputRef: InputRefFunc; lastBase: number; oneRow: boolean; - positions: Map; seqBlockRef: unknown; width: number; y: number; @@ -87,7 +88,7 @@ const AnnotationRow = (props: { key={`annotation-linear-${a.id}-${i}-${props.firstBase}-${props.lastBase}`} element={a} elements={props.annotations} - index={props.positions.get(a.id) as number} + index={i} /> ))} diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index 1dc96bf33..434ecc9dd 100644 --- a/src/Linear/Linear.tsx +++ b/src/Linear/Linear.tsx @@ -154,8 +154,6 @@ export default class Linear extends React.Component { ? createSingleRows(createTranslations(translations, seq, seqType), bpsPerBlock, arrSize) : new Array(arrSize).fill([]); - let maxBlockHeight = 0; - for (let i = 0; i < arrSize; i += 1) { const firstBase = i * bpsPerBlock; const lastBase = firstBase + bpsPerBlock; @@ -191,15 +189,6 @@ export default class Linear extends React.Component { blockHeights[i] = blockHeight; blockWidths[i] = blockWidth; - if (blockHeight > maxBlockHeight) { - maxBlockHeight = blockHeight; - } - } - - if (oneRow) { - for (let i = 0; i < arrSize; i += 1) { - blockHeights[i] = maxBlockHeight; - } } const seqBlocks: JSX.Element[] = []; diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index ab503be6b..897433c35 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -275,15 +275,16 @@ export class SeqBlock extends React.PureComponent { // height and yDiff of translations const translationYDiff = compYDiff + compHeight; - const translationHeight = - elementHeight * (oneRow ? Math.max(...annotationPositions.values()) + 1 : translations.length); + const maxStackedTranslations = translationPositions.size ? Math.max(...translationPositions.values()) + 1 : 0; + const translationHeight = elementHeight * (oneRow ? maxStackedTranslations : translations.length); // height and yDiff of annotations const annYDiff = translationYDiff + translationHeight; - const annHeight = elementHeight * (oneRow ? Math.max(...translationPositions.values()) + 1 : annotationRows.length); + const maxStackedAnnotations = annotationPositions.size ? Math.max(...annotationPositions.values()) + 1 : 0; + const annHeight = elementHeight * (oneRow ? maxStackedAnnotations : annotationRows.length); // height and ydiff of the index row. - const elementGap = annotationRows.length || translations.length || oneRow ? 3 : 0; + const elementGap = translationHeight || annHeight ? 3 : 0; const indexRowYDiff = annYDiff + annHeight + elementGap; // calc the height necessary for the sequence selection @@ -379,6 +380,7 @@ export class SeqBlock extends React.PureComponent { fullSeq={fullSeq} inputRef={inputRef} lastBase={lastBase} + oneRow={oneRow} positions={translationPositions} seqType={seqType} translations={translations} diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index bd8cf04b5..e8538729e 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -16,6 +16,7 @@ interface TranslationRowsProps { inputRef: InputRefFunc; lastBase: number; onUnmount: (a: unknown) => void; + oneRow: boolean; positions: Map; seqType: SeqType; translations: Translation[]; @@ -33,13 +34,14 @@ export const TranslationRows = ({ inputRef, lastBase, onUnmount, + oneRow, positions, seqType, translations, yDiff, }: TranslationRowsProps) => ( - {translations.map((t) => ( + {translations.map((t, i) => ( ))} From 3b932334bee5d095cda0c0f134c53b117e2c9b34 Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 10:51:53 -0700 Subject: [PATCH 07/12] fix test --- src/Linear/SeqBlock.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Linear/SeqBlock.test.tsx b/src/Linear/SeqBlock.test.tsx index 1ca82baee..725493f54 100644 --- a/src/Linear/SeqBlock.test.tsx +++ b/src/Linear/SeqBlock.test.tsx @@ -6,6 +6,7 @@ import * as React from "react"; import { SeqBlock } from "./SeqBlock"; const defaultProps = { + annotationPositions: new Map(), blockHeight: 40, bpsPerBlock: 100, charWidth: 12, @@ -31,6 +32,7 @@ const defaultProps = { showComplement: true, showIndex: true, size: { height: 600, width: 1200 }, + translationPositions: new Map(), translations: [], y: 0, zoom: { linear: 50 }, From 694857b989e3ce6e1cc3312fd99c42451e842285 Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 11:44:00 -0700 Subject: [PATCH 08/12] simplify stacked calcs --- src/Linear/Annotations.tsx | 6 +++--- src/Linear/Linear.tsx | 33 +++++---------------------------- src/Linear/SeqBlock.test.tsx | 4 ++-- src/Linear/SeqBlock.tsx | 18 ++++++++---------- src/Linear/Translations.tsx | 8 ++++---- 5 files changed, 22 insertions(+), 47 deletions(-) diff --git a/src/Linear/Annotations.tsx b/src/Linear/Annotations.tsx index 7674a2865..d33bd51e1 100644 --- a/src/Linear/Annotations.tsx +++ b/src/Linear/Annotations.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { InputRefFunc } from "../SelectionHandler"; import { COLOR_BORDER_MAP, darkerColor } from "../colors"; -import { NameRange } from "../elements"; +import { Annotation, NameRange } from "../elements"; import { FindXAndWidthElementType } from "./SeqBlock"; const hoverOtherAnnotationRows = (className: string, opacity: number) => { @@ -27,8 +27,8 @@ const AnnotationRows = (props: { inputRef: InputRefFunc; lastBase: number; oneRow: boolean; - positions: Map; seqBlockRef: unknown; + stackedPositions: Annotation[][]; width: number; yDiff: number; }) => ( @@ -49,7 +49,7 @@ const AnnotationRows = (props: { width={props.width} y={ props.yDiff + - props.elementHeight * (props.oneRow ? Math.max(...anns.map(ann => props.positions.get(ann.id) as number)) : i) + props.elementHeight * (props.oneRow ? Math.max(...anns.map(ann => props.stackedPositions.findIndex( row => row.some(item => item.id === ann.id)) as number)) : i) } /> ))} diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index 434ecc9dd..216f1b221 100644 --- a/src/Linear/Linear.tsx +++ b/src/Linear/Linear.tsx @@ -112,32 +112,8 @@ export default class Linear extends React.Component { return annotations; }; - const computeRangePositions = (ranges: NameRange[]) => { - const results = new Map(); - const sortedRanges = [...ranges]; - const curIndices = new Map(); - sortedRanges - .sort((a, b) => { - if (a.start === b.start) { - return a.end - b.end; - } - return a.start - b.start; - }) - .forEach(range => { - for (let ind = 0; ; ind++) { - const existingRange = curIndices.get(ind); - if (!existingRange || existingRange.end < range.start - 1) { - curIndices.set(ind, range); - results.set(range.id, ind); - break; - } - } - }); - return results; - }; - - const annotationPositions = computeRangePositions(annotations); - const translationPositions = computeRangePositions(translations); + const stackedAnnotations = stackElements(vetAnnotations(annotations), seq.length); + const stackedTranslations = stackElements(translations, seq.length); const annotationRows = createMultiRows( stackElements(vetAnnotations(annotations), seq.length), @@ -150,6 +126,7 @@ export default class Linear extends React.Component { const highlightRows = createSingleRows(highlights, bpsPerBlock, arrSize); + // TODO: this should also use createMultiRows const translationRows = translations.length ? createSingleRows(createTranslations(translations, seq, seqType), bpsPerBlock, arrSize) : new Array(arrSize).fill([]); @@ -199,7 +176,6 @@ export default class Linear extends React.Component { seqBlocks.push( { showComplement={showComplement} showIndex={showIndex} size={size} - translationPositions={translationPositions} + stackedAnnotations={stackedAnnotations} + stackedTranslations={stackedTranslations} translations={translationRows[i]} x={xDiff} y={yDiff} diff --git a/src/Linear/SeqBlock.test.tsx b/src/Linear/SeqBlock.test.tsx index 725493f54..1d9ea8ca5 100644 --- a/src/Linear/SeqBlock.test.tsx +++ b/src/Linear/SeqBlock.test.tsx @@ -6,7 +6,6 @@ import * as React from "react"; import { SeqBlock } from "./SeqBlock"; const defaultProps = { - annotationPositions: new Map(), blockHeight: 40, bpsPerBlock: 100, charWidth: 12, @@ -32,7 +31,8 @@ const defaultProps = { showComplement: true, showIndex: true, size: { height: 600, width: 1200 }, - translationPositions: new Map(), + stackedAnnotations: [], + stackedTranslations: [], translations: [], y: 0, zoom: { linear: 50 }, diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index 897433c35..214bc5e86 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -25,7 +25,6 @@ export type FindXAndWidthElementType = ( ) => { overflowLeft: boolean; overflowRight: boolean; width: number; x: number }; interface SeqBlockProps { - annotationPositions: Map; annotationRows: Annotation[][]; blockHeight: number; blockWidth: number; @@ -52,7 +51,8 @@ interface SeqBlockProps { showComplement: boolean; showIndex: boolean; size: Size; - translationPositions: Map; + stackedAnnotations: Annotation[][]; + stackedTranslations: NameRange[][]; translations: Translation[]; x: number; y: number; @@ -218,7 +218,6 @@ export class SeqBlock extends React.PureComponent { render() { const { - annotationPositions, annotationRows, blockHeight, blockWidth, @@ -243,7 +242,8 @@ export class SeqBlock extends React.PureComponent { showComplement, showIndex, size, - translationPositions, + stackedAnnotations, + stackedTranslations, translations, zoom, zoomed, @@ -275,13 +275,11 @@ export class SeqBlock extends React.PureComponent { // height and yDiff of translations const translationYDiff = compYDiff + compHeight; - const maxStackedTranslations = translationPositions.size ? Math.max(...translationPositions.values()) + 1 : 0; - const translationHeight = elementHeight * (oneRow ? maxStackedTranslations : translations.length); + const translationHeight = elementHeight * (oneRow ? stackedTranslations.length : translations.length); // height and yDiff of annotations const annYDiff = translationYDiff + translationHeight; - const maxStackedAnnotations = annotationPositions.size ? Math.max(...annotationPositions.values()) + 1 : 0; - const annHeight = elementHeight * (oneRow ? maxStackedAnnotations : annotationRows.length); + const annHeight = elementHeight * (oneRow ? stackedAnnotations.length : annotationRows.length); // height and ydiff of the index row. const elementGap = translationHeight || annHeight ? 3 : 0; @@ -381,8 +379,8 @@ export class SeqBlock extends React.PureComponent { inputRef={inputRef} lastBase={lastBase} oneRow={oneRow} - positions={translationPositions} seqType={seqType} + stackedPositions={stackedTranslations} translations={translations} yDiff={translationYDiff} onUnmount={onUnmount} @@ -399,8 +397,8 @@ export class SeqBlock extends React.PureComponent { inputRef={inputRef} lastBase={lastBase} oneRow={oneRow} - positions={annotationPositions} seqBlockRef={this} + stackedPositions={stackedAnnotations} width={size.width} yDiff={annYDiff} /> diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index e8538729e..47a183ff0 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -2,7 +2,7 @@ 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 { FindXAndWidthType } from "./SeqBlock"; @@ -17,8 +17,8 @@ interface TranslationRowsProps { lastBase: number; onUnmount: (a: unknown) => void; oneRow: boolean; - positions: Map; seqType: SeqType; + stackedPositions: NameRange[][]; translations: Translation[]; yDiff: number; } @@ -35,8 +35,8 @@ export const TranslationRows = ({ lastBase, onUnmount, oneRow, - positions, seqType, + stackedPositions, translations, yDiff, }: TranslationRowsProps) => ( @@ -55,7 +55,7 @@ export const TranslationRows = ({ lastBase={lastBase} seqType={seqType} translation={t} - y={yDiff + elementHeight * (oneRow ? (positions.get(t.id) as number) : i)} + y={yDiff + elementHeight * (oneRow ? (stackedPositions.findIndex(rows => rows.some(item => item.id === t.id)) as number) : i)} onUnmount={onUnmount} /> ))} From 1a8eb6350f505744ec4006af12067a2a6671c5ab Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 11:57:02 -0700 Subject: [PATCH 09/12] fix lint --- src/Circular/Labels.tsx | 2 +- src/Linear/Annotations.tsx | 9 ++++++++- src/Linear/Translations.tsx | 6 +++++- src/SeqViz.tsx | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Circular/Labels.tsx b/src/Circular/Labels.tsx index 157387a53..dc5f18e32 100644 --- a/src/Circular/Labels.tsx +++ b/src/Circular/Labels.tsx @@ -18,7 +18,7 @@ export interface GroupedLabelsWithCoors { labels: ILabel[]; lineCoor: Coor; name: string; - overflow: unknown; + overflow: boolean; textAnchor: "start" | "end"; textCoor: Coor; } diff --git a/src/Linear/Annotations.tsx b/src/Linear/Annotations.tsx index d33bd51e1..78ef9a6b7 100644 --- a/src/Linear/Annotations.tsx +++ b/src/Linear/Annotations.tsx @@ -49,7 +49,14 @@ const AnnotationRows = (props: { width={props.width} y={ props.yDiff + - props.elementHeight * (props.oneRow ? Math.max(...anns.map(ann => props.stackedPositions.findIndex( row => row.some(item => item.id === ann.id)) as number)) : i) + props.elementHeight * + (props.oneRow + ? Math.max( + ...anns.map( + ann => props.stackedPositions.findIndex(row => row.some(item => item.id === ann.id)) as number + ) + ) + : i) } /> ))} diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 47a183ff0..fcab04076 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -55,7 +55,11 @@ export const TranslationRows = ({ lastBase={lastBase} seqType={seqType} translation={t} - y={yDiff + elementHeight * (oneRow ? (stackedPositions.findIndex(rows => rows.some(item => item.id === t.id)) as number) : i)} + y={ + yDiff + + elementHeight * + (oneRow ? (stackedPositions.findIndex(rows => rows.some(item => item.id === t.id)) as number) : i) + } onUnmount={onUnmount} /> ))} diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index e680c409f..4d50a3c1e 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -134,7 +134,7 @@ export interface SeqVizProps { showIndex?: boolean; /** extra style props to apply to the outermost div of SeqViz */ - style?: Record; + style?: React.CSSProperties; /** ranges of sequence that should have amino acid translations shown */ translations?: { direction?: number; end: number; start: number }[]; From b787684f3d46b14cbff31c406fd2cab407f90e00 Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 21:49:16 -0700 Subject: [PATCH 10/12] fix merge --- demo/lib/App.tsx | 4 ++-- src/Linear/InfiniteHorizontalScroll.tsx | 8 +++++++- src/Linear/Linear.tsx | 5 ++--- src/Linear/SeqBlock.tsx | 10 +++++----- src/Linear/Translations.tsx | 16 ++++++++++++++-- src/style.ts | 15 +++++++++++++++ 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index 9fa3e798b..bd7860346 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -217,9 +217,8 @@ export default class App extends React.Component { key={`${this.state.viewer}${this.state.customChildren}`} annotations={this.state.annotations} enzymes={this.state.enzymes} - highlights={[{ start: 0, end: 10 }]} + highlights={[{ end: 10, start: 0 }]} name={this.state.name} - onSelection={selection => this.setState({ selection })} refs={{ circular: this.circularRef, linear: this.linearRef }} search={this.state.search} selection={this.state.selection} @@ -229,6 +228,7 @@ export default class App extends React.Component { translations={this.state.translations} viewer={this.state.viewer as "linear" | "linear_one_row" | "circular"} zoom={{ linear: this.state.zoom }} + onSelection={selection => this.setState({ selection })} > {customChildren} diff --git a/src/Linear/InfiniteHorizontalScroll.tsx b/src/Linear/InfiniteHorizontalScroll.tsx index 38cc93b96..2b9f38808 100644 --- a/src/Linear/InfiniteHorizontalScroll.tsx +++ b/src/Linear/InfiniteHorizontalScroll.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import CentralIndexContext from "../centralIndexContext"; import { Size } from "../elements"; import { isEqual } from "../isEqual"; +import { linearOneRowScroller } from "../style"; interface InfiniteHorizontalScrollProps { blockWidths: number[]; @@ -296,13 +297,18 @@ export class InfiniteHorizontalScroll extends React.PureComponent< ref={this.scroller} className="la-vz-linear-one-row-scroller" data-testid="la-vz-viewer-linear" + style={linearOneRowScroller} onFocus={() => { // do nothing }} onMouseOver={this.handleMouseOver} onScroll={this.handleScrollOrResize} > -
+
{visibleBlocks.map(i => seqBlocks[i])}
diff --git a/src/Linear/Linear.tsx b/src/Linear/Linear.tsx index 286d7f494..106384bbb 100644 --- a/src/Linear/Linear.tsx +++ b/src/Linear/Linear.tsx @@ -113,7 +113,7 @@ export default class Linear extends React.Component { }; const stackedAnnotations = stackElements(vetAnnotations(annotations), seq.length); - const stackedTranslations = stackElements(translations, seq.length); + const stackedTranslations = stackElements(createTranslations(translations, seq, seqType), seq.length); const annotationRows = createMultiRows( stackElements(vetAnnotations(annotations), seq.length), @@ -126,9 +126,8 @@ export default class Linear extends React.Component { const highlightRows = createSingleRows(highlights, bpsPerBlock, arrSize); - // TODO: this should also use createMultiRows const translationRows = translations.length - ? createMultiRows(stackElements(createTranslations(translations, seq, seqType), seq.length), bpsPerBlock, arrSize) + ? createMultiRows(stackedTranslations, bpsPerBlock, arrSize) : new Array(arrSize).fill([]); for (let i = 0; i < arrSize; i += 1) { diff --git a/src/Linear/SeqBlock.tsx b/src/Linear/SeqBlock.tsx index 87279852d..2131fd8ae 100644 --- a/src/Linear/SeqBlock.tsx +++ b/src/Linear/SeqBlock.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { InputRefFunc } from "../SelectionHandler"; import { Annotation, CutSite, Highlight, NameRange, Range, SeqType, Size, Translation } from "../elements"; -import { seqBlock, svgText } from "../style"; +import { linearOneRowSeqBlock, seqBlock, svgText } from "../style"; import AnnotationRows from "./Annotations"; import { CutSites } from "./CutSites"; import Find from "./Find"; @@ -283,7 +283,7 @@ export class SeqBlock extends React.PureComponent { const annHeight = elementHeight * (oneRow ? stackedAnnotations.length : annotationRows.length); // height and yDiff of the index row. - const elementGap = annotationRows.length + translationRows.length ? 3 : 0; + const elementGap = translationHeight || annHeight ? 3 : 0; const indexRowYDiff = annYDiff + annHeight + elementGap; // calc the height necessary for the sequence selection @@ -305,15 +305,15 @@ export class SeqBlock extends React.PureComponent { type: "SEQ", viewer: "LINEAR", })} - className={oneRow ? "la-vz-linear-one-row-seqblock" : "la-vz-seqblock"} + className="la-vz-seqblock" cursor="text" data-testid="la-vz-seqblock" display="block" height={blockHeight} id={id} overflow="visible" - style={seqBlock} - width={size.width >= 0 ? size.width : 0} + style={oneRow ? linearOneRowSeqBlock : seqBlock} + width={oneRow ? blockWidth : size.width} onMouseDown={handleMouseEvent} onMouseMove={handleMouseEvent} onMouseUp={handleMouseEvent} diff --git a/src/Linear/Translations.tsx b/src/Linear/Translations.tsx index 309b0bc1c..434997d6b 100644 --- a/src/Linear/Translations.tsx +++ b/src/Linear/Translations.tsx @@ -35,14 +35,16 @@ export const TranslationRows = ({ inputRef, lastBase, onUnmount, + oneRow, seqType, + stackedPositions, translationRows, yDiff, }: TranslationRowsProps) => ( {translationRows.map((translations, i) => ( stackedPositions.findIndex(row => row.some(item => item.id === t.id)) as number + ) + ) + : i) + } onUnmount={onUnmount} /> ))} diff --git a/src/style.ts b/src/style.ts index abb0a84e2..1b803215a 100644 --- a/src/style.ts +++ b/src/style.ts @@ -151,3 +151,18 @@ export const seqBlock: CSS.Properties = { padding: 0, width: "100%", }; + +export const linearOneRowScroller: CSS.Properties = { + cursor: "text", + fontWeight: 300, + height: "100%", + outline: "none !important", + overflowX: "auto", + overflowY: "hidden", + padding: "10", + position: "relative", +}; + +export const linearOneRowSeqBlock: CSS.Properties = { + padding: 0, +}; From 45952e48bd29eec0d24b13ccd4e6fe9330b4ab73 Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Sun, 7 May 2023 22:06:21 -0700 Subject: [PATCH 11/12] fix padding --- src/style.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/style.ts b/src/style.ts index 1b803215a..8989e0a78 100644 --- a/src/style.ts +++ b/src/style.ts @@ -142,7 +142,7 @@ export const linearScroller: CSS.Properties = { outline: "none !important", overflowX: "hidden", overflowY: "scroll", - padding: "10", + padding: "10px", position: "relative", }; @@ -159,7 +159,7 @@ export const linearOneRowScroller: CSS.Properties = { outline: "none !important", overflowX: "auto", overflowY: "hidden", - padding: "10", + padding: "10px", position: "relative", }; From 47334572afc9e190a45fd54a2f45f6f7b7ea245d Mon Sep 17 00:00:00 2001 From: Jack Zhao Date: Thu, 15 Jun 2023 04:43:19 +0000 Subject: [PATCH 12/12] minor fixes --- src/Linear/InfiniteHorizontalScroll.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Linear/InfiniteHorizontalScroll.tsx b/src/Linear/InfiniteHorizontalScroll.tsx index 2b9f38808..909e0ca05 100644 --- a/src/Linear/InfiniteHorizontalScroll.tsx +++ b/src/Linear/InfiniteHorizontalScroll.tsx @@ -149,7 +149,7 @@ export class InfiniteHorizontalScroll extends React.PureComponent< this.scroller.current.scrollLeft = centerBlock.props.x; } - if (!isEqual(newVisibleBlocks, visibleBlocks)) { + if (newVisibleBlocks.length && !isEqual(newVisibleBlocks, visibleBlocks)) { this.setState({ centralIndex: centralIndex, visibleBlocks: newVisibleBlocks, @@ -307,7 +307,7 @@ export class InfiniteHorizontalScroll extends React.PureComponent<
{visibleBlocks.map(i => seqBlocks[i])}