Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | 2x 2x 63x 63x 63x 63x 2x 343x 343x 2x 52x 52x 52x 52x 52x 52x 52x 2x 28x 28x 28x 28x 28x 28x 28x 28x 28x 28x 2x 23x 23x 23x 2x 241x 241x 241x 2x 7x 7x 7x 7x 7x 7x 2x 3x 3x 3x 2x 52x 52x 2x 9x 9x 9x 9x 9x 9x 9x 18x 7x 7x 7x 7x 18x 18x 18x 5x 5x 5x 18x 9x 9x 2x 14x 12x 12x 14x 11x 11x 9x 11x 12x 12x | /**
* The dot plot's view of its own (SPECS §28.2), as arithmetic: a window over
* the two sets' concatenated axes, in bases, at one scale on both — so a
* collinear stretch is a line at 45° whatever the two sets' sizes — zoomed
* about the pointer and moved by the hand; where a pixel is, in bases and as a
* locus of each set; and the streak the pointer is on.
*
* Tested without a canvas; `ui/dotplotFigure.ts` draws it, and the reference
* editor's corner plot (§27.3) draws the same streaks through an edit.
*/
/** A set's sequences along one axis, in order: their names and their lengths in bases. */
export interface DotplotAxis {
names: readonly string[]
lengths: readonly number[]
}
/** The streaks of a dot plot, one entry each: extents on each axis, half-open, in bases along the concatenated axes. */
export interface DotplotLines {
x0: ArrayLike<number>
x1: ArrayLike<number>
y0: ArrayLike<number>
y1: ArrayLike<number>
/** `1` the same way round, `-1` the other. */
strand: ArrayLike<number>
}
/** The window on screen, in bases along each axis: `x` left to right, `y` top to bottom. */
export interface DotplotView {
x0: number
x1: number
y0: number
y1: number
}
/** Where the plot is drawn on the canvas, in CSS pixels. */
export interface PlotRect {
x: number
y: number
width: number
height: number
}
/** The fewest bases a pixel shows, zoomed in all the way: a base is as fine as a streak has anything to say. */
export const LEAST_BASES_A_PIXEL = 1
/** Where each of an axis's sequences starts along it, and its total last. */
export function offsetsOf(axis: DotplotAxis): number[] {
const out = [0]
for (const length of axis.lengths) out.push(out[out.length - 1] + length)
return out
}
/** Bases a pixel, the same on both axes. */
export function scaleOf(view: DotplotView, rect: PlotRect): number {
return (view.x1 - view.x0) / Math.max(1, rect.width)
}
/**
* The whole of both sets at one scale, centred in the plot: the longer of the
* two fills its side, and the other is centred along its own with room either
* side.
*/
export function fitView(xTotal: number, yTotal: number, rect: PlotRect): DotplotView {
const scale = Math.max(xTotal / Math.max(1, rect.width), yTotal / Math.max(1, rect.height), LEAST_BASES_A_PIXEL)
const width = rect.width * scale
const height = rect.height * scale
const cx = xTotal / 2
const cy = yTotal / 2
return { x0: cx - width / 2, x1: cx + width / 2, y0: cy - height / 2, y1: cy + height / 2 }
}
/**
* A view kept over the data: never wider than the fitted whole, never finer
* than {@link LEAST_BASES_A_PIXEL}, and never moved so far that the data
* leaves the plot — its centre over both axes. Only its centre: a view held to
* the data's edges would shift under the pointer zooming into the last
* sequence of either set, which is where a fused chromosome, written last,
* usually is. An axis the view is wider than is centred on it.
*/
export function clampView(view: DotplotView, xTotal: number, yTotal: number, rect: PlotRect): DotplotView {
const whole = fitView(xTotal, yTotal, rect)
const most = scaleOf(whole, rect)
const scale = Math.min(most, Math.max(LEAST_BASES_A_PIXEL, scaleOf(view, rect)))
const width = rect.width * scale
const height = rect.height * scale
const clamp = (value: number, lo: number, hi: number): number => Math.min(hi, Math.max(lo, value))
const cx = width >= xTotal ? xTotal / 2 : clamp((view.x0 + view.x1) / 2, 0, xTotal)
const cy = height >= yTotal ? yTotal / 2 : clamp((view.y0 + view.y1) / 2, 0, yTotal)
return { x0: cx - width / 2, x1: cx + width / 2, y0: cy - height / 2, y1: cy + height / 2 }
}
/** A pixel of the plot, in bases along each axis. */
export function toBases(view: DotplotView, rect: PlotRect, px: number, py: number): { x: number; y: number } {
const scale = scaleOf(view, rect)
return { x: view.x0 + (px - rect.x) * scale, y: view.y0 + (py - rect.y) * scale }
}
/** A place in bases, as a pixel of the plot. */
export function toPixels(view: DotplotView, rect: PlotRect, x: number, y: number): { px: number; py: number } {
const scale = scaleOf(view, rect)
return { px: rect.x + (x - view.x0) / scale, py: rect.y + (y - view.y0) / scale }
}
/** Zoomed by `factor` — above one, in — about the pixel `(px, py)`, which stays over the same bases. */
export function zoomView(view: DotplotView, rect: PlotRect, px: number, py: number, factor: number): DotplotView {
const at = toBases(view, rect, px, py)
const scale = scaleOf(view, rect) / factor
const left = at.x - (px - rect.x) * scale
const top = at.y - (py - rect.y) * scale
return { x0: left, x1: left + rect.width * scale, y0: top, y1: top + rect.height * scale }
}
/** Moved by `(dx, dy)` pixels: the picture follows the hand. */
export function panView(view: DotplotView, rect: PlotRect, dx: number, dy: number): DotplotView {
const scale = scaleOf(view, rect)
return { x0: view.x0 - dx * scale, x1: view.x1 - dx * scale, y0: view.y0 - dy * scale, y1: view.y1 - dy * scale }
}
/** A streak's two ends, in bases: from its start on x, down its diagonal — or up it, the other way round. */
export function streakEnds(lines: DotplotLines, i: number): [number, number, number, number] {
return lines.strand[i] === -1 ? [lines.x0[i], lines.y1[i], lines.x1[i], lines.y0[i]] : [lines.x0[i], lines.y0[i], lines.x1[i], lines.y1[i]]
}
/**
* The streak nearest the pixel `(px, py)` within `reach` pixels of it, measured
* to the line it is drawn as; `-1` for none.
*/
export function streakAt(lines: DotplotLines, view: DotplotView, rect: PlotRect, px: number, py: number, reach: number): number {
const scale = scaleOf(view, rect)
const at = toBases(view, rect, px, py)
const room = reach * scale
let best = -1
let bestDistance = Number.POSITIVE_INFINITY
const count = lines.x0.length
for (let i = 0; i < count; i++) {
// Outside its box, grown by the reach, it cannot be within reach.
if (at.x < lines.x0[i] - room || at.x > lines.x1[i] + room || at.y < lines.y0[i] - room || at.y > lines.y1[i] + room) continue
const [ax, ay, bx, by] = streakEnds(lines, i)
const dx = bx - ax
const dy = by - ay
const length = dx * dx + dy * dy
const t = length === 0 ? 0 : Math.max(0, Math.min(1, ((at.x - ax) * dx + (at.y - ay) * dy) / length))
const distance = Math.hypot(at.x - (ax + t * dx), at.y - (ay + t * dy)) / scale
if (distance <= reach && distance < bestDistance) {
best = i
bestDistance = distance
}
}
return best
}
/** A position along an axis as the sequence it is in and the base in it, 0-based; `null` off the axis. */
export function locusOf(axis: DotplotAxis, offsets: readonly number[], position: number): { name: string; at: number } | null {
if (!(position >= 0) || position >= offsets[offsets.length - 1]) return null
let low = 0
let high = axis.names.length - 1
while (low < high) {
const mid = (low + high + 1) >> 1
if (offsets[mid] <= position) low = mid
else high = mid - 1
}
return { name: axis.names[low], at: Math.floor(position - offsets[low]) }
}
|