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
|
@@ -0,0 +1,410 @@
|
|
|
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
|
+
/* 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 */
|
|
13
|
+
import { DataSource, DataTransform, NO_POINTS } from './data.js';
|
|
14
|
+
import { ChartElement, fail } from './element.js';
|
|
15
|
+
/**
|
|
16
|
+
* The reduction itself. A kernel is an element so its parameters serialize and a new one is just a class.
|
|
17
|
+
* Kernels read the source's storage in place; sources always deliver the domain ascending.
|
|
18
|
+
*/
|
|
19
|
+
export class DecimationKernel extends ChartElement {
|
|
20
|
+
}
|
|
21
|
+
/** Writes point `i` of a view into `out` slot `m`, if there is room. */
|
|
22
|
+
function putPoint(v, out, m, cap, i) {
|
|
23
|
+
if (m < cap) {
|
|
24
|
+
const r = v.regularDomain;
|
|
25
|
+
out[2 * m] = r ? r.start + r.step * i : v.data[i * v.stride + v.domainOffset];
|
|
26
|
+
out[2 * m + 1] = v.data[i * v.stride + v.valueOffset];
|
|
27
|
+
m++;
|
|
28
|
+
}
|
|
29
|
+
return m;
|
|
30
|
+
}
|
|
31
|
+
/** A gap point, unless the last point written already is one. */
|
|
32
|
+
function putGap(v, out, m, cap, i) {
|
|
33
|
+
if (m < cap && !(m > 0 && out[2 * m - 1] !== out[2 * m - 1])) {
|
|
34
|
+
const r = v.regularDomain;
|
|
35
|
+
out[2 * m] = r ? r.start + r.step * i : v.data[i * v.stride + v.domainOffset];
|
|
36
|
+
out[2 * m + 1] = NaN;
|
|
37
|
+
m++;
|
|
38
|
+
}
|
|
39
|
+
return m;
|
|
40
|
+
}
|
|
41
|
+
/** Writes the kept points of one column in domain order: [first,] min/max by position, [last]. */
|
|
42
|
+
function emitRun(v, out, m, cap, keepEnds, f, l, im, iM) {
|
|
43
|
+
const a = im < iM ? im : iM, b = im < iM ? iM : im;
|
|
44
|
+
if (!keepEnds) {
|
|
45
|
+
m = putPoint(v, out, m, cap, a);
|
|
46
|
+
if (b !== a)
|
|
47
|
+
m = putPoint(v, out, m, cap, b);
|
|
48
|
+
return m;
|
|
49
|
+
}
|
|
50
|
+
let prev = -1;
|
|
51
|
+
if (f !== prev) {
|
|
52
|
+
m = putPoint(v, out, m, cap, f);
|
|
53
|
+
prev = f;
|
|
54
|
+
}
|
|
55
|
+
if (a !== prev) {
|
|
56
|
+
m = putPoint(v, out, m, cap, a);
|
|
57
|
+
prev = a;
|
|
58
|
+
}
|
|
59
|
+
if (b !== prev) {
|
|
60
|
+
m = putPoint(v, out, m, cap, b);
|
|
61
|
+
prev = b;
|
|
62
|
+
}
|
|
63
|
+
if (l !== prev) {
|
|
64
|
+
m = putPoint(v, out, m, cap, l);
|
|
65
|
+
}
|
|
66
|
+
return m;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The value extent of the data one `reduceByColumn` call saw. Module-level so the hot loop returns nothing but a count
|
|
70
|
+
* and allocates nothing; safe because reduction is synchronous and never re-entered.
|
|
71
|
+
*/
|
|
72
|
+
const runExtent = { min: Infinity, max: -Infinity };
|
|
73
|
+
/** Slow path for a column that contains NaN: split it into runs between the gaps and reduce each. */
|
|
74
|
+
function reduceAroundGaps(v, i0, i1, out, m, cap, keepEnds) {
|
|
75
|
+
const d = v.data, s = v.stride, yo = v.valueOffset;
|
|
76
|
+
let run = false, f = 0, l = 0, mn = 0, mx = 0, im = 0, iM = 0;
|
|
77
|
+
for (let i = i0; i <= i1; i++) {
|
|
78
|
+
const y = i < i1 ? d[i * s + yo] : NaN;
|
|
79
|
+
if (y !== y) {
|
|
80
|
+
if (run) {
|
|
81
|
+
if (mn < runExtent.min)
|
|
82
|
+
runExtent.min = mn;
|
|
83
|
+
if (mx > runExtent.max)
|
|
84
|
+
runExtent.max = mx;
|
|
85
|
+
m = emitRun(v, out, m, cap, keepEnds, f, l, im, iM);
|
|
86
|
+
run = false;
|
|
87
|
+
}
|
|
88
|
+
if (i < i1)
|
|
89
|
+
m = putGap(v, out, m, cap, i);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!run) {
|
|
93
|
+
run = true;
|
|
94
|
+
f = i;
|
|
95
|
+
mn = mx = y;
|
|
96
|
+
im = iM = i;
|
|
97
|
+
}
|
|
98
|
+
else if (y < mn) {
|
|
99
|
+
mn = y;
|
|
100
|
+
im = i;
|
|
101
|
+
}
|
|
102
|
+
else if (y > mx) {
|
|
103
|
+
mx = y;
|
|
104
|
+
iM = i;
|
|
105
|
+
}
|
|
106
|
+
l = i;
|
|
107
|
+
}
|
|
108
|
+
return m;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Per pixel column, find the range of points it covers (arithmetic for a regular domain, binary search otherwise),
|
|
112
|
+
* then scan only the values in that range. Four values at a time are checked against the running extremes,
|
|
113
|
+
* and a group strictly inside them costs one comparison chain and nothing else.
|
|
114
|
+
*/
|
|
115
|
+
function reduceByColumn(v, domainRange, W, out, cap, keepEnds) {
|
|
116
|
+
const d = v.data, s = v.stride, xo = v.domainOffset, yo = v.valueOffset, n = v.count;
|
|
117
|
+
const reg = v.regularDomain;
|
|
118
|
+
const ux = reg ? reg.start : 0, dx = reg ? reg.step : 1;
|
|
119
|
+
const x0 = domainRange.min, k = W / (domainRange.max - domainRange.min);
|
|
120
|
+
runExtent.min = Infinity;
|
|
121
|
+
runExtent.max = -Infinity;
|
|
122
|
+
let m = 0, i0 = 0;
|
|
123
|
+
for (let c = 0; c < W && i0 < n; c++) {
|
|
124
|
+
let i1;
|
|
125
|
+
if (c === W - 1)
|
|
126
|
+
i1 = n;
|
|
127
|
+
else if (reg) {
|
|
128
|
+
i1 = Math.ceil((x0 + (c + 1) / k - ux) / dx);
|
|
129
|
+
if (i1 > n)
|
|
130
|
+
i1 = n;
|
|
131
|
+
if (i1 < i0)
|
|
132
|
+
i1 = i0;
|
|
133
|
+
// Agree exactly with the multiply-and-truncate the column test uses.
|
|
134
|
+
while (i1 > i0 && (((ux + dx * (i1 - 1) - x0) * k) | 0) > c)
|
|
135
|
+
i1--;
|
|
136
|
+
while (i1 < n && (((ux + dx * i1 - x0) * k) | 0) <= c)
|
|
137
|
+
i1++;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
let lo = i0, hi = n;
|
|
141
|
+
while (lo < hi) {
|
|
142
|
+
const mid = (lo + hi) >>> 1;
|
|
143
|
+
if ((((d[mid * s + xo] - x0) * k) | 0) <= c)
|
|
144
|
+
lo = mid + 1;
|
|
145
|
+
else
|
|
146
|
+
hi = mid;
|
|
147
|
+
}
|
|
148
|
+
i1 = lo;
|
|
149
|
+
}
|
|
150
|
+
if (i1 <= i0)
|
|
151
|
+
continue;
|
|
152
|
+
const y0 = d[i0 * s + yo];
|
|
153
|
+
let mn = y0, mx = y0, im = i0, iM = i0, nan = y0 !== y0;
|
|
154
|
+
let i = i0 + 1, p = i * s + yo;
|
|
155
|
+
const s2 = 2 * s, s3 = 3 * s;
|
|
156
|
+
for (; i + 3 < i1; i += 4, p += 4 * s) {
|
|
157
|
+
const a = d[p], b = d[p + s], e = d[p + s2], f = d[p + s3];
|
|
158
|
+
// Four values strictly inside (min, max) change nothing. NaN fails both compares, so it falls
|
|
159
|
+
// through to the careful loop and is flagged there.
|
|
160
|
+
if (a > mn && a < mx && b > mn && b < mx && e > mn && e < mx && f > mn && f < mx)
|
|
161
|
+
continue;
|
|
162
|
+
for (let j = i, q = p; j < i + 4; j++, q += s) {
|
|
163
|
+
const y = d[q];
|
|
164
|
+
if (y < mn) {
|
|
165
|
+
mn = y;
|
|
166
|
+
im = j;
|
|
167
|
+
}
|
|
168
|
+
else if (y > mx) {
|
|
169
|
+
mx = y;
|
|
170
|
+
iM = j;
|
|
171
|
+
}
|
|
172
|
+
else if (y !== y)
|
|
173
|
+
nan = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (; i < i1; i++, p += s) {
|
|
177
|
+
const y = d[p];
|
|
178
|
+
if (y < mn) {
|
|
179
|
+
mn = y;
|
|
180
|
+
im = i;
|
|
181
|
+
}
|
|
182
|
+
else if (y > mx) {
|
|
183
|
+
mx = y;
|
|
184
|
+
iM = i;
|
|
185
|
+
}
|
|
186
|
+
else if (y !== y)
|
|
187
|
+
nan = true;
|
|
188
|
+
}
|
|
189
|
+
if (nan)
|
|
190
|
+
m = reduceAroundGaps(v, i0, i1, out, m, cap, keepEnds);
|
|
191
|
+
else {
|
|
192
|
+
if (mn < runExtent.min)
|
|
193
|
+
runExtent.min = mn;
|
|
194
|
+
if (mx > runExtent.max)
|
|
195
|
+
runExtent.max = mx;
|
|
196
|
+
m = emitRun(v, out, m, cap, keepEnds, i0, i1 - 1, im, iM);
|
|
197
|
+
}
|
|
198
|
+
i0 = i1;
|
|
199
|
+
}
|
|
200
|
+
return { count: m, yMin: runExtent.min, yMax: runExtent.max };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The lowest and highest value in each pixel column, in the order they occur. Every extreme survives, so the
|
|
204
|
+
* drawn line covers what the full data would to within a pixel. Keeps gaps. At most 2 points per column.
|
|
205
|
+
*/
|
|
206
|
+
export class MinMaxKernel extends DecimationKernel {
|
|
207
|
+
capacity(columns) { return 2 * columns + 2; }
|
|
208
|
+
reduce(view, range, columns, out, capacity) {
|
|
209
|
+
return reduceByColumn(view, range, columns, out, capacity, false);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Min/max plus the first and last value in each column, which keeps joins between columns exact. At most 4 per column. */
|
|
213
|
+
export class M4Kernel extends DecimationKernel {
|
|
214
|
+
capacity(columns) { return 4 * columns + 4; }
|
|
215
|
+
reduce(view, range, columns, out, capacity) {
|
|
216
|
+
return reduceByColumn(view, range, columns, out, capacity, true);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Largest Triangle Three Buckets: one point per column, chosen to keep the line's shape. It does not keep extremes,
|
|
221
|
+
* so spikes can vanish, and it does not handle gaps (a NaN value poisons its bucket). Prefer MinMaxKernel.
|
|
222
|
+
*/
|
|
223
|
+
export class LttbKernel extends DecimationKernel {
|
|
224
|
+
capacity(columns) { return columns + 2; }
|
|
225
|
+
reduce(v, _range, W, out, cap) {
|
|
226
|
+
const d = v.data, s = v.stride, xo = v.domainOffset, yo = v.valueOffset, n = v.count, reg = v.regularDomain;
|
|
227
|
+
const ux = reg ? reg.start : 0, dx = reg ? reg.step : 1;
|
|
228
|
+
let m = 0, yMin = Infinity, yMax = -Infinity;
|
|
229
|
+
m = putPoint(v, out, m, cap, 0);
|
|
230
|
+
const every = (n - 2) / (W - 2);
|
|
231
|
+
let a = 0;
|
|
232
|
+
for (let b = 0; b < W - 2; b++) {
|
|
233
|
+
const ns = Math.floor((b + 1) * every) + 1;
|
|
234
|
+
let ne = Math.floor((b + 2) * every) + 1;
|
|
235
|
+
if (ne > n)
|
|
236
|
+
ne = n;
|
|
237
|
+
let avgX = 0, avgY = 0;
|
|
238
|
+
for (let i = ns; i < ne; i++) {
|
|
239
|
+
avgX += reg ? ux + dx * i : d[i * s + xo];
|
|
240
|
+
avgY += d[i * s + yo];
|
|
241
|
+
}
|
|
242
|
+
const cnt = ne - ns;
|
|
243
|
+
avgX /= cnt;
|
|
244
|
+
avgY /= cnt;
|
|
245
|
+
const rs = Math.floor(b * every) + 1, re = Math.floor((b + 1) * every) + 1;
|
|
246
|
+
const ax = reg ? ux + dx * a : d[a * s + xo], ay = d[a * s + yo];
|
|
247
|
+
let best = -1, bi = rs;
|
|
248
|
+
for (let i = rs; i < re; i++) {
|
|
249
|
+
const x = reg ? ux + dx * i : d[i * s + xo], y = d[i * s + yo];
|
|
250
|
+
if (y < yMin)
|
|
251
|
+
yMin = y;
|
|
252
|
+
if (y > yMax)
|
|
253
|
+
yMax = y;
|
|
254
|
+
const area = Math.abs((ax - avgX) * (y - ay) - (ax - x) * (avgY - ay));
|
|
255
|
+
if (area > best) {
|
|
256
|
+
best = area;
|
|
257
|
+
bi = i;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
m = putPoint(v, out, m, cap, bi);
|
|
261
|
+
a = bi;
|
|
262
|
+
}
|
|
263
|
+
m = putPoint(v, out, m, cap, n - 1);
|
|
264
|
+
const y0 = d[yo], yl = d[(n - 1) * s + yo];
|
|
265
|
+
if (y0 < yMin)
|
|
266
|
+
yMin = y0;
|
|
267
|
+
if (y0 > yMax)
|
|
268
|
+
yMax = y0;
|
|
269
|
+
if (yl < yMin)
|
|
270
|
+
yMin = yl;
|
|
271
|
+
if (yl > yMax)
|
|
272
|
+
yMax = yl;
|
|
273
|
+
return { count: m, yMin, yMax };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Every k-th point. The cheapest possible, and it aliases: extremes are lost. For previews and for comparison. */
|
|
277
|
+
export class StrideKernel extends DecimationKernel {
|
|
278
|
+
capacity(columns) { return columns + 2; }
|
|
279
|
+
reduce(v, _range, columns, out, cap) {
|
|
280
|
+
const n = v.count, d = v.data, s = v.stride, yo = v.valueOffset;
|
|
281
|
+
let m = 0, yMin = Infinity, yMax = -Infinity;
|
|
282
|
+
const steps = Math.min(columns, n);
|
|
283
|
+
for (let j = 0; j < steps; j++) {
|
|
284
|
+
const i = steps === 1 ? 0 : Math.round(j * (n - 1) / (steps - 1));
|
|
285
|
+
const y = d[i * s + yo];
|
|
286
|
+
if (y < yMin)
|
|
287
|
+
yMin = y;
|
|
288
|
+
if (y > yMax)
|
|
289
|
+
yMax = y;
|
|
290
|
+
m = putPoint(v, out, m, cap, i);
|
|
291
|
+
}
|
|
292
|
+
return { count: m, yMin, yMax };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Narrows a view to the points that can touch `range`, plus one either side so lines enter and leave correctly.
|
|
297
|
+
* Needs a sorted domain and storage that can be sliced without copying (a typed array); otherwise returns the view.
|
|
298
|
+
*/
|
|
299
|
+
function windowView(v, range) {
|
|
300
|
+
const n = v.count, r = v.regularDomain;
|
|
301
|
+
if (n < 3)
|
|
302
|
+
return v;
|
|
303
|
+
const x = r ? (i) => r.start + r.step * i : (i) => v.data[i * v.stride + v.domainOffset];
|
|
304
|
+
if (x(0) >= range.min && x(n - 1) <= range.max)
|
|
305
|
+
return v;
|
|
306
|
+
let lo = 0, hi = n;
|
|
307
|
+
while (lo < hi) {
|
|
308
|
+
const mid = (lo + hi) >>> 1;
|
|
309
|
+
if (x(mid) < range.min)
|
|
310
|
+
lo = mid + 1;
|
|
311
|
+
else
|
|
312
|
+
hi = mid;
|
|
313
|
+
}
|
|
314
|
+
const first = lo > 0 ? lo - 1 : 0;
|
|
315
|
+
hi = n;
|
|
316
|
+
while (lo < hi) {
|
|
317
|
+
const mid = (lo + hi) >>> 1;
|
|
318
|
+
if (x(mid) <= range.max)
|
|
319
|
+
lo = mid + 1;
|
|
320
|
+
else
|
|
321
|
+
hi = mid;
|
|
322
|
+
}
|
|
323
|
+
const end = lo < n ? lo + 1 : n;
|
|
324
|
+
if (first === 0 && end === n)
|
|
325
|
+
return v;
|
|
326
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- typed arrays have `subarray`, plain arrays do not: it is probed on the next line
|
|
327
|
+
const data = v.data;
|
|
328
|
+
if (typeof data.subarray !== 'function')
|
|
329
|
+
return v;
|
|
330
|
+
return {
|
|
331
|
+
...v,
|
|
332
|
+
data: data.subarray(first * v.stride),
|
|
333
|
+
count: end - first,
|
|
334
|
+
regularDomain: r ? { start: r.start + r.step * first, step: r.step } : null,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Sits between a series and its source and reduces the source to what the plot can show:
|
|
339
|
+
* `new Series(new Decimator({}, new MinMaxKernel(), source), new LinePath(new LinearCurve()))`.
|
|
340
|
+
* Only the visible part of the domain is read, so zooming in refills from the full data again.
|
|
341
|
+
* Sources always deliver points ascending in the domain, so any source works; one flagged `isSorted` is read in place.
|
|
342
|
+
*/
|
|
343
|
+
export class Decimator extends DataTransform {
|
|
344
|
+
#version = 0;
|
|
345
|
+
#scratch = new Float64Array(0);
|
|
346
|
+
get kernel() { return this.child(0, DecimationKernel); }
|
|
347
|
+
get source() { return this.child(1, DataSource, DataTransform); }
|
|
348
|
+
get channelNames() { return this.source.channelNames ?? []; }
|
|
349
|
+
/** Anything beneath bubbles an invalidation up through here, so one counter covers source, kernel and attributes. */
|
|
350
|
+
invalidate() {
|
|
351
|
+
this.#version++;
|
|
352
|
+
super.invalidate();
|
|
353
|
+
}
|
|
354
|
+
get version() { return this.#version; }
|
|
355
|
+
get pointCount() { return this.source.pointCount; }
|
|
356
|
+
domain() { return this.source.domain(); }
|
|
357
|
+
valueExtent() { return this.source.valueExtent(); }
|
|
358
|
+
channelExtent(name) { return this.source.channelExtent(name); }
|
|
359
|
+
plan(context) {
|
|
360
|
+
const source = this.source, kernel = this.kernel, a = this.attrs;
|
|
361
|
+
const inner = source.plan(context);
|
|
362
|
+
if (a.enabled === false)
|
|
363
|
+
return inner;
|
|
364
|
+
// Kernels thin by pixel column of the domain, which a path that only has an order does not have.
|
|
365
|
+
if (context.sequence)
|
|
366
|
+
fail('a Decimator needs a domain that is a position on an axis: an XY series has only an order, so draw it undecimated');
|
|
367
|
+
// Kernels reduce (domain, value) pairs; a channel column would not follow the points they keep.
|
|
368
|
+
if (context.channels.length > 0)
|
|
369
|
+
fail('a Decimator cannot carry channels: bind on undecimated data, or turn the Decimator off');
|
|
370
|
+
// At least 3 so the kernels' first/last handling has columns to work with.
|
|
371
|
+
const columns = Math.max(3, Math.floor(context.width * (a.resolution ?? 1)));
|
|
372
|
+
const capacity = kernel.capacity(columns);
|
|
373
|
+
// Already small enough: reducing would only cost time and change nothing.
|
|
374
|
+
if (inner.capacity <= capacity)
|
|
375
|
+
return inner;
|
|
376
|
+
return {
|
|
377
|
+
capacity,
|
|
378
|
+
fill: (out) => {
|
|
379
|
+
const view = this.#read(source, inner);
|
|
380
|
+
const reg = view.regularDomain;
|
|
381
|
+
if (reg && !(reg.step > 0))
|
|
382
|
+
fail('a Decimator needs a regular domain that ascends');
|
|
383
|
+
const windowed = windowView(view, context.domainRange);
|
|
384
|
+
const t0 = performance.now();
|
|
385
|
+
const r = kernel.reduce(windowed, context.domainRange, columns, out, capacity);
|
|
386
|
+
const counters = this.countersOrNull;
|
|
387
|
+
if (counters)
|
|
388
|
+
counters.stages.decimate += performance.now() - t0;
|
|
389
|
+
const n = windowed.count;
|
|
390
|
+
const x = (i) => windowed.regularDomain
|
|
391
|
+
? windowed.regularDomain.start + windowed.regularDomain.step * i
|
|
392
|
+
: windowed.data[i * windowed.stride + windowed.domainOffset];
|
|
393
|
+
return {
|
|
394
|
+
count: r.count,
|
|
395
|
+
extents: [n > 0 ? { min: x(0), max: x(n - 1) } : NO_POINTS, { min: r.yMin, max: r.yMax }],
|
|
396
|
+
};
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/** The source's own storage if it offers it, else the source filled into a scratch buffer. */
|
|
401
|
+
#read(source, inner) {
|
|
402
|
+
const own = source.view?.();
|
|
403
|
+
if (own)
|
|
404
|
+
return own; // null for generated data and for transforms, which fill a scratch buffer instead
|
|
405
|
+
if (this.#scratch.length < inner.capacity * 2)
|
|
406
|
+
this.#scratch = new Float64Array(inner.capacity * 2);
|
|
407
|
+
const filled = inner.fill(this.#scratch);
|
|
408
|
+
return { data: this.#scratch, count: filled.count, stride: 2, domainOffset: 0, valueOffset: 1, regularDomain: null };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type DataNode, DataTransform, type FillPlan, type PlanContext } from './data.ts';
|
|
2
|
+
import { type BaseAttributes } from './element.ts';
|
|
3
|
+
import type { Extent } from './geometry.ts';
|
|
4
|
+
type Stat = 'count' | 'sum' | 'mean' | 'fraction' | 'density';
|
|
5
|
+
export interface DensityAttributes extends BaseAttributes {
|
|
6
|
+
/** The dimensions to bin: 'domain', 'value' or channel names. Default ['domain']. */
|
|
7
|
+
by?: readonly string[];
|
|
8
|
+
/** What each point counts for: a channel name or 'value'. Default: 1 for each point. */
|
|
9
|
+
weight?: string;
|
|
10
|
+
/** The value of a one-dimensional output: 'count', 'sum', 'mean', 'fraction' (the share of the total, 0 to 1) or 'density'. Default 'count', or 'sum' with a `weight`. */
|
|
11
|
+
stat?: Stat;
|
|
12
|
+
/** Bin size in each dimension's units: one number for all, or one for each dimension. */
|
|
13
|
+
width?: number | readonly number[];
|
|
14
|
+
/** Number of bins in each dimension, spread evenly over its extent (by default the data's own range). */
|
|
15
|
+
bins?: number | readonly number[];
|
|
16
|
+
/** Where a bin edge falls when `width` is used, in each dimension: edges are at `origin` plus whole widths. Default 0. */
|
|
17
|
+
origin?: number | readonly number[];
|
|
18
|
+
/** The span to bin over in each dimension, as `{ min, max }` (one for all, or an array with one for each); points outside are left out. Default: the data's extent. */
|
|
19
|
+
extent?: Extent | readonly (Extent | null)[];
|
|
20
|
+
/** Most bins along a dimension when the size is chosen by the rule. Default 200. */
|
|
21
|
+
maxBins?: number;
|
|
22
|
+
/** 'keep' bins with no points as zeros, 'omit' leaves them out. Default 'keep' for one dimension, 'omit' for more. */
|
|
23
|
+
empty?: 'keep' | 'omit';
|
|
24
|
+
}
|
|
25
|
+
export declare class Density extends DataTransform<DensityAttributes, [source: DataNode]> {
|
|
26
|
+
#private;
|
|
27
|
+
static attrTypes: {
|
|
28
|
+
id: 1;
|
|
29
|
+
meta: {
|
|
30
|
+
title: 1;
|
|
31
|
+
description: 1;
|
|
32
|
+
role: 1;
|
|
33
|
+
};
|
|
34
|
+
by: 0;
|
|
35
|
+
weight: 1;
|
|
36
|
+
stat: 1;
|
|
37
|
+
width: 0;
|
|
38
|
+
bins: 0;
|
|
39
|
+
origin: 0;
|
|
40
|
+
extent: 0;
|
|
41
|
+
maxBins: 2;
|
|
42
|
+
empty: 1;
|
|
43
|
+
};
|
|
44
|
+
get source(): DataNode;
|
|
45
|
+
invalidate(): void;
|
|
46
|
+
get version(): number;
|
|
47
|
+
/** What it offers: the other dimensions, then the statistics and sizes. */
|
|
48
|
+
get channelNames(): readonly string[];
|
|
49
|
+
get pointCount(): number;
|
|
50
|
+
domain(): Extent | null;
|
|
51
|
+
valueExtent(): Extent | null;
|
|
52
|
+
channelExtent(name: string): Extent | null;
|
|
53
|
+
plan(context: PlanContext): FillPlan;
|
|
54
|
+
}
|
|
55
|
+
export {};
|