inkplot 0.0.0-stage → 1.0.0-beta.1
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 +21 -0
- package/README.md +46 -2
- package/dist/adapt.d.ts +10 -0
- package/dist/adapt.js +34 -0
- package/dist/annotation.d.ts +120 -0
- package/dist/annotation.js +267 -0
- package/dist/aria.d.ts +24 -0
- package/dist/aria.js +31 -0
- package/dist/attrs.d.ts +81 -0
- package/dist/attrs.js +84 -0
- package/dist/axis.d.ts +137 -0
- package/dist/axis.js +185 -0
- package/dist/bars.d.ts +80 -0
- package/dist/bars.js +255 -0
- package/dist/boxplot.d.ts +57 -0
- package/dist/boxplot.js +177 -0
- package/dist/boxzoom.d.ts +39 -0
- package/dist/boxzoom.js +138 -0
- package/dist/brush.d.ts +60 -0
- package/dist/brush.js +185 -0
- package/dist/bucket.d.ts +22 -0
- package/dist/bucket.js +62 -0
- package/dist/callout.d.ts +30 -0
- package/dist/callout.js +98 -0
- package/dist/chart.d.ts +126 -0
- package/dist/chart.js +257 -0
- package/dist/crosshair.d.ts +28 -0
- package/dist/crosshair.js +78 -0
- package/dist/cursor.d.ts +76 -0
- package/dist/cursor.js +344 -0
- package/dist/curve.d.ts +50 -0
- package/dist/curve.js +87 -0
- package/dist/curves.d.ts +104 -0
- package/dist/curves.js +375 -0
- package/dist/data.d.ts +209 -0
- package/dist/data.js +276 -0
- package/dist/decimation.d.ts +86 -0
- package/dist/decimation.js +410 -0
- package/dist/density.d.ts +55 -0
- package/dist/density.js +252 -0
- package/dist/dimension.d.ts +103 -0
- package/dist/dimension.js +205 -0
- package/dist/dom-platform.d.ts +53 -0
- package/dist/dom-platform.js +190 -0
- package/dist/element.d.ts +232 -0
- package/dist/element.js +414 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.js +237 -0
- package/dist/events.d.ts +144 -0
- package/dist/events.js +19 -0
- package/dist/focus.d.ts +22 -0
- package/dist/focus.js +51 -0
- package/dist/furniture.d.ts +70 -0
- package/dist/furniture.js +166 -0
- package/dist/geometry.d.ts +40 -0
- package/dist/geometry.js +7 -0
- package/dist/heatmap.d.ts +57 -0
- package/dist/heatmap.js +207 -0
- package/dist/hit.d.ts +103 -0
- package/dist/hit.js +428 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +73 -0
- package/dist/input.d.ts +42 -0
- package/dist/input.js +166 -0
- package/dist/jsx-dev-runtime.d.ts +9 -0
- package/dist/jsx-dev-runtime.js +13 -0
- package/dist/jsx-runtime.d.ts +59 -0
- package/dist/jsx-runtime.js +81 -0
- package/dist/key.d.ts +118 -0
- package/dist/key.js +199 -0
- package/dist/legend.d.ts +36 -0
- package/dist/legend.js +220 -0
- package/dist/mapped.d.ts +80 -0
- package/dist/mapped.js +106 -0
- package/dist/marker.d.ts +143 -0
- package/dist/marker.js +213 -0
- package/dist/panzoom.d.ts +119 -0
- package/dist/panzoom.js +467 -0
- package/dist/plane.d.ts +173 -0
- package/dist/plane.js +370 -0
- package/dist/platform.d.ts +113 -0
- package/dist/platform.js +80 -0
- package/dist/png.d.ts +44 -0
- package/dist/png.js +71 -0
- package/dist/pointer.d.ts +54 -0
- package/dist/pointer.js +132 -0
- package/dist/quantile.d.ts +3 -0
- package/dist/quantile.js +11 -0
- package/dist/radial.d.ts +116 -0
- package/dist/radial.js +336 -0
- package/dist/react.d.ts +441 -0
- package/dist/react.js +252 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +51 -0
- package/dist/renderer.d.ts +191 -0
- package/dist/renderer.js +164 -0
- package/dist/scale.d.ts +37 -0
- package/dist/scale.js +19 -0
- package/dist/scales.d.ts +69 -0
- package/dist/scales.js +134 -0
- package/dist/serialize.d.ts +77 -0
- package/dist/serialize.js +321 -0
- package/dist/series.d.ts +304 -0
- package/dist/series.js +513 -0
- package/dist/simple.d.ts +35 -0
- package/dist/simple.js +20 -0
- package/dist/sources.d.ts +116 -0
- package/dist/sources.js +237 -0
- package/dist/stack.d.ts +61 -0
- package/dist/stack.js +172 -0
- package/dist/stats.d.ts +38 -0
- package/dist/stats.js +159 -0
- package/dist/steps.d.ts +50 -0
- package/dist/steps.js +154 -0
- package/dist/stub.d.ts +3 -0
- package/dist/stub.js +5 -0
- package/dist/style-names.d.ts +1 -0
- package/dist/style-names.js +6 -0
- package/dist/summary.d.ts +44 -0
- package/dist/summary.js +58 -0
- package/dist/surface.d.ts +94 -0
- package/dist/surface.js +145 -0
- package/dist/table.d.ts +39 -0
- package/dist/table.js +37 -0
- package/dist/text.d.ts +23 -0
- package/dist/text.js +44 -0
- package/dist/theme.d.ts +174 -0
- package/dist/theme.js +73 -0
- package/dist/themes.d.ts +9 -0
- package/dist/themes.js +56 -0
- package/dist/time-intl.d.ts +194 -0
- package/dist/time-intl.js +144 -0
- package/dist/time-temporal.d.ts +236 -0
- package/dist/time-temporal.js +58 -0
- package/dist/time.d.ts +216 -0
- package/dist/time.js +154 -0
- package/dist/trend.d.ts +151 -0
- package/dist/trend.js +500 -0
- package/dist/web-components.d.ts +62 -0
- package/dist/web-components.js +264 -0
- package/package.json +53 -3
package/dist/data.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data contract and the base of data sources (the sources themselves are in sources.ts): always delivered domain-sorted, read in place when already sorted.
|
|
3
|
+
*
|
|
4
|
+
* The data contract, and why it is shaped this way
|
|
5
|
+
* ─────────────────────────────────────────────────
|
|
6
|
+
* A *point* is a row of numbers: `[domain, value, ...channels]`. The domain is the position along the independent
|
|
7
|
+
* axis; the channels are extra named columns (size, colour, the x of an XY plot) that a series asks for by name.
|
|
8
|
+
*
|
|
9
|
+
* Everything downstream (windowing to the visible range, decimation to the pixel width, path generation) wants the
|
|
10
|
+
* domain *ascending*: it can then binary-search a window and sweep pixel columns in one pass. So the contract is that
|
|
11
|
+
* every stage **delivers** ascending points. How a source achieves that is its own business:
|
|
12
|
+
* - Already sorted (`isSorted`, a regular domain, generated data): read where it is stored, no copy ("in place").
|
|
13
|
+
* - Otherwise: copied once per change into a buffer the source owns ("managed"), NaN domains dropped, sorted if needed.
|
|
14
|
+
* Callers see the same interface either way, which is what lets a million sorted points cost nothing to set up while
|
|
15
|
+
* unsorted object data still just works.
|
|
16
|
+
*
|
|
17
|
+
* Stages form a pipeline (`DataStage`): a source, optionally wrapped by transforms (a Decimator). Each stage answers
|
|
18
|
+
* `plan(context)` with a capacity and a `fill` that writes points into a caller-supplied buffer — the caller owns the
|
|
19
|
+
* memory, so a series can reuse one allocation across redraws. `version` increases on every change and keys all
|
|
20
|
+
* caches (fills, extents). `view()` is the optional zero-copy path used by decimators and extent scans.
|
|
21
|
+
*
|
|
22
|
+
* Extents are tracked per column and reported by `fill`, because the loop that writes a value is the cheapest place
|
|
23
|
+
* to see it. `valueExtent`/`channelExtent` additionally report the extent of *all* the data regardless of the window,
|
|
24
|
+
* which zoom limits and mapped scales need.
|
|
25
|
+
*/
|
|
26
|
+
/* oxlint-disable typescript/no-non-null-assertion -- numeric kernels over flat typed-array buffers (stride-addressed rows): every index is bounded by the row count, which `noUncheckedIndexedAccess` cannot see */
|
|
27
|
+
import { BOOLEAN } from './attrs.js';
|
|
28
|
+
import { ChartElement, fail } from './element.js';
|
|
29
|
+
/** What a source's loader is given: nothing about the plot, since loading from storage does not depend on it. */
|
|
30
|
+
const LOAD_CONTEXT = { width: 0, height: 0, domainRange: { min: -Infinity, max: Infinity }, channels: [] };
|
|
31
|
+
const identity = (i) => i;
|
|
32
|
+
/** The extent of an empty column: min above max, so any real value replaces it and `min <= max` tests emptiness. */
|
|
33
|
+
export const NO_POINTS = { min: Infinity, max: -Infinity };
|
|
34
|
+
/** Shared with sources.ts. Missing values become NaN, which every stage treats as a gap. */
|
|
35
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- `+v` is how any value is coerced to a number, whatever its type
|
|
36
|
+
export const num = (v) => (v === null || v === undefined ? NaN : +v);
|
|
37
|
+
/** The first and last finite values of `n` readings, which for an ascending domain are its extent. */
|
|
38
|
+
export function endsOf(n, at) {
|
|
39
|
+
let min = Infinity, max = -Infinity;
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
const x = at(i);
|
|
42
|
+
if (Number.isFinite(x)) {
|
|
43
|
+
min = x;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
48
|
+
const x = at(i);
|
|
49
|
+
if (Number.isFinite(x)) {
|
|
50
|
+
max = x;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return min <= max ? { min, max } : null;
|
|
55
|
+
}
|
|
56
|
+
/** The extent of every column of `count` rows of `stride` numbers, NaN skipped. The domain column is ascending, so only its ends matter. */
|
|
57
|
+
function columnExtents(buf, count, stride) {
|
|
58
|
+
const out = [count > 0 ? { min: buf[0], max: buf[stride * (count - 1)] } : NO_POINTS];
|
|
59
|
+
for (let c = 1; c < stride; c++) {
|
|
60
|
+
let min = Infinity, max = -Infinity;
|
|
61
|
+
for (let i = 0, j = c; i < count; i++, j += stride) {
|
|
62
|
+
const v = buf[j];
|
|
63
|
+
if (v < min)
|
|
64
|
+
min = v;
|
|
65
|
+
if (v > max)
|
|
66
|
+
max = v;
|
|
67
|
+
}
|
|
68
|
+
out.push(count > 0 && min <= max ? { min, max } : NO_POINTS);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Everything the contract needs that does not depend on where the numbers are stored: ordering, the managed buffer,
|
|
74
|
+
* wrapping, extents and caching. A concrete source supplies only `fill`, `scanDomain` and (if it can) `orderedView`.
|
|
75
|
+
*/
|
|
76
|
+
export class DataSource extends ChartElement {
|
|
77
|
+
static attrTypes = { ...ChartElement.attrTypes, isSorted: BOOLEAN };
|
|
78
|
+
#version = 0;
|
|
79
|
+
domainCache = null;
|
|
80
|
+
valueCache = null;
|
|
81
|
+
channelCache = null;
|
|
82
|
+
/** Increases whenever the data changes. Cached fills are keyed on it. */
|
|
83
|
+
get version() { return this.#version; }
|
|
84
|
+
/**
|
|
85
|
+
* True when `fill` already writes ascending points, so nothing needs arranging: the data is flagged sorted,
|
|
86
|
+
* or the source generates it in order. A generated source overrides this.
|
|
87
|
+
*/
|
|
88
|
+
get ordered() { return this.attrs.isSorted === true; }
|
|
89
|
+
/** Ordered, and so read where it is stored: not when wrapped, since the extra point has to live somewhere. */
|
|
90
|
+
get inPlace() { return this.ordered && this.attrs.wrap === undefined; }
|
|
91
|
+
/** The channels this source can deliver, in the order `managed` stores them. Default none. */
|
|
92
|
+
get channelNames() { return []; }
|
|
93
|
+
/** An `ordered` source that can be read in place offers its storage here. Default: nothing to offer. */
|
|
94
|
+
orderedView() { return null; }
|
|
95
|
+
// The managed copy is rebuilt lazily, at most once per version, and only for sources that need arranging.
|
|
96
|
+
#managed = null;
|
|
97
|
+
#loadBuffer = new Float64Array(0);
|
|
98
|
+
/**
|
|
99
|
+
* The points in a buffer this source owns, ascending, rebuilt lazily after each change. An ordered source is
|
|
100
|
+
* only copied; any other has NaN-domain points dropped and is sorted (stably) unless it happens to be in order.
|
|
101
|
+
*/
|
|
102
|
+
managed() {
|
|
103
|
+
const cached = this.#managed;
|
|
104
|
+
if (cached && cached.version === this.version)
|
|
105
|
+
return cached;
|
|
106
|
+
const names = this.channelNames, stride = 2 + names.length, n = this.pointCount;
|
|
107
|
+
if (this.#loadBuffer.length < stride * (n + 1))
|
|
108
|
+
this.#loadBuffer = new Float64Array(stride * (n + 1));
|
|
109
|
+
const loaded = this.fill(this.#loadBuffer, { ...LOAD_CONTEXT, channels: names });
|
|
110
|
+
let buf = this.#loadBuffer, count = loaded.count, extents = loaded.extents;
|
|
111
|
+
let rows = null;
|
|
112
|
+
if (!this.inPlace) {
|
|
113
|
+
// One pass: close the gaps left by NaN domains (compacting in place) and notice whether the order is already right.
|
|
114
|
+
// `origin` (made at the first gap) remembers which stored row each kept point came from.
|
|
115
|
+
let kept = 0, ascending = true, prev = -Infinity, origin = null;
|
|
116
|
+
for (let i = 0; i < count; i++) {
|
|
117
|
+
const x = buf[stride * i];
|
|
118
|
+
if (!Number.isFinite(x)) {
|
|
119
|
+
if (!origin) {
|
|
120
|
+
origin = new Uint32Array(count);
|
|
121
|
+
for (let k = 0; k < kept; k++)
|
|
122
|
+
origin[k] = k;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (x < prev)
|
|
127
|
+
ascending = false;
|
|
128
|
+
prev = x;
|
|
129
|
+
if (kept !== i)
|
|
130
|
+
for (let c = 0; c < stride; c++)
|
|
131
|
+
buf[stride * kept + c] = buf[stride * i + c];
|
|
132
|
+
if (origin)
|
|
133
|
+
origin[kept] = i;
|
|
134
|
+
kept++;
|
|
135
|
+
}
|
|
136
|
+
rows = origin;
|
|
137
|
+
count = kept;
|
|
138
|
+
if (!ascending) {
|
|
139
|
+
// Sort an index rather than the rows, with the index as tie-break so equal domains keep their original order.
|
|
140
|
+
const order = new Uint32Array(count);
|
|
141
|
+
for (let i = 0; i < count; i++)
|
|
142
|
+
order[i] = i;
|
|
143
|
+
order.sort((a, b) => buf[stride * a] - buf[stride * b] || a - b);
|
|
144
|
+
const sorted = new Float64Array(stride * (count + 1));
|
|
145
|
+
for (let i = 0; i < count; i++)
|
|
146
|
+
for (let c = 0; c < stride; c++)
|
|
147
|
+
sorted[stride * i + c] = buf[stride * order[i] + c];
|
|
148
|
+
buf = sorted;
|
|
149
|
+
rows = order.map((o) => (origin ? origin[o] : o));
|
|
150
|
+
}
|
|
151
|
+
// The closing point is a copy of the first one period on; the loader allocated one spare row for it.
|
|
152
|
+
const wrap = this.attrs.wrap;
|
|
153
|
+
if (wrap !== undefined && count > 0) {
|
|
154
|
+
if (buf[0] + wrap < buf[stride * (count - 1)])
|
|
155
|
+
fail('the wrap period is shorter than the domain of the data');
|
|
156
|
+
for (let c = 1; c < stride; c++)
|
|
157
|
+
buf[stride * count + c] = buf[c];
|
|
158
|
+
buf[stride * count] = buf[0] + wrap;
|
|
159
|
+
// The copy is the first point again, so an edit to it lands on the first point's row.
|
|
160
|
+
const longer = new Uint32Array(count + 1);
|
|
161
|
+
if (rows)
|
|
162
|
+
longer.set(rows.subarray(0, count));
|
|
163
|
+
else
|
|
164
|
+
for (let k = 0; k < count; k++)
|
|
165
|
+
longer[k] = k;
|
|
166
|
+
longer[count] = longer[0];
|
|
167
|
+
rows = longer;
|
|
168
|
+
count++;
|
|
169
|
+
}
|
|
170
|
+
extents = columnExtents(buf, count, stride);
|
|
171
|
+
}
|
|
172
|
+
return (this.#managed = { version: this.version, data: buf, count, stride, extents, rows });
|
|
173
|
+
}
|
|
174
|
+
/** The managed buffer as a view. */
|
|
175
|
+
managedView() {
|
|
176
|
+
const m = this.managed();
|
|
177
|
+
return { data: m.data, count: m.count, stride: m.stride, domainOffset: 0, valueOffset: 1, regularDomain: null };
|
|
178
|
+
}
|
|
179
|
+
/** The points ascending in the domain, to read in place; valid until the next change. Null: use `plan`. */
|
|
180
|
+
view() { return this.inPlace ? this.orderedView() : this.managedView(); }
|
|
181
|
+
/** Any change to the data bumps the version, which is what discards cached fills. */
|
|
182
|
+
invalidate() {
|
|
183
|
+
this.#version++;
|
|
184
|
+
super.invalidate();
|
|
185
|
+
}
|
|
186
|
+
/** Cached per version; an in-place source only looks at its two ends, a managed one already knows. */
|
|
187
|
+
domain() {
|
|
188
|
+
const c = this.domainCache;
|
|
189
|
+
if (c && c.version === this.version)
|
|
190
|
+
return c.extent;
|
|
191
|
+
const extent = this.inPlace ? this.scanDomain() : (this.managed().extents[0] ?? null);
|
|
192
|
+
const result = extent && extent.min <= extent.max ? extent : null;
|
|
193
|
+
this.domainCache = { version: this.version, extent: result };
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The extent of all the values, gaps skipped. A scan of the stored points, made once per change and only when asked,
|
|
198
|
+
* for anything that needs the whole range while drawing only part of it (the limits of a zoom).
|
|
199
|
+
*/
|
|
200
|
+
valueExtent() {
|
|
201
|
+
const c = this.valueCache;
|
|
202
|
+
if (c && c.version === this.version)
|
|
203
|
+
return c.extent;
|
|
204
|
+
let min = Infinity, max = -Infinity;
|
|
205
|
+
const v = this.view();
|
|
206
|
+
if (v) {
|
|
207
|
+
const d = v.data, s = v.stride, n = v.count;
|
|
208
|
+
for (let i = 0, j = v.valueOffset; i < n; i++, j += s) {
|
|
209
|
+
const y = d[j];
|
|
210
|
+
if (y < min)
|
|
211
|
+
min = y;
|
|
212
|
+
if (y > max)
|
|
213
|
+
max = y;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const plan = this.plan(LOAD_CONTEXT), extent = plan.fill(new Float64Array(2 * plan.capacity)).extents[1];
|
|
218
|
+
if (extent) {
|
|
219
|
+
min = extent.min;
|
|
220
|
+
max = extent.max;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const extent = min <= max ? { min, max } : null;
|
|
224
|
+
this.valueCache = { version: this.version, extent };
|
|
225
|
+
return extent;
|
|
226
|
+
}
|
|
227
|
+
/** Like `valueExtent` for one channel: a scan made once per change and name, for anything that needs the whole range. */
|
|
228
|
+
channelExtent(name) {
|
|
229
|
+
let c = this.channelCache;
|
|
230
|
+
if (!c || c.version !== this.version)
|
|
231
|
+
c = this.channelCache = { version: this.version, byName: new Map() };
|
|
232
|
+
const known = c.byName.get(name);
|
|
233
|
+
if (known !== undefined)
|
|
234
|
+
return known;
|
|
235
|
+
const plan = this.plan({ ...LOAD_CONTEXT, channels: [name] }), column = plan.fill(new Float64Array(3 * plan.capacity)).extents[2];
|
|
236
|
+
const extent = column && column.min <= column.max ? column : null;
|
|
237
|
+
c.byName.set(name, extent);
|
|
238
|
+
return extent;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* In place: fill straight from storage in the requested column order. Managed: copy the sorted buffer, repacking rows
|
|
242
|
+
* only when the series wants a different selection of channels than the source stores.
|
|
243
|
+
*/
|
|
244
|
+
plan(context) {
|
|
245
|
+
if (this.inPlace)
|
|
246
|
+
return { capacity: this.pointCount, fill: (out) => ({ ...this.fill(out, context), rowOf: identity }) };
|
|
247
|
+
const m = this.managed(), want = context.channels, have = this.channelNames;
|
|
248
|
+
const rows = m.rows, rowOf = rows ? (i) => rows[i] : identity;
|
|
249
|
+
if (want.length === have.length && want.every((w, i) => w === have[i])) {
|
|
250
|
+
return {
|
|
251
|
+
capacity: m.count,
|
|
252
|
+
fill: (out) => { out.set(m.data.subarray(0, m.stride * m.count)); return { count: m.count, extents: m.extents, rowOf }; },
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// A different selection of channels: repack each row.
|
|
256
|
+
const columns = want.map((name) => { const i = have.indexOf(name); return i < 0 ? fail(`the data has no channel "${name}"`) : 2 + i; });
|
|
257
|
+
const out2 = 2 + want.length;
|
|
258
|
+
return {
|
|
259
|
+
capacity: m.count,
|
|
260
|
+
fill: (out) => {
|
|
261
|
+
for (let i = 0; i < m.count; i++) {
|
|
262
|
+
out[out2 * i] = m.data[m.stride * i];
|
|
263
|
+
out[out2 * i + 1] = m.data[m.stride * i + 1];
|
|
264
|
+
for (let c = 0; c < columns.length; c++)
|
|
265
|
+
out[out2 * i + 2 + c] = m.data[m.stride * i + columns[c]];
|
|
266
|
+
}
|
|
267
|
+
return { count: m.count, extents: [m.extents[0], m.extents[1], ...columns.map((c) => m.extents[c])], rowOf };
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** A stage that takes another stage as input: a decimator, a filter, a resampler. It has no storage of its own, so it implements the stage interface directly. */
|
|
273
|
+
export class DataTransform extends ChartElement {
|
|
274
|
+
/** The channels it delivers by name. Default none; a transform that passes its source's through, or makes its own, says so. */
|
|
275
|
+
get channelNames() { return []; }
|
|
276
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decimation: reducing many points to the few a plot can show, by pixel column.
|
|
3
|
+
*
|
|
4
|
+
* Why: a line over a million points cannot show more than about one distinct value per pixel column, yet generating,
|
|
5
|
+
* normalising and stringifying a million points is the dominant cost of a redraw. Reducing to the extremes of each
|
|
6
|
+
* column first keeps the picture (to within a pixel) at a cost proportional to the plot's width, not the data's size.
|
|
7
|
+
*
|
|
8
|
+
* Structure: a `Decimator` (a data transform) owns a `DecimationKernel` (the reduction rule, swappable) and a source.
|
|
9
|
+
* It windows the source to the visible domain, hands the kernel a zero-copy `PointView`, and reports extents of what the
|
|
10
|
+
* kernel *saw* so the value axis fits the visible data rather than the whole of it.
|
|
11
|
+
*/
|
|
12
|
+
import { type DataNode, DataTransform, type FillPlan, type PlanContext, type PointView } from './data.ts';
|
|
13
|
+
import { type BaseAttributes, ChartElement } from './element.ts';
|
|
14
|
+
import type { Extent } from './geometry.ts';
|
|
15
|
+
/** Tuning for the reduction. */
|
|
16
|
+
export interface DecimatorAttributes extends BaseAttributes {
|
|
17
|
+
/** Pixel columns reduced to per pixel of plot width. Default 1; raise it for high-DPI output. */
|
|
18
|
+
resolution?: number;
|
|
19
|
+
/** Turn it off without removing it. Default true. */
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** What a kernel reports besides the points it wrote: the value extent of the data it looked at. */
|
|
23
|
+
export interface ReduceResult {
|
|
24
|
+
readonly count: number;
|
|
25
|
+
readonly yMin: number;
|
|
26
|
+
readonly yMax: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The reduction itself. A kernel is an element so its parameters serialize and a new one is just a class.
|
|
30
|
+
* Kernels read the source's storage in place; sources always deliver the domain ascending.
|
|
31
|
+
*/
|
|
32
|
+
export declare abstract class DecimationKernel<A extends BaseAttributes = BaseAttributes> extends ChartElement<Decimator, [], A> {
|
|
33
|
+
/** The most points `reduce` can write for this many columns. */
|
|
34
|
+
abstract capacity(columns: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Reduce `view` to at most `capacity` points over `domainRange`, writing (domain, value) pairs into `out`.
|
|
37
|
+
* Points outside the range land in the edge columns. A gap (NaN value) is kept as one NaN point if the kernel handles gaps.
|
|
38
|
+
*/
|
|
39
|
+
abstract reduce(view: PointView, domainRange: Extent, columns: number, out: Float64Array, capacity: number): ReduceResult;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The lowest and highest value in each pixel column, in the order they occur. Every extreme survives, so the
|
|
43
|
+
* drawn line covers what the full data would to within a pixel. Keeps gaps. At most 2 points per column.
|
|
44
|
+
*/
|
|
45
|
+
export declare class MinMaxKernel extends DecimationKernel {
|
|
46
|
+
capacity(columns: number): number;
|
|
47
|
+
reduce(view: PointView, range: Extent, columns: number, out: Float64Array, capacity: number): ReduceResult;
|
|
48
|
+
}
|
|
49
|
+
/** Min/max plus the first and last value in each column, which keeps joins between columns exact. At most 4 per column. */
|
|
50
|
+
export declare class M4Kernel extends DecimationKernel {
|
|
51
|
+
capacity(columns: number): number;
|
|
52
|
+
reduce(view: PointView, range: Extent, columns: number, out: Float64Array, capacity: number): ReduceResult;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Largest Triangle Three Buckets: one point per column, chosen to keep the line's shape. It does not keep extremes,
|
|
56
|
+
* so spikes can vanish, and it does not handle gaps (a NaN value poisons its bucket). Prefer MinMaxKernel.
|
|
57
|
+
*/
|
|
58
|
+
export declare class LttbKernel extends DecimationKernel {
|
|
59
|
+
capacity(columns: number): number;
|
|
60
|
+
reduce(v: PointView, _range: Extent, W: number, out: Float64Array, cap: number): ReduceResult;
|
|
61
|
+
}
|
|
62
|
+
/** Every k-th point. The cheapest possible, and it aliases: extremes are lost. For previews and for comparison. */
|
|
63
|
+
export declare class StrideKernel extends DecimationKernel {
|
|
64
|
+
capacity(columns: number): number;
|
|
65
|
+
reduce(v: PointView, _range: Extent, columns: number, out: Float64Array, cap: number): ReduceResult;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Sits between a series and its source and reduces the source to what the plot can show:
|
|
69
|
+
* `new Series(new Decimator({}, new MinMaxKernel(), source), new LinePath(new LinearCurve()))`.
|
|
70
|
+
* Only the visible part of the domain is read, so zooming in refills from the full data again.
|
|
71
|
+
* Sources always deliver points ascending in the domain, so any source works; one flagged `isSorted` is read in place.
|
|
72
|
+
*/
|
|
73
|
+
export declare class Decimator extends DataTransform<DecimatorAttributes, [kernel: DecimationKernel, source: DataNode]> {
|
|
74
|
+
#private;
|
|
75
|
+
get kernel(): DecimationKernel;
|
|
76
|
+
get source(): DataNode;
|
|
77
|
+
get channelNames(): readonly string[];
|
|
78
|
+
/** Anything beneath bubbles an invalidation up through here, so one counter covers source, kernel and attributes. */
|
|
79
|
+
invalidate(): void;
|
|
80
|
+
get version(): number;
|
|
81
|
+
get pointCount(): number;
|
|
82
|
+
domain(): Extent | null;
|
|
83
|
+
valueExtent(): Extent | null;
|
|
84
|
+
channelExtent(name: string): Extent | null;
|
|
85
|
+
plan(context: PlanContext): FillPlan;
|
|
86
|
+
}
|