@jbrowse/wiggle-core 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +292 -0
- package/esm/CrossHatches.d.ts +11 -0
- package/esm/CrossHatches.js +32 -0
- package/esm/ResolutionStepper.d.ts +17 -0
- package/esm/ResolutionStepper.js +56 -0
- package/esm/ScoreRules.d.ts +11 -0
- package/esm/ScoreRules.js +37 -0
- package/esm/ScoreScaleMixin.d.ts +105 -0
- package/esm/ScoreScaleMixin.js +136 -0
- package/esm/SetMinMaxDialog.d.ts +11 -0
- package/esm/SetMinMaxDialog.js +18 -0
- package/esm/YScaleBar.d.ts +6 -0
- package/esm/YScaleBar.js +46 -0
- package/esm/YScaleBarOverlay.d.ts +6 -0
- package/esm/YScaleBarOverlay.js +24 -0
- package/esm/autoscale.d.ts +55 -0
- package/esm/autoscale.js +252 -0
- package/esm/computeYTicks.d.ts +15 -0
- package/esm/computeYTicks.js +44 -0
- package/esm/constants.d.ts +2 -0
- package/esm/constants.js +10 -0
- package/esm/dataTypes.d.ts +26 -0
- package/esm/dataTypes.js +1 -0
- package/esm/displayModel.d.ts +24 -0
- package/esm/displayModel.js +1 -0
- package/esm/formatNumber.d.ts +1 -0
- package/esm/formatNumber.js +6 -0
- package/esm/gapBreak.d.ts +59 -0
- package/esm/gapBreak.js +58 -0
- package/esm/index.d.ts +36 -0
- package/esm/index.js +26 -0
- package/esm/normalize.d.ts +40 -0
- package/esm/normalize.js +106 -0
- package/esm/pointMarker.d.ts +3 -0
- package/esm/pointMarker.js +27 -0
- package/esm/pointSizeMenu.d.ts +13 -0
- package/esm/pointSizeMenu.js +21 -0
- package/esm/renderingBackendTypes.d.ts +39 -0
- package/esm/renderingBackendTypes.js +29 -0
- package/esm/resolveRenderState.d.ts +1 -0
- package/esm/resolveRenderState.js +20 -0
- package/esm/scale.d.ts +60 -0
- package/esm/scale.js +161 -0
- package/esm/scoreAxisConfigSchemaFields.d.ts +44 -0
- package/esm/scoreAxisConfigSchemaFields.js +54 -0
- package/esm/scoreMenuItems.d.ts +37 -0
- package/esm/scoreMenuItems.js +104 -0
- package/esm/scoreRuleMarks.d.ts +73 -0
- package/esm/scoreRuleMarks.js +98 -0
- package/esm/symlogTicks.d.ts +17 -0
- package/esm/symlogTicks.js +92 -0
- package/esm/transferables.d.ts +2 -0
- package/esm/transferables.js +32 -0
- package/esm/vendor/d3-scale.d.ts +33 -0
- package/esm/vendor/d3-scale.js +249 -0
- package/esm/visibleStatsDomain.d.ts +59 -0
- package/esm/visibleStatsDomain.js +33 -0
- package/esm/wiggleRenderModes.generated.d.ts +9 -0
- package/esm/wiggleRenderModes.generated.js +11 -0
- package/esm/yScaleTicks.d.ts +59 -0
- package/esm/yScaleTicks.js +59 -0
- package/package.json +54 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { linearTicks } from './vendor/d3-scale.js';
|
|
2
|
+
// Tick VALUES for a symlog axis, which d3 does not have and this axis needs.
|
|
3
|
+
//
|
|
4
|
+
// d3's scaleSymlog is `linearish`, so `ticks()` hands back evenly spaced values
|
|
5
|
+
// in SCORE space while the axis places them in TRANSFORMED space. The two
|
|
6
|
+
// disagree by exactly the amount symlog exists to provide: on a [0, 1000] domain
|
|
7
|
+
// with the resolved constant, d3's 0, 200, 400, 600, 800, 1000 land at 100%,
|
|
8
|
+
// 23%, 13%, 7%, 3% and 0% of the plot height — five 10px labels stacked in the
|
|
9
|
+
// top quarter, and two thirds of the axis carrying one tick. The plot is right
|
|
10
|
+
// and the axis over it is unreadable.
|
|
11
|
+
//
|
|
12
|
+
// So the values are chosen in transformed space instead — evenly spaced there,
|
|
13
|
+
// which is evenly spaced on screen — and each is then rounded to the nearest
|
|
14
|
+
// 1/2/5 × 10^k so the labels read as numbers a person would have picked. A
|
|
15
|
+
// [0, 1000] domain comes out 0, 5, 20, 200, 1000: five labels spread down the
|
|
16
|
+
// whole axis.
|
|
17
|
+
//
|
|
18
|
+
// `vendor/d3-scale.ts` stays d3's arithmetic unchanged, and `scaleTicks` there
|
|
19
|
+
// still answers what d3 answers — `scaleParity.test.ts` pins it. This is
|
|
20
|
+
// `getScale`'s own choice, applied where a scale is built for drawing.
|
|
21
|
+
const symlog = (x, c) => Math.sign(x) * Math.log1p(Math.abs(x / c));
|
|
22
|
+
const symexp = (t, c) => Math.sign(t) * c * Math.expm1(Math.abs(t));
|
|
23
|
+
// Nearest 1/2/5/10 × 10^k, compared in log space so the choice doesn't depend on
|
|
24
|
+
// which decade the value happens to sit in. `toPrecision(15)` drops the binary
|
|
25
|
+
// residue off `f * 10 ** k` (5e-5 comes out 0.00005000000000000001 otherwise),
|
|
26
|
+
// which matters because these are rendered as label text.
|
|
27
|
+
function niceMagnitude(x) {
|
|
28
|
+
const magnitude = Math.abs(x);
|
|
29
|
+
const decade = 10 ** Math.floor(Math.log10(magnitude));
|
|
30
|
+
let best = decade;
|
|
31
|
+
for (const factor of [2, 5, 10]) {
|
|
32
|
+
const candidate = factor * decade;
|
|
33
|
+
if (Math.abs(Math.log(candidate / magnitude)) <
|
|
34
|
+
Math.abs(Math.log(best / magnitude))) {
|
|
35
|
+
best = candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Math.sign(x) * Number(best.toPrecision(15));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Tick values for a symlog domain, spaced evenly down the axis rather than
|
|
42
|
+
* evenly across the scores.
|
|
43
|
+
*
|
|
44
|
+
* The domain endpoints are always ticks: symlog's whole point is that the small
|
|
45
|
+
* end stays legible, and both ends of an autoscaled domain are what tell a
|
|
46
|
+
* reader what the plot's extent is. Zero joins them when the domain crosses it —
|
|
47
|
+
* on a log-ratio track that is the one score with an absolute meaning, and
|
|
48
|
+
* rounding never lands on it.
|
|
49
|
+
*
|
|
50
|
+
* Rounding can put an interior tick on top of a neighbour (a domain crossing
|
|
51
|
+
* zero rounds its smallest magnitudes to something a pixel from the zero line),
|
|
52
|
+
* so a candidate closer than half a slot to a tick already kept is dropped
|
|
53
|
+
* rather than drawn over it. Endpoints and zero are kept in preference to any
|
|
54
|
+
* rounded value, which is the same order they are stated in above.
|
|
55
|
+
*/
|
|
56
|
+
export function symlogTicks(domain, constant, count = 10) {
|
|
57
|
+
const [domainMin, domainMax] = domain;
|
|
58
|
+
const descending = domainMax < domainMin;
|
|
59
|
+
const min = descending ? domainMax : domainMin;
|
|
60
|
+
const max = descending ? domainMin : domainMax;
|
|
61
|
+
const tMin = symlog(min, constant);
|
|
62
|
+
const tMax = symlog(max, constant);
|
|
63
|
+
const slots = Math.max(1, Math.floor(count));
|
|
64
|
+
if (!(constant > 0) || !(tMax - tMin > 0)) {
|
|
65
|
+
// A constant that isn't positive makes the transform undefined, and a
|
|
66
|
+
// transformed span of zero (a flat domain) has nothing to space out. Neither
|
|
67
|
+
// is this function's call to make, so hand both back to the linear ticks the
|
|
68
|
+
// rest of the module uses.
|
|
69
|
+
return linearTicks(domainMin, domainMax, count);
|
|
70
|
+
}
|
|
71
|
+
const normalized = (v) => (symlog(v, constant) - tMin) / (tMax - tMin);
|
|
72
|
+
const minGap = 0.5 / slots;
|
|
73
|
+
// Anchors first so a rounded value never displaces one of them, then the
|
|
74
|
+
// rounded candidates in ascending order; `kept` stays sorted because the
|
|
75
|
+
// anchors are the extremes and zero is between them.
|
|
76
|
+
const anchors = min < 0 && max > 0 ? [min, 0, max] : [min, max];
|
|
77
|
+
const candidates = [];
|
|
78
|
+
for (let i = 1; i < slots; i++) {
|
|
79
|
+
const value = niceMagnitude(symexp(tMin + ((tMax - tMin) * i) / slots, constant));
|
|
80
|
+
if (value > min && value < max) {
|
|
81
|
+
candidates.push(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const kept = [...anchors];
|
|
85
|
+
for (const value of candidates) {
|
|
86
|
+
if (kept.every(k => Math.abs(normalized(value) - normalized(k)) >= minGap)) {
|
|
87
|
+
kept.push(value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
kept.sort((a, b) => a - b);
|
|
91
|
+
return descending ? kept.reverse() : kept;
|
|
92
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Walks every result's sources once and returns each source's eight per-feature
|
|
2
|
+
// TypedArray buffers as one flat list for postMessage transfer. Field set
|
|
3
|
+
// matches WiggleFeatureArrays — when fields change there, update here so the
|
|
4
|
+
// buffers actually transfer instead of being structured-cloned.
|
|
5
|
+
//
|
|
6
|
+
// The Set dedupe is load-bearing, not insurance: processFeaturesFromArrays
|
|
7
|
+
// aliases fields onto one buffer whenever a copy would be identical (no summary
|
|
8
|
+
// data → min/max are the scores; an all-one-sided window → the pos or neg
|
|
9
|
+
// arrays are the full arrays). postMessage throws on a repeated transferable.
|
|
10
|
+
//
|
|
11
|
+
// Takes ALL the results rather than one, so the dedupe spans regions too — a
|
|
12
|
+
// per-result Set would list a shared buffer twice.
|
|
13
|
+
//
|
|
14
|
+
// Aliasing the adapter's own arrays here as well, rather than copying them, is
|
|
15
|
+
// costed and declined in agent-docs/reference/REJECTED_IDEAS.md: it retains 20
|
|
16
|
+
// bytes a feature on the main thread where copying retains 12.
|
|
17
|
+
export function collectWiggleTransferables(results) {
|
|
18
|
+
const buffers = new Set();
|
|
19
|
+
for (const result of results) {
|
|
20
|
+
for (const s of result.sources) {
|
|
21
|
+
buffers.add(s.featurePositions.buffer);
|
|
22
|
+
buffers.add(s.featureScores.buffer);
|
|
23
|
+
buffers.add(s.featureMinScores.buffer);
|
|
24
|
+
buffers.add(s.featureMaxScores.buffer);
|
|
25
|
+
buffers.add(s.posFeaturePositions.buffer);
|
|
26
|
+
buffers.add(s.posFeatureScores.buffer);
|
|
27
|
+
buffers.add(s.negFeaturePositions.buffer);
|
|
28
|
+
buffers.add(s.negFeatureScores.buffer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...buffers];
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ScaleKind = 'linear' | 'log' | 'symlog';
|
|
2
|
+
export interface ScaleSpec {
|
|
3
|
+
kind: ScaleKind;
|
|
4
|
+
domain: readonly [number, number];
|
|
5
|
+
range: readonly [number, number];
|
|
6
|
+
/** log only; 10 when unset, and every caller here passes 2 */
|
|
7
|
+
base?: number;
|
|
8
|
+
/** symlog only; the half-width of the linear region around zero */
|
|
9
|
+
constant?: number;
|
|
10
|
+
}
|
|
11
|
+
/** d3-array's `ticks`: round values inside [start, stop], about `count` of them. */
|
|
12
|
+
export declare function linearTicks(start: number, stop: number, count: number): number[];
|
|
13
|
+
/**
|
|
14
|
+
* Where `x` lands in the range.
|
|
15
|
+
*
|
|
16
|
+
* The arithmetic is d3's bimap, expression for expression — normalize to t,
|
|
17
|
+
* then `r0 * (1 - t) + r1 * t`, with the endpoints swapped for a descending
|
|
18
|
+
* domain. The algebraically identical `r0 + t * (r1 - r0)` differs in the last
|
|
19
|
+
* bit, which `scaleParity.test.ts` compares exactly and rejects.
|
|
20
|
+
*
|
|
21
|
+
* A zero-width domain returns the range's midpoint rather than dividing by
|
|
22
|
+
* zero, which is d3's behaviour and the one a flat track wants: every score
|
|
23
|
+
* equal means every bar at the same height, not every bar at NaN.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scaleValue(spec: ScaleSpec, x: number): number;
|
|
26
|
+
/** The domain rounded out to tick boundaries — d3's `nice`. */
|
|
27
|
+
export declare function niceDomain(spec: ScaleSpec, count?: number): [number, number];
|
|
28
|
+
/**
|
|
29
|
+
* Tick values inside the domain. Log ticks sit at every k×base^i — 2, 4, 8 for
|
|
30
|
+
* base 2 — falling back to linear ticks when the domain spans too many powers
|
|
31
|
+
* to enumerate, or too few to fill the count.
|
|
32
|
+
*/
|
|
33
|
+
export declare function scaleTicks(spec: ScaleSpec, count?: number): number[];
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Derived from d3-scale 4.0.2 and d3-array 3.2.4, ISC License —
|
|
2
|
+
// https://github.com/d3/d3-scale/blob/main/LICENSE
|
|
3
|
+
//
|
|
4
|
+
// Three scales, because `scaleType` is a stringEnum of exactly linear, log and
|
|
5
|
+
// symlog. What d3 offers beyond them, and what these three offered beyond
|
|
6
|
+
// `getScale`'s use of them — clamp, invert, interpolate, rangeRound, unknown,
|
|
7
|
+
// tickFormat, copy, piecewise domains, quantize — is not here.
|
|
8
|
+
//
|
|
9
|
+
// **Pure functions over a spec**, rather than d3's chainable callable objects:
|
|
10
|
+
// nothing here holds state, so a scale is a value you can compare, log and
|
|
11
|
+
// build in a worker, and `scale.ts` assembles the one callable the public
|
|
12
|
+
// `getScale` still returns.
|
|
13
|
+
//
|
|
14
|
+
// **The tick and nice arithmetic is d3's, unchanged**, and deliberately so:
|
|
15
|
+
// `yScaleTicksParity.test.ts` pins the axis against what the renderer draws,
|
|
16
|
+
// and a tick sequence differing in the last decimal moves a labelled line off
|
|
17
|
+
// the bar it belongs to. `scaleParity.test.ts` holds it to that.
|
|
18
|
+
const e10 = Math.sqrt(50);
|
|
19
|
+
const e5 = Math.sqrt(10);
|
|
20
|
+
const e2 = Math.sqrt(2);
|
|
21
|
+
function tickSpec(start, stop, count) {
|
|
22
|
+
const step = (stop - start) / Math.max(0, count);
|
|
23
|
+
const power = Math.floor(Math.log10(step));
|
|
24
|
+
const error = step / 10 ** power;
|
|
25
|
+
const factor = error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1;
|
|
26
|
+
let i1;
|
|
27
|
+
let i2;
|
|
28
|
+
let inc;
|
|
29
|
+
if (power < 0) {
|
|
30
|
+
inc = 10 ** -power / factor;
|
|
31
|
+
i1 = Math.round(start * inc);
|
|
32
|
+
i2 = Math.round(stop * inc);
|
|
33
|
+
if (i1 / inc < start) {
|
|
34
|
+
++i1;
|
|
35
|
+
}
|
|
36
|
+
if (i2 / inc > stop) {
|
|
37
|
+
--i2;
|
|
38
|
+
}
|
|
39
|
+
inc = -inc;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
inc = 10 ** power * factor;
|
|
43
|
+
i1 = Math.round(start / inc);
|
|
44
|
+
i2 = Math.round(stop / inc);
|
|
45
|
+
if (i1 * inc < start) {
|
|
46
|
+
++i1;
|
|
47
|
+
}
|
|
48
|
+
if (i2 * inc > stop) {
|
|
49
|
+
--i2;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (i2 < i1 && count >= 0.5 && count < 2) {
|
|
53
|
+
return tickSpec(start, stop, count * 2);
|
|
54
|
+
}
|
|
55
|
+
return [i1, i2, inc];
|
|
56
|
+
}
|
|
57
|
+
/** d3-array's `ticks`: round values inside [start, stop], about `count` of them. */
|
|
58
|
+
export function linearTicks(start, stop, count) {
|
|
59
|
+
if (!(count > 0)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
if (start === stop) {
|
|
63
|
+
return [start];
|
|
64
|
+
}
|
|
65
|
+
const reverse = stop < start;
|
|
66
|
+
const [i1, i2, inc] = reverse
|
|
67
|
+
? tickSpec(stop, start, count)
|
|
68
|
+
: tickSpec(start, stop, count);
|
|
69
|
+
if (!(i2 >= i1)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
// Multiply or divide by `inc` as its sign says, rather than always
|
|
73
|
+
// multiplying: for a fractional step d3 carries the reciprocal as a negative
|
|
74
|
+
// integer so the ticks land on exact decimals.
|
|
75
|
+
const at = (i) => (inc < 0 ? i / -inc : i * inc);
|
|
76
|
+
return Array.from({ length: i2 - i1 + 1 }, (_, i) => reverse ? at(i2 - i) : at(i1 + i));
|
|
77
|
+
}
|
|
78
|
+
const tickIncrement = (start, stop, count) => tickSpec(start, stop, count)[2];
|
|
79
|
+
// Exact for the bases anyone uses. `Math.log(8) / Math.log(2)` is
|
|
80
|
+
// 2.9999999999999996, and the log `nice` floors that logarithm — so the generic
|
|
81
|
+
// form would round a domain endpoint down a whole decade.
|
|
82
|
+
const logp = (base) => base === Math.E
|
|
83
|
+
? Math.log
|
|
84
|
+
: base === 10
|
|
85
|
+
? Math.log10
|
|
86
|
+
: base === 2
|
|
87
|
+
? Math.log2
|
|
88
|
+
: (x) => Math.log(x) / Math.log(base);
|
|
89
|
+
const powp = (base) => base === 10
|
|
90
|
+
? (x) => (Number.isFinite(x) ? +`1e${x}` : Math.max(x, 0))
|
|
91
|
+
: base === Math.E
|
|
92
|
+
? Math.exp
|
|
93
|
+
: (x) => base ** x;
|
|
94
|
+
// sign(x) * log1p(|x / c|). Unlike a log scale it is defined at 0 — and either
|
|
95
|
+
// side of it — which is the whole reason it is here: coverage that touches
|
|
96
|
+
// zero, and log-ratio data that crosses it, have no log domain at all.
|
|
97
|
+
//
|
|
98
|
+
// `constant` is not decoration. At c = 1 this IS log(x + 1), which flattens any
|
|
99
|
+
// data living below 1 (p-values, fractions) into the linear part of the curve
|
|
100
|
+
// and throws away exactly the resolution such a track is read for. Shrinking c
|
|
101
|
+
// moves the linear knee down to where the interesting values are.
|
|
102
|
+
const symlog = (c) => (x) => Math.sign(x) * Math.log1p(Math.abs(x / c));
|
|
103
|
+
// A wholly negative log domain is handled by reflection — d3 negates the input,
|
|
104
|
+
// takes the logarithm, and negates the result — which is what lets a log scale
|
|
105
|
+
// run from -100 to -1. `getNiceDomain` floors its own log domains above zero,
|
|
106
|
+
// so this is for callers reaching `getScale` directly.
|
|
107
|
+
const reflect = (f) => (x) => -f(-x);
|
|
108
|
+
const logsFor = (spec) => {
|
|
109
|
+
const log = logp(spec.base ?? 10);
|
|
110
|
+
return spec.domain[0] < 0 ? reflect(log) : log;
|
|
111
|
+
};
|
|
112
|
+
const powsFor = (spec) => {
|
|
113
|
+
const pow = powp(spec.base ?? 10);
|
|
114
|
+
return spec.domain[0] < 0 ? reflect(pow) : pow;
|
|
115
|
+
};
|
|
116
|
+
/** The spec's transform. */
|
|
117
|
+
function transformFor(spec) {
|
|
118
|
+
if (spec.kind === 'symlog') {
|
|
119
|
+
return symlog(spec.constant ?? 1);
|
|
120
|
+
}
|
|
121
|
+
if (spec.kind === 'log') {
|
|
122
|
+
// Natural log whatever the base, which is what d3 maps values through:
|
|
123
|
+
// `loggish` keeps the base-specific pair for nice() and ticks() only. The
|
|
124
|
+
// base cancels in the normalized interpolation algebraically, but log2 and
|
|
125
|
+
// log disagree in the last bit and the parity table compares exactly.
|
|
126
|
+
return spec.domain[0] < 0 ? x => -Math.log(-x) : Math.log;
|
|
127
|
+
}
|
|
128
|
+
return x => x;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Where `x` lands in the range.
|
|
132
|
+
*
|
|
133
|
+
* The arithmetic is d3's bimap, expression for expression — normalize to t,
|
|
134
|
+
* then `r0 * (1 - t) + r1 * t`, with the endpoints swapped for a descending
|
|
135
|
+
* domain. The algebraically identical `r0 + t * (r1 - r0)` differs in the last
|
|
136
|
+
* bit, which `scaleParity.test.ts` compares exactly and rejects.
|
|
137
|
+
*
|
|
138
|
+
* A zero-width domain returns the range's midpoint rather than dividing by
|
|
139
|
+
* zero, which is d3's behaviour and the one a flat track wants: every score
|
|
140
|
+
* equal means every bar at the same height, not every bar at NaN.
|
|
141
|
+
*/
|
|
142
|
+
export function scaleValue(spec, x) {
|
|
143
|
+
const transform = transformFor(spec);
|
|
144
|
+
const t0 = transform(spec.domain[0]);
|
|
145
|
+
const t1 = transform(spec.domain[1]);
|
|
146
|
+
const [r0, r1] = spec.range;
|
|
147
|
+
const [a, b, ra, rb] = t1 < t0 ? [t1, t0, r1, r0] : [t0, t1, r0, r1];
|
|
148
|
+
const span = b - a;
|
|
149
|
+
if (!span) {
|
|
150
|
+
return Number.isNaN(span) ? NaN : (ra + rb) / 2;
|
|
151
|
+
}
|
|
152
|
+
const t = (transform(x) - a) / span;
|
|
153
|
+
return ra * (1 - t) + rb * t;
|
|
154
|
+
}
|
|
155
|
+
/** The domain rounded out to tick boundaries — d3's `nice`. */
|
|
156
|
+
export function niceDomain(spec, count = 10) {
|
|
157
|
+
if (spec.kind === 'log') {
|
|
158
|
+
const logs = logsFor(spec);
|
|
159
|
+
const pows = powsFor(spec);
|
|
160
|
+
return [
|
|
161
|
+
pows(Math.floor(logs(spec.domain[0]))),
|
|
162
|
+
pows(Math.ceil(logs(spec.domain[1]))),
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
// d3's linearish.nice, which symlog composes too: widen to the tick step,
|
|
166
|
+
// then re-derive the step from the widened domain until it stops moving,
|
|
167
|
+
// because widening can change which step tickIncrement picks.
|
|
168
|
+
let [start, stop] = spec.domain;
|
|
169
|
+
const flip = stop < start;
|
|
170
|
+
if (flip) {
|
|
171
|
+
;
|
|
172
|
+
[start, stop] = [stop, start];
|
|
173
|
+
}
|
|
174
|
+
// The widened endpoints are COMMITTED only where the step converges, which
|
|
175
|
+
// is d3's own control flow and not a detail: a degenerate domain — [5, 5],
|
|
176
|
+
// what a flat coverage track hands over — drives the step to -Infinity and
|
|
177
|
+
// the endpoints to NaN on the first pass. d3 leaves such a domain alone;
|
|
178
|
+
// returning the intermediate values instead gives the axis a NaN domain.
|
|
179
|
+
let prestep;
|
|
180
|
+
for (let iter = 0; iter < 10; iter++) {
|
|
181
|
+
const step = tickIncrement(start, stop, count);
|
|
182
|
+
if (step === prestep) {
|
|
183
|
+
return flip ? [stop, start] : [start, stop];
|
|
184
|
+
}
|
|
185
|
+
if (step > 0) {
|
|
186
|
+
start = Math.floor(start / step) * step;
|
|
187
|
+
stop = Math.ceil(stop / step) * step;
|
|
188
|
+
}
|
|
189
|
+
else if (step < 0) {
|
|
190
|
+
start = Math.ceil(start * step) / step;
|
|
191
|
+
stop = Math.floor(stop * step) / step;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
prestep = step;
|
|
197
|
+
}
|
|
198
|
+
return [spec.domain[0], spec.domain[1]];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Tick values inside the domain. Log ticks sit at every k×base^i — 2, 4, 8 for
|
|
202
|
+
* base 2 — falling back to linear ticks when the domain spans too many powers
|
|
203
|
+
* to enumerate, or too few to fill the count.
|
|
204
|
+
*/
|
|
205
|
+
export function scaleTicks(spec, count = 10) {
|
|
206
|
+
if (spec.kind !== 'log') {
|
|
207
|
+
return linearTicks(spec.domain[0], spec.domain[1], count);
|
|
208
|
+
}
|
|
209
|
+
const base = spec.base ?? 10;
|
|
210
|
+
const logs = logsFor(spec);
|
|
211
|
+
const pows = powsFor(spec);
|
|
212
|
+
let [u, v] = spec.domain;
|
|
213
|
+
const reverse = v < u;
|
|
214
|
+
if (reverse) {
|
|
215
|
+
;
|
|
216
|
+
[u, v] = [v, u];
|
|
217
|
+
}
|
|
218
|
+
let i = logs(u);
|
|
219
|
+
const j0 = logs(v);
|
|
220
|
+
// `!(j0 - i < count)`, not `>= count`: a negative domain makes both
|
|
221
|
+
// logarithms NaN, and every comparison against NaN is false — so the `>=`
|
|
222
|
+
// spelling falls through to the enumeration below and invents linear ticks
|
|
223
|
+
// for a domain d3 declines to tick at all.
|
|
224
|
+
if (base % 1 || !(j0 - i < count)) {
|
|
225
|
+
return linearTicks(i, j0, Math.min(j0 - i, count)).map(pows);
|
|
226
|
+
}
|
|
227
|
+
const j = Math.ceil(j0);
|
|
228
|
+
i = Math.floor(i);
|
|
229
|
+
// Ascending multipliers for a positive domain, descending for a negative one,
|
|
230
|
+
// so the ticks come out in increasing order either way.
|
|
231
|
+
const ks = Array.from({ length: base - 1 }, (_, k) => u > 0 ? k + 1 : base - 1 - k);
|
|
232
|
+
const out = [];
|
|
233
|
+
for (; i <= j; ++i) {
|
|
234
|
+
for (const k of ks) {
|
|
235
|
+
const t = (u > 0 ? i < 0 : i > 0) ? k / pows(-i) : k * pows(i);
|
|
236
|
+
if (t < u) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (t > v) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
out.push(t);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (out.length * 2 < count) {
|
|
246
|
+
return linearTicks(u, v, count);
|
|
247
|
+
}
|
|
248
|
+
return reverse ? out.reverse() : out;
|
|
249
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One visible block's worth of one stats-bearing thing, carrying the block span
|
|
3
|
+
* it is clipped to: a wiggle source's feature arrays, a coverage region, a
|
|
4
|
+
* region's worker-shipped extremes.
|
|
5
|
+
*/
|
|
6
|
+
export interface VisibleEntry<T> {
|
|
7
|
+
visStart: number;
|
|
8
|
+
visEnd: number;
|
|
9
|
+
data: T;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The half of the containing view this reads.
|
|
13
|
+
*
|
|
14
|
+
* `settledDynamicBlocks` — the 500ms-debounced coarse blocks once the view has
|
|
15
|
+
* settled once, the live ones before that — so a per-bp or per-feature scan
|
|
16
|
+
* doesn't recompute on every animation frame during pan/zoom. Not
|
|
17
|
+
* `coarseDynamicBlocks`: over the empty initial coarse list every walker below
|
|
18
|
+
* yields no entries, and no entries is not a stale domain but the caller's
|
|
19
|
+
* `[0, 1]` fallback — a bigwig line track drew blank and a density track solid.
|
|
20
|
+
* ARCHITECTURE.md, "Every fetch autorun runs on the leading edge".
|
|
21
|
+
*/
|
|
22
|
+
export interface SettledBlocksView {
|
|
23
|
+
initialized: boolean;
|
|
24
|
+
settledDynamicBlocks: {
|
|
25
|
+
start: number;
|
|
26
|
+
end: number;
|
|
27
|
+
displayedRegionIndex?: number;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* How one display gets from its per-region fetch results to a score domain. The
|
|
32
|
+
* walk, the readiness guard and the bounded nice-rounding are the same for all
|
|
33
|
+
* of them; what differs is where a region's payload lives, which pieces of it
|
|
34
|
+
* are in scope, how those pieces reduce to stats, and what an autoscale mode
|
|
35
|
+
* makes of the stats.
|
|
36
|
+
*/
|
|
37
|
+
export interface VisibleStatsDomainSpec<Payload, Item, Stats> {
|
|
38
|
+
/** false when the band this domain scales is not drawn: nothing is walked */
|
|
39
|
+
active: boolean;
|
|
40
|
+
view: SettledBlocksView;
|
|
41
|
+
payloadFor: (displayedRegionIndex: number) => Payload | undefined;
|
|
42
|
+
/** the payload's stats-bearing pieces, already filtered to what is shown */
|
|
43
|
+
itemsFor: (payload: Payload) => Item[];
|
|
44
|
+
accumulate: (entries: VisibleEntry<Item>[]) => Stats | undefined;
|
|
45
|
+
/** stats to a raw `[min, max]`, before bounds and nice-rounding */
|
|
46
|
+
range: (stats: Stats, entries: VisibleEntry<Item>[]) => [number, number];
|
|
47
|
+
/** `ScoreScaleMixin`'s resolved bounds; `undefined` autoscales that end */
|
|
48
|
+
bounds: readonly [number | undefined, number | undefined];
|
|
49
|
+
scaleType: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* #api
|
|
53
|
+
* The visible score domain four displays derive identically: walk the settled
|
|
54
|
+
* blocks, accumulate the stats of what each one shows, and nice-round the
|
|
55
|
+
* autoscaled range inside the configured bounds. `undefined` while there is
|
|
56
|
+
* nothing to scale against — no data, a hidden band, or a view that has not
|
|
57
|
+
* initialized — which every caller distinguishes from a domain.
|
|
58
|
+
*/
|
|
59
|
+
export declare function visibleStatsDomain<Payload, Item, Stats>({ active, view, payloadFor, itemsFor, accumulate, range, bounds, scaleType, }: VisibleStatsDomainSpec<Payload, Item, Stats>): [number, number] | undefined;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getNiceDomain } from './scale.js';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* The visible score domain four displays derive identically: walk the settled
|
|
5
|
+
* blocks, accumulate the stats of what each one shows, and nice-round the
|
|
6
|
+
* autoscaled range inside the configured bounds. `undefined` while there is
|
|
7
|
+
* nothing to scale against — no data, a hidden band, or a view that has not
|
|
8
|
+
* initialized — which every caller distinguishes from a domain.
|
|
9
|
+
*/
|
|
10
|
+
export function visibleStatsDomain({ active, view, payloadFor, itemsFor, accumulate, range, bounds, scaleType, }) {
|
|
11
|
+
let domain;
|
|
12
|
+
if (active && view.initialized) {
|
|
13
|
+
const entries = view.settledDynamicBlocks.flatMap(block => {
|
|
14
|
+
const payload = block.displayedRegionIndex === undefined
|
|
15
|
+
? undefined
|
|
16
|
+
: payloadFor(block.displayedRegionIndex);
|
|
17
|
+
const visStart = Math.floor(block.start);
|
|
18
|
+
const visEnd = Math.ceil(block.end);
|
|
19
|
+
return payload
|
|
20
|
+
? itemsFor(payload).map(data => ({ visStart, visEnd, data }))
|
|
21
|
+
: [];
|
|
22
|
+
});
|
|
23
|
+
const stats = accumulate(entries);
|
|
24
|
+
if (stats) {
|
|
25
|
+
domain = getNiceDomain({
|
|
26
|
+
domain: range(stats, entries),
|
|
27
|
+
bounds,
|
|
28
|
+
scaleType,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return domain;
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const RENDERING_TYPE_XYPLOT = 0;
|
|
2
|
+
export declare const RENDERING_TYPE_DENSITY = 1;
|
|
3
|
+
export declare const RENDERING_TYPE_LINE = 2;
|
|
4
|
+
export declare const RENDERING_TYPE_SCATTER = 3;
|
|
5
|
+
export declare const RENDERING_TYPE_LINE_CENTER = 4;
|
|
6
|
+
export declare const SCALE_TYPE_LOG = 1;
|
|
7
|
+
export declare const SCALE_TYPE_SYMLOG = 2;
|
|
8
|
+
export declare const NO_PREV_START = 4294967295;
|
|
9
|
+
export declare const MIN_FILL_WIDTH_PX = 1.5;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// AUTO-GENERATED by packages/shader-tools/src/shader-codegen from wiggleCommon.slang.
|
|
2
|
+
// Do not edit. Run `pnpm gen:shaders` to regenerate.
|
|
3
|
+
export const RENDERING_TYPE_XYPLOT = 0;
|
|
4
|
+
export const RENDERING_TYPE_DENSITY = 1;
|
|
5
|
+
export const RENDERING_TYPE_LINE = 2;
|
|
6
|
+
export const RENDERING_TYPE_SCATTER = 3;
|
|
7
|
+
export const RENDERING_TYPE_LINE_CENTER = 4;
|
|
8
|
+
export const SCALE_TYPE_LOG = 1;
|
|
9
|
+
export const SCALE_TYPE_SYMLOG = 2;
|
|
10
|
+
export const NO_PREV_START = 4294967295;
|
|
11
|
+
export const MIN_FILL_WIDTH_PX = 1.5;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface YScaleTicks {
|
|
2
|
+
items: {
|
|
3
|
+
value: number;
|
|
4
|
+
y: number;
|
|
5
|
+
label?: string;
|
|
6
|
+
}[];
|
|
7
|
+
yTop: number;
|
|
8
|
+
yBottom: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The plot box a wiggle-family display insets inside its own height, for both
|
|
12
|
+
* sides of the contract above: the renderer positions its canvas at `yTop` and
|
|
13
|
+
* draws `plotHeight` tall, and the axis puts domain-max at `yTop` and
|
|
14
|
+
* domain-min at `yBottom`.
|
|
15
|
+
*
|
|
16
|
+
* One function because the two have to agree exactly, and `height - 2 * offset`
|
|
17
|
+
* spelled out at each end is how they stopped: `computeYTicks` derived its own
|
|
18
|
+
* bottom as `height - offset - 1`, and every tick above it inherited a fraction
|
|
19
|
+
* of that pixel.
|
|
20
|
+
*/
|
|
21
|
+
export declare function axisPlotBox(height: number, offset?: number): {
|
|
22
|
+
yTop: number;
|
|
23
|
+
yBottom: number;
|
|
24
|
+
plotHeight: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Screen y for an already-normalized ([0,1], 0 at domain-min) score. The
|
|
28
|
+
* composition of {@link axisPlotBox} with a `makeScoreNormalizer` result, which
|
|
29
|
+
* is what a tick at that score must land on.
|
|
30
|
+
*/
|
|
31
|
+
export declare function scoreToAxisY(normalized: number, box: ReturnType<typeof axisPlotBox>): number;
|
|
32
|
+
/**
|
|
33
|
+
* Width of the gutter a left-oriented {@link YScaleBar} is drawn in, and the x
|
|
34
|
+
* inside that gutter its spine goes at.
|
|
35
|
+
*
|
|
36
|
+
* `YScaleBar` grows its ticks and numbers *away* from the spine, so a
|
|
37
|
+
* left-oriented axis has to inset its spine by the label width or the labels
|
|
38
|
+
* land at negative x — which is what once put an exported coverage axis off the
|
|
39
|
+
* image entirely. Both numbers live here, beside the component whose own
|
|
40
|
+
* geometry (`x = ±9`, `fontSize 10`, `textAnchor: end`) is what makes them the
|
|
41
|
+
* right ones: the alignments coverage axis and the MAF band axes had each
|
|
42
|
+
* arrived at this same 50/45 pair independently, so a change to the labels moved
|
|
43
|
+
* neither.
|
|
44
|
+
*
|
|
45
|
+
* An axis drawn over its own plot rather than in reserved space may narrow the
|
|
46
|
+
* gutter — every px of it is a px of data hidden. Such an axis still places its
|
|
47
|
+
* spine at `width - 1`, the same rule stated against its own width.
|
|
48
|
+
*/
|
|
49
|
+
export declare const AXIS_GUTTER_WIDTH_PX = 50;
|
|
50
|
+
export declare function leftAxisSpineX(gutterLeft?: number): number;
|
|
51
|
+
/**
|
|
52
|
+
* Keep a 1px stroke's center inside the axis box. At `yBottom` — the box's own
|
|
53
|
+
* bottom edge — the stroke has to cover the pixel *above* the boundary, since
|
|
54
|
+
* the one below belongs to whatever is drawn next; in multi-wiggle, where rows
|
|
55
|
+
* stack edge to edge, that is literally the next sample's first row of pixels.
|
|
56
|
+
* Callers pass whatever stroke center their own convention gives (the axis
|
|
57
|
+
* crispens to `y + 0.5`, the guide lines don't); only the bottom edge moves.
|
|
58
|
+
*/
|
|
59
|
+
export declare function clampStrokeInsideAxis(strokeY: number, yBottom: number): number;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { YSCALEBAR_LABEL_OFFSET } from './constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* The plot box a wiggle-family display insets inside its own height, for both
|
|
4
|
+
* sides of the contract above: the renderer positions its canvas at `yTop` and
|
|
5
|
+
* draws `plotHeight` tall, and the axis puts domain-max at `yTop` and
|
|
6
|
+
* domain-min at `yBottom`.
|
|
7
|
+
*
|
|
8
|
+
* One function because the two have to agree exactly, and `height - 2 * offset`
|
|
9
|
+
* spelled out at each end is how they stopped: `computeYTicks` derived its own
|
|
10
|
+
* bottom as `height - offset - 1`, and every tick above it inherited a fraction
|
|
11
|
+
* of that pixel.
|
|
12
|
+
*/
|
|
13
|
+
export function axisPlotBox(height, offset = YSCALEBAR_LABEL_OFFSET) {
|
|
14
|
+
return {
|
|
15
|
+
yTop: offset,
|
|
16
|
+
yBottom: height - offset,
|
|
17
|
+
plotHeight: height - 2 * offset,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Screen y for an already-normalized ([0,1], 0 at domain-min) score. The
|
|
22
|
+
* composition of {@link axisPlotBox} with a `makeScoreNormalizer` result, which
|
|
23
|
+
* is what a tick at that score must land on.
|
|
24
|
+
*/
|
|
25
|
+
export function scoreToAxisY(normalized, box) {
|
|
26
|
+
return box.yTop + (1 - normalized) * box.plotHeight;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Width of the gutter a left-oriented {@link YScaleBar} is drawn in, and the x
|
|
30
|
+
* inside that gutter its spine goes at.
|
|
31
|
+
*
|
|
32
|
+
* `YScaleBar` grows its ticks and numbers *away* from the spine, so a
|
|
33
|
+
* left-oriented axis has to inset its spine by the label width or the labels
|
|
34
|
+
* land at negative x — which is what once put an exported coverage axis off the
|
|
35
|
+
* image entirely. Both numbers live here, beside the component whose own
|
|
36
|
+
* geometry (`x = ±9`, `fontSize 10`, `textAnchor: end`) is what makes them the
|
|
37
|
+
* right ones: the alignments coverage axis and the MAF band axes had each
|
|
38
|
+
* arrived at this same 50/45 pair independently, so a change to the labels moved
|
|
39
|
+
* neither.
|
|
40
|
+
*
|
|
41
|
+
* An axis drawn over its own plot rather than in reserved space may narrow the
|
|
42
|
+
* gutter — every px of it is a px of data hidden. Such an axis still places its
|
|
43
|
+
* spine at `width - 1`, the same rule stated against its own width.
|
|
44
|
+
*/
|
|
45
|
+
export const AXIS_GUTTER_WIDTH_PX = 50;
|
|
46
|
+
export function leftAxisSpineX(gutterLeft = 0) {
|
|
47
|
+
return gutterLeft + AXIS_GUTTER_WIDTH_PX - YSCALEBAR_LABEL_OFFSET;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Keep a 1px stroke's center inside the axis box. At `yBottom` — the box's own
|
|
51
|
+
* bottom edge — the stroke has to cover the pixel *above* the boundary, since
|
|
52
|
+
* the one below belongs to whatever is drawn next; in multi-wiggle, where rows
|
|
53
|
+
* stack edge to edge, that is literally the next sample's first row of pixels.
|
|
54
|
+
* Callers pass whatever stroke center their own convention gives (the axis
|
|
55
|
+
* crispens to `y + 0.5`, the guide lines don't); only the bottom edge moves.
|
|
56
|
+
*/
|
|
57
|
+
export function clampStrokeInsideAxis(strokeY, yBottom) {
|
|
58
|
+
return Math.min(strokeY, yBottom - 0.5);
|
|
59
|
+
}
|