From e6794f12a170eb074af9e1b0c72b69422a4f0275 Mon Sep 17 00:00:00 2001 From: isaacguerreir Date: Tue, 12 Dec 2023 20:00:27 -0300 Subject: [PATCH] linear map index component created --- demo/lib/App.tsx | 21 +++++++-- src/LinearMap/Index.tsx | 57 +++++++++++++++++++++++ src/LinearMap/LinearMap.tsx | 0 src/LinearMap/Ruler.tsx | 30 +++++++++++++ src/LinearMap/Tick.tsx | 72 ++++++++++++++++++++++++++++++ src/LinearMap/hooks/useBuilder.tsx | 18 ++++++++ 6 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 src/LinearMap/Index.tsx create mode 100644 src/LinearMap/LinearMap.tsx create mode 100644 src/LinearMap/Ruler.tsx create mode 100644 src/LinearMap/Tick.tsx create mode 100644 src/LinearMap/hooks/useBuilder.tsx diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index c787f118b..d6262bf76 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -22,11 +22,24 @@ import { AnnotationProp, Primer } from "../../src/elements"; import Header from "./Header"; import file from "./file"; +enum ViewerTypeOptions { + LINEAR="LINEAR", + CIRCULAR="CIRCULAR", + LINEAR_MAP="LINEAR_MAP", + BOTH_CIRCULAR="BOTH_CIRCULAR", + BOTH_FLIP_CIRCULAR="BOTH_FLIP_CIRCULAR", + BOTH_LINEAR_MAP="BOTH_LINEAR_MAP", + BOTH_FLIP_LINEAR_MAP="BOTH_FLIP_LINEAR_MAP" +} + 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: ViewerTypeOptions.CIRCULAR, text: "Circular", value: ViewerTypeOptions.CIRCULAR }, + { key: ViewerTypeOptions.LINEAR, text: "Linear", value: ViewerTypeOptions.LINEAR }, + { key: ViewerTypeOptions.LINEAR_MAP, text: "Linear Map", value: ViewerTypeOptions.LINEAR_MAP }, + { key: ViewerTypeOptions.BOTH_CIRCULAR, text: "Circular + Linear", value: ViewerTypeOptions.BOTH_CIRCULAR }, + { key: ViewerTypeOptions.BOTH_FLIP_CIRCULAR, text: "Linear + Circular", value: ViewerTypeOptions.BOTH_FLIP_CIRCULAR }, + { key: ViewerTypeOptions.BOTH_LINEAR_MAP, text: "Linear Map + Linear", value: ViewerTypeOptions.BOTH_LINEAR_MAP }, + { key: ViewerTypeOptions.BOTH_FLIP_LINEAR_MAP, text: "Linear + Linear Map", value: ViewerTypeOptions.BOTH_FLIP_LINEAR_MAP } ]; interface AppState { diff --git a/src/LinearMap/Index.tsx b/src/LinearMap/Index.tsx new file mode 100644 index 000000000..3837e5165 --- /dev/null +++ b/src/LinearMap/Index.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { indexLine, indexTick, indexTickLabel } from "../style"; +import { useBuilder } from "./hooks/useBuilder"; +import { Tick } from "./Tick"; + + +/** + * The Index component renders the Linear Map's: + * 1. name (center or bottom) + * 2. number of bps (center or bottom) + * 3. index ticks and numbers along the Linear Map + */ + +export interface IndexProps { + height: number + width: number + seqLength: number +} + +export function Index(props: IndexProps) { + const { ruler, ticks } = useBuilder(props) + + return ( + + + {/* The ticks and their index labels */} + {ticks.map((tick: Tick) => ( + + + + {tick.position} + + + )) + } + + {/* The ruler is abstract line representing sequence length and giving relative references for other elements as Annotations, Cut sites, Primers, etc. */} + + + + + ); +} diff --git a/src/LinearMap/LinearMap.tsx b/src/LinearMap/LinearMap.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/LinearMap/Ruler.tsx b/src/LinearMap/Ruler.tsx new file mode 100644 index 000000000..32871b44a --- /dev/null +++ b/src/LinearMap/Ruler.tsx @@ -0,0 +1,30 @@ +export class Ruler { + readonly LATERAL_PADDING = 10 + + height: number + width: number + seqLength: number + start: number + end: number + padByBp: number + + constructor( + height: number, + width: number, + seqLength: number + ) { + this.height = height/2 + this.start = this.LATERAL_PADDING + this.end = width - this.LATERAL_PADDING + this.width = this.end - this.start + this.seqLength = seqLength + this.padByBp = this.width / seqLength + } + + get path() { + return ` + M ${this.start} ${this.height} + L ${this.end} ${this.height} + ` + } +} diff --git a/src/LinearMap/Tick.tsx b/src/LinearMap/Tick.tsx new file mode 100644 index 000000000..4c972e9af --- /dev/null +++ b/src/LinearMap/Tick.tsx @@ -0,0 +1,72 @@ +import { Ruler } from "./Ruler" + +export class RulerTicks { + private readonly HORIZONTAL_TICK_LENGTH = 10 + private readonly TICK_COUNT = 6 + private ruler: Ruler + + constructor(ruler: Ruler) { + this.ruler = ruler + } + + private getPadByPosition(bpPosition: number) { + return this.ruler.start + this.ruler.padByBp * bpPosition + } + + private getTickPath(position: number) { + const pad = this.getPadByPosition(position) + return ` + M ${pad} ${this.ruler.height} + L ${pad} ${this.ruler.height + this.HORIZONTAL_TICK_LENGTH} + ` + } + + private getTickText(rightPad: number) { + const TICK_TEXT_LINE = this.HORIZONTAL_TICK_LENGTH * 2 + const textLineHeight = this.ruler.height + TICK_TEXT_LINE + return { + x: rightPad, + y: textLineHeight + } + } + + private getTick(position: number): Tick { + const rightPad = this.getPadByPosition(position) + return { + position, + path: this.getTickPath(position), + text: this.getTickText(rightPad) + } + } + + private buildTicks(positions: number[]) { + return positions.map(position => this.getTick(position)) + } + + get positions(): number[] { + const increments = Math.floor(this.ruler.seqLength / this.TICK_COUNT); + let indexInc = Math.max(+increments.toPrecision(2), 10); + while (indexInc % 10 !== 0) indexInc += 1; + // + let ticks: number[] = []; + for (let i = indexInc; i <= this.ruler.seqLength - indexInc; i += indexInc) { + ticks.push(i === 0 ? 1 : i); + } + + return ticks + } + + get ticks(): Tick[] { + return this.buildTicks(this.positions) + } +} + + +export interface Tick { + position: number + path: string + text: { + x: number + y: number + } +} diff --git a/src/LinearMap/hooks/useBuilder.tsx b/src/LinearMap/hooks/useBuilder.tsx new file mode 100644 index 000000000..bebc39c73 --- /dev/null +++ b/src/LinearMap/hooks/useBuilder.tsx @@ -0,0 +1,18 @@ +import { useMemo } from "react" +import { IndexProps } from "../Index" +import { Ruler } from "../Ruler" +import { RulerTicks } from "../Tick" + +type BuilderProps = IndexProps +export function useBuilder({ height, width, seqLength }: BuilderProps) { + const { ruler, ticks } = useMemo(() => { + const ruler = new Ruler(height, width, seqLength) + const ticks = new RulerTicks(ruler).ticks + return { + ruler, + ticks + } + }, [height, width, seqLength]) + + return { ruler, ticks } +}