@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.
Files changed (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +292 -0
  3. package/esm/CrossHatches.d.ts +11 -0
  4. package/esm/CrossHatches.js +32 -0
  5. package/esm/ResolutionStepper.d.ts +17 -0
  6. package/esm/ResolutionStepper.js +56 -0
  7. package/esm/ScoreRules.d.ts +11 -0
  8. package/esm/ScoreRules.js +37 -0
  9. package/esm/ScoreScaleMixin.d.ts +105 -0
  10. package/esm/ScoreScaleMixin.js +136 -0
  11. package/esm/SetMinMaxDialog.d.ts +11 -0
  12. package/esm/SetMinMaxDialog.js +18 -0
  13. package/esm/YScaleBar.d.ts +6 -0
  14. package/esm/YScaleBar.js +46 -0
  15. package/esm/YScaleBarOverlay.d.ts +6 -0
  16. package/esm/YScaleBarOverlay.js +24 -0
  17. package/esm/autoscale.d.ts +55 -0
  18. package/esm/autoscale.js +252 -0
  19. package/esm/computeYTicks.d.ts +15 -0
  20. package/esm/computeYTicks.js +44 -0
  21. package/esm/constants.d.ts +2 -0
  22. package/esm/constants.js +10 -0
  23. package/esm/dataTypes.d.ts +26 -0
  24. package/esm/dataTypes.js +1 -0
  25. package/esm/displayModel.d.ts +24 -0
  26. package/esm/displayModel.js +1 -0
  27. package/esm/formatNumber.d.ts +1 -0
  28. package/esm/formatNumber.js +6 -0
  29. package/esm/gapBreak.d.ts +59 -0
  30. package/esm/gapBreak.js +58 -0
  31. package/esm/index.d.ts +36 -0
  32. package/esm/index.js +26 -0
  33. package/esm/normalize.d.ts +40 -0
  34. package/esm/normalize.js +106 -0
  35. package/esm/pointMarker.d.ts +3 -0
  36. package/esm/pointMarker.js +27 -0
  37. package/esm/pointSizeMenu.d.ts +13 -0
  38. package/esm/pointSizeMenu.js +21 -0
  39. package/esm/renderingBackendTypes.d.ts +39 -0
  40. package/esm/renderingBackendTypes.js +29 -0
  41. package/esm/resolveRenderState.d.ts +1 -0
  42. package/esm/resolveRenderState.js +20 -0
  43. package/esm/scale.d.ts +60 -0
  44. package/esm/scale.js +161 -0
  45. package/esm/scoreAxisConfigSchemaFields.d.ts +44 -0
  46. package/esm/scoreAxisConfigSchemaFields.js +54 -0
  47. package/esm/scoreMenuItems.d.ts +37 -0
  48. package/esm/scoreMenuItems.js +104 -0
  49. package/esm/scoreRuleMarks.d.ts +73 -0
  50. package/esm/scoreRuleMarks.js +98 -0
  51. package/esm/symlogTicks.d.ts +17 -0
  52. package/esm/symlogTicks.js +92 -0
  53. package/esm/transferables.d.ts +2 -0
  54. package/esm/transferables.js +32 -0
  55. package/esm/vendor/d3-scale.d.ts +33 -0
  56. package/esm/vendor/d3-scale.js +249 -0
  57. package/esm/visibleStatsDomain.d.ts +59 -0
  58. package/esm/visibleStatsDomain.js +33 -0
  59. package/esm/wiggleRenderModes.generated.d.ts +9 -0
  60. package/esm/wiggleRenderModes.generated.js +11 -0
  61. package/esm/yScaleTicks.d.ts +59 -0
  62. package/esm/yScaleTicks.js +59 -0
  63. 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,2 @@
1
+ import type { WiggleDataResult } from './dataTypes.ts';
2
+ export declare function collectWiggleTransferables(results: WiggleDataResult[]): ArrayBuffer[];
@@ -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
+ }