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/curves.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { Curve } from './curve.js';
|
|
2
|
+
import { f2 } from './geometry.js';
|
|
3
|
+
const pt = (x, y) => f2(x) + ' ' + f2(y);
|
|
4
|
+
/** Vertices a Bézier or an arc is cut into, for tracing: enough that a chord is within a fraction of a pixel on a plot. */
|
|
5
|
+
const SAMPLES = 16;
|
|
6
|
+
/** A straight line to (x, y). */
|
|
7
|
+
export function lineTo(x, y, a, b) {
|
|
8
|
+
return { cmd: 'L', d: 'L' + pt(x, y), x, y, a, b, sample: (_x0, _y0, push) => push(x, y, 1) };
|
|
9
|
+
}
|
|
10
|
+
/** A cubic Bézier with control points (x1, y1) and (x2, y2), ending at (x, y). */
|
|
11
|
+
export function cubicTo(x1, y1, x2, y2, x, y, a, b) {
|
|
12
|
+
return {
|
|
13
|
+
cmd: 'C', d: 'C' + pt(x1, y1) + ' ' + pt(x2, y2) + ' ' + pt(x, y), x, y, a, b,
|
|
14
|
+
sample(x0, y0, push) {
|
|
15
|
+
for (let j = 1; j <= SAMPLES; j++) {
|
|
16
|
+
const t = j / SAMPLES, u = 1 - t;
|
|
17
|
+
push(u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x, u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y, t);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* One Hermite segment of a run as a cubic: from point `k` to `k + 1` of `p`, with the given tangents (derivatives with
|
|
24
|
+
* respect to a parameter that spans `h` over the segment) at its two ends.
|
|
25
|
+
*/
|
|
26
|
+
export function hermiteTo(p, k, lx, ly, rx, ry, h) {
|
|
27
|
+
return cubicTo(p[2 * k] + (lx * h) / 3, p[2 * k + 1] + (ly * h) / 3, p[2 * k + 2] - (rx * h) / 3, p[2 * k + 3] - (ry * h) / 3, p[2 * k + 2], p[2 * k + 3], k, k + 1);
|
|
28
|
+
}
|
|
29
|
+
function quadSample(qx, qy, x, y) {
|
|
30
|
+
return (x0, y0, push) => {
|
|
31
|
+
for (let j = 1; j <= SAMPLES; j++) {
|
|
32
|
+
const t = j / SAMPLES, u = 1 - t;
|
|
33
|
+
push(u * u * x0 + 2 * u * t * qx + t * t * x, u * u * y0 + 2 * u * t * qy + t * t * y, t);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** A quadratic Bézier with control point (x1, y1), ending at (x, y). */
|
|
38
|
+
export function quadTo(x1, y1, x, y, a, b) {
|
|
39
|
+
return { cmd: 'Q', d: 'Q' + pt(x1, y1) + ' ' + pt(x, y), x, y, a, b, sample: quadSample(x1, y1, x, y) };
|
|
40
|
+
}
|
|
41
|
+
/** A smooth quadratic ending at (x, y), whose control point SVG reflects from the last piece; (cx, cy) is that reflected point, which tracing needs. */
|
|
42
|
+
export function smoothTo(cx, cy, x, y, a, b) {
|
|
43
|
+
return { cmd: 'T', d: 'T' + pt(x, y), x, y, a, b, sample: quadSample(cx, cy, x, y) };
|
|
44
|
+
}
|
|
45
|
+
/** A circular arc of radius `r` to (x, y), on the side and of the size the flags pick (as the SVG `A` command with equal radii). */
|
|
46
|
+
export function arcTo(r, large, sweep, x, y, a, b) {
|
|
47
|
+
return {
|
|
48
|
+
cmd: 'A', d: 'A' + f2(r) + ' ' + f2(r) + ' 0 ' + large + ' ' + sweep + ' ' + pt(x, y), x, y, a, b,
|
|
49
|
+
sample(x0, y0, push) {
|
|
50
|
+
// The circle of radius r through both ends, on the side the flags pick.
|
|
51
|
+
const dx = x - x0, dy = y - y0, d = Math.hypot(dx, dy), h = Math.sqrt(Math.max(0, r * r - (d * d) / 4));
|
|
52
|
+
let best = null;
|
|
53
|
+
for (const side of [1, -1]) {
|
|
54
|
+
const cx = (x0 + x) / 2 - (side * h * dy) / d, cy = (y0 + y) / 2 + (side * h * dx) / d;
|
|
55
|
+
const t0 = Math.atan2(y0 - cy, x0 - cx), t1 = Math.atan2(y - cy, x - cx);
|
|
56
|
+
let span = t1 - t0;
|
|
57
|
+
if (sweep === 1) {
|
|
58
|
+
while (span <= 0)
|
|
59
|
+
span += 2 * Math.PI;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
while (span >= 0)
|
|
63
|
+
span -= 2 * Math.PI;
|
|
64
|
+
}
|
|
65
|
+
if ((Math.abs(span) > Math.PI) === (large === 1) && !best)
|
|
66
|
+
best = { cx, cy, t0, span };
|
|
67
|
+
}
|
|
68
|
+
const arc = best ?? { cx: (x0 + x) / 2, cy: (y0 + y) / 2, t0: Math.atan2(y0 - (y0 + y) / 2, x0 - (x0 + x) / 2), span: sweep === 1 ? Math.PI : -Math.PI };
|
|
69
|
+
const m = Math.max(SAMPLES, Math.ceil(Math.abs(arc.span) / 0.2));
|
|
70
|
+
for (let j = 1; j <= m; j++) {
|
|
71
|
+
const t = j / m, q = arc.t0 + arc.span * t;
|
|
72
|
+
push(j === m ? x : arc.cx + r * Math.cos(q), j === m ? y : arc.cy + r * Math.sin(q), t);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** The tangents of a monotone cubic through the values (Fritsch–Carlson), per unit of the index. */
|
|
78
|
+
function monotone(v, n) {
|
|
79
|
+
const m = new Float64Array(n), s = new Float64Array(n - 1);
|
|
80
|
+
for (let k = 0; k < n - 1; k++)
|
|
81
|
+
s[k] = v[k + 1] - v[k];
|
|
82
|
+
m[0] = s[0];
|
|
83
|
+
m[n - 1] = s[n - 2];
|
|
84
|
+
for (let k = 1; k < n - 1; k++)
|
|
85
|
+
m[k] = s[k - 1] * s[k] <= 0 ? 0 : (s[k - 1] + s[k]) / 2;
|
|
86
|
+
for (let k = 0; k < n - 1; k++) {
|
|
87
|
+
if (s[k] === 0) {
|
|
88
|
+
m[k] = m[k + 1] = 0;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const a = m[k] / s[k], b = m[k + 1] / s[k], q = a * a + b * b;
|
|
92
|
+
if (q > 9) {
|
|
93
|
+
const tau = 3 / Math.sqrt(q);
|
|
94
|
+
m[k] = tau * a * s[k];
|
|
95
|
+
m[k + 1] = tau * b * s[k];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return m;
|
|
99
|
+
}
|
|
100
|
+
/** The second derivatives of the natural cubic spline through the values at parameters with these spacings `h`. */
|
|
101
|
+
function natural(v, h, n) {
|
|
102
|
+
const M = new Float64Array(n);
|
|
103
|
+
if (n < 3)
|
|
104
|
+
return M;
|
|
105
|
+
// Tridiagonal system for the interior second derivatives, by the Thomas algorithm.
|
|
106
|
+
const diag = new Float64Array(n), rhs = new Float64Array(n);
|
|
107
|
+
for (let k = 1; k < n - 1; k++) {
|
|
108
|
+
diag[k] = 2 * (h[k - 1] + h[k]);
|
|
109
|
+
rhs[k] = 6 * ((v[k + 1] - v[k]) / h[k] - (v[k] - v[k - 1]) / h[k - 1]);
|
|
110
|
+
}
|
|
111
|
+
for (let k = 2; k < n - 1; k++) {
|
|
112
|
+
const w = h[k - 1] / diag[k - 1];
|
|
113
|
+
diag[k] = diag[k] - w * h[k - 1];
|
|
114
|
+
rhs[k] = rhs[k] - w * rhs[k - 1];
|
|
115
|
+
}
|
|
116
|
+
for (let k = n - 2; k >= 1; k--)
|
|
117
|
+
M[k] = (rhs[k] - (k < n - 2 ? h[k] * M[k + 1] : 0)) / diag[k];
|
|
118
|
+
return M;
|
|
119
|
+
}
|
|
120
|
+
/** The runs of points that are real: their pairs, and the index of each one's point in the series. One run per stretch between gaps, or all in one when connecting. */
|
|
121
|
+
function runsOf(points, connect, also) {
|
|
122
|
+
const { data, count, stride } = points, out = [];
|
|
123
|
+
const ok = (i) => { const x = data[i * stride], y = data[i * stride + 1]; return Number.isFinite(x) && Number.isFinite(y) && (!also || also(i)); };
|
|
124
|
+
let run = [], index = [];
|
|
125
|
+
const end = () => { if (index.length)
|
|
126
|
+
out.push({ pts: Float64Array.from(run), index }); run = []; index = []; };
|
|
127
|
+
for (let i = 0; i < count; i++) {
|
|
128
|
+
if (ok(i)) {
|
|
129
|
+
run.push(data[i * stride], data[i * stride + 1]);
|
|
130
|
+
index.push(i);
|
|
131
|
+
}
|
|
132
|
+
else if (!connect)
|
|
133
|
+
end();
|
|
134
|
+
}
|
|
135
|
+
end();
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/** The vertices of a run of curves as one buffer, a NaN point after each run, and each vertex's owner. */
|
|
139
|
+
function gather(parts) {
|
|
140
|
+
let total = 0;
|
|
141
|
+
for (const p of parts)
|
|
142
|
+
total += p.owner.length + 1;
|
|
143
|
+
const data = new Float64Array(2 * total), owner = new Int32Array(total);
|
|
144
|
+
let k = 0;
|
|
145
|
+
for (const p of parts) {
|
|
146
|
+
for (let i = 0; i < p.owner.length; i++, k++) {
|
|
147
|
+
data[2 * k] = p.pts[2 * i];
|
|
148
|
+
data[2 * k + 1] = p.pts[2 * i + 1];
|
|
149
|
+
owner[k] = p.owner[i];
|
|
150
|
+
}
|
|
151
|
+
data[2 * k] = data[2 * k + 1] = NaN;
|
|
152
|
+
owner[k] = -1;
|
|
153
|
+
k++;
|
|
154
|
+
}
|
|
155
|
+
return { line: { data, stride: 2, count: total }, owner };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A curve made of pieces. It says what the pieces through a run of points are (`pieces`), and this does the rest: the path
|
|
159
|
+
* text of a line and of an area, and the trace. Every run has at least two points; a single point draws nothing.
|
|
160
|
+
*/
|
|
161
|
+
export class StrokeCurve extends Curve {
|
|
162
|
+
/**
|
|
163
|
+
* The path text through `p`, starting with `lead`: 'M' to begin a path at the first point, or 'L' to carry on from
|
|
164
|
+
* wherever the pen is (for the way back round an area).
|
|
165
|
+
*/
|
|
166
|
+
path(p, lead, series) {
|
|
167
|
+
const n = p.length >> 1;
|
|
168
|
+
if (n === 0)
|
|
169
|
+
return '';
|
|
170
|
+
let d = lead + pt(p[0], p[1]), previous = '';
|
|
171
|
+
if (n > 1) {
|
|
172
|
+
for (const c of this.pieces(p, series)) {
|
|
173
|
+
d += c.cmd === 'L' && previous === 'L' ? ' ' + c.d.slice(1) : c.d;
|
|
174
|
+
previous = c.cmd;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return d;
|
|
178
|
+
}
|
|
179
|
+
/** The curve through `p` as vertices, and for each the index (into `p`'s points) of the point it belongs to (the nearer end of the piece it is on). The same curve `path` writes. */
|
|
180
|
+
polyline(p, series) {
|
|
181
|
+
const n = p.length >> 1, pts = [], owner = [];
|
|
182
|
+
if (n === 0)
|
|
183
|
+
return { pts, owner };
|
|
184
|
+
pts.push(p[0], p[1]);
|
|
185
|
+
owner.push(0);
|
|
186
|
+
if (n === 1)
|
|
187
|
+
return { pts, owner };
|
|
188
|
+
let x = p[0], y = p[1];
|
|
189
|
+
for (const c of this.pieces(p, series)) {
|
|
190
|
+
c.sample(x, y, (px, py, t) => { pts.push(px, py); owner.push(t < 0.5 ? c.a : c.b); });
|
|
191
|
+
x = c.x;
|
|
192
|
+
y = c.y;
|
|
193
|
+
}
|
|
194
|
+
return { pts, owner };
|
|
195
|
+
}
|
|
196
|
+
line(series) {
|
|
197
|
+
let d = '';
|
|
198
|
+
for (const run of runsOf(series.points, series.attrs.gaps === 'connect'))
|
|
199
|
+
d += this.path(run.pts, 'M', series);
|
|
200
|
+
return d;
|
|
201
|
+
}
|
|
202
|
+
/** Each run of the shape: its top as pairs, the way back along its baseline (reversed), and the series' index of each point. */
|
|
203
|
+
#runs(series, baseline) {
|
|
204
|
+
const base = series.baseline(baseline);
|
|
205
|
+
const real = (i) => { const x = base.data[2 * i], y = base.data[2 * i + 1]; return Number.isFinite(x) && Number.isFinite(y); };
|
|
206
|
+
return runsOf(series.points, series.attrs.gaps === 'connect', real).map((run) => {
|
|
207
|
+
const foot = new Float64Array(run.pts.length);
|
|
208
|
+
run.index.forEach((row, j) => { foot[run.pts.length - 2 - 2 * j] = base.data[2 * row]; foot[run.pts.length - 1 - 2 * j] = base.data[2 * row + 1]; });
|
|
209
|
+
return { top: run.pts, back: foot, index: run.index };
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
area(series, baseline) {
|
|
213
|
+
let d = '';
|
|
214
|
+
for (const r of this.#runs(series, baseline))
|
|
215
|
+
d += this.path(r.top, 'M', series) + this.path(r.back, 'L', series) + 'Z';
|
|
216
|
+
return d;
|
|
217
|
+
}
|
|
218
|
+
#key = '';
|
|
219
|
+
#value = null;
|
|
220
|
+
trace(series, baseline) {
|
|
221
|
+
const key = `${series.projection}|${baseline}|${series.attrs.gaps}|${JSON.stringify(this.attrs)}`;
|
|
222
|
+
if (key === this.#key)
|
|
223
|
+
return this.#value;
|
|
224
|
+
this.#key = key;
|
|
225
|
+
if (baseline === null) {
|
|
226
|
+
const parts = runsOf(series.points, series.attrs.gaps === 'connect').map((run) => {
|
|
227
|
+
const { pts, owner } = this.polyline(run.pts, series);
|
|
228
|
+
return { pts, owner: owner.map((i) => run.index[i]) };
|
|
229
|
+
});
|
|
230
|
+
return (this.#value = { ...gather(parts), base: null });
|
|
231
|
+
}
|
|
232
|
+
const tops = [], feet = [];
|
|
233
|
+
for (const r of this.#runs(series, baseline)) {
|
|
234
|
+
const t = this.polyline(r.top, series), b = this.polyline(r.back, series);
|
|
235
|
+
tops.push({ pts: t.pts, owner: t.owner.map((i) => r.index[i]) });
|
|
236
|
+
// The way back is drawn from the last point to the first; kept here in the direction of the line, so that a vertex lines up with the top's.
|
|
237
|
+
const forward = [], m = b.pts.length >> 1;
|
|
238
|
+
for (let i = m - 1; i >= 0; i--)
|
|
239
|
+
forward.push(b.pts[2 * i], b.pts[2 * i + 1]);
|
|
240
|
+
feet.push({ pts: forward, owner: b.owner.map(() => 0) });
|
|
241
|
+
}
|
|
242
|
+
return (this.#value = { ...gather(tops), base: gather(feet).line });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const xs = (p) => { const n = p.length >> 1, a = new Float64Array(n); for (let i = 0; i < n; i++)
|
|
246
|
+
a[i] = p[2 * i]; return a; };
|
|
247
|
+
const ys = (p) => { const n = p.length >> 1, a = new Float64Array(n); for (let i = 0; i < n; i++)
|
|
248
|
+
a[i] = p[2 * i + 1]; return a; };
|
|
249
|
+
/** Straight lines to every point after the first. */
|
|
250
|
+
const straight = (p) => { const out = []; for (let i = 1; i < p.length >> 1; i++)
|
|
251
|
+
out.push(lineTo(p[2 * i], p[2 * i + 1], i - 1, i)); return out; };
|
|
252
|
+
/** The points of `p` extended past both ends by reflection, which leaves the end tangents along the first and last segment. */
|
|
253
|
+
const reflected = (p) => {
|
|
254
|
+
const n = p.length >> 1;
|
|
255
|
+
const at = (c) => (i) => (i < 0 ? 2 * p[c] - p[2 + c] : i >= n ? 2 * p[2 * n - 2 + c] - p[2 * n - 4 + c] : p[2 * i + c]);
|
|
256
|
+
return [at(0), at(1)];
|
|
257
|
+
};
|
|
258
|
+
/** A smooth line that never overshoots the data: a monotone cubic (Fritsch–Carlson). */
|
|
259
|
+
export class MonotoneCurve extends StrokeCurve {
|
|
260
|
+
pieces(p) {
|
|
261
|
+
const n = p.length >> 1;
|
|
262
|
+
if (n === 2)
|
|
263
|
+
return straight(p);
|
|
264
|
+
const mx = monotone(xs(p), n), my = monotone(ys(p), n), out = [];
|
|
265
|
+
for (let k = 0; k < n - 1; k++)
|
|
266
|
+
out.push(hermiteTo(p, k, mx[k], my[k], mx[k + 1], my[k + 1], 1));
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** The natural cubic spline: the smoothest line through the points, which swings past them. */
|
|
271
|
+
export class NaturalCurve extends StrokeCurve {
|
|
272
|
+
pieces(p) {
|
|
273
|
+
const n = p.length >> 1;
|
|
274
|
+
if (n === 2)
|
|
275
|
+
return straight(p);
|
|
276
|
+
const h = new Float64Array(n - 1), X = xs(p), Y = ys(p), out = [];
|
|
277
|
+
// Parameter by the length of the polyline, so that a path which doubles back is as smooth as a function.
|
|
278
|
+
for (let k = 0; k < n - 1; k++)
|
|
279
|
+
h[k] = Math.max(1e-6, Math.hypot(X[k + 1] - X[k], Y[k + 1] - Y[k]));
|
|
280
|
+
const Mx = natural(X, h, n), My = natural(Y, h, n);
|
|
281
|
+
for (let k = 0; k < n - 1; k++) {
|
|
282
|
+
const hk = h[k], dx = (X[k + 1] - X[k]) / hk, dy = (Y[k + 1] - Y[k]) / hk;
|
|
283
|
+
out.push(hermiteTo(p, k, dx - (hk * (2 * Mx[k] + Mx[k + 1])) / 6, dy - (hk * (2 * My[k] + My[k + 1])) / 6, dx + (hk * (Mx[k] + 2 * Mx[k + 1])) / 6, dy + (hk * (My[k] + 2 * My[k + 1])) / 6, hk));
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** A cardinal spline: through every point, with a tangent along the neighbours that `tension` shortens. */
|
|
289
|
+
export class CardinalCurve extends StrokeCurve {
|
|
290
|
+
pieces(p) {
|
|
291
|
+
const n = p.length >> 1, c = 1 - (this.attrs.tension ?? 0), [px, py] = reflected(p), out = [];
|
|
292
|
+
for (let k = 0; k < n - 1; k++) {
|
|
293
|
+
out.push(hermiteTo(p, k, (c * (px(k + 1) - px(k - 1))) / 2, (c * (py(k + 1) - py(k - 1))) / 2, (c * (px(k + 2) - px(k))) / 2, (c * (py(k + 2) - py(k))) / 2, 1));
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** The Catmull–Rom spline: through every point, centripetal by default so that it neither loops nor cusps when the spacing is uneven. */
|
|
299
|
+
export class CatmullRomCurve extends StrokeCurve {
|
|
300
|
+
pieces(p) {
|
|
301
|
+
const n = p.length >> 1, alpha = this.attrs.alpha ?? 0.5, [px, py] = reflected(p), out = [];
|
|
302
|
+
const knot = (a, b) => Math.pow(Math.hypot(px(b) - px(a), py(b) - py(a)), alpha);
|
|
303
|
+
for (let k = 0; k < n - 1; k++) {
|
|
304
|
+
const t01 = knot(k - 1, k), t12 = knot(k, k + 1), t23 = knot(k + 1, k + 2);
|
|
305
|
+
if (!(t12 > 1e-9) || !(t01 > 1e-9) || !(t23 > 1e-9)) {
|
|
306
|
+
out.push(lineTo(p[2 * k + 2], p[2 * k + 3], k, k + 1));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const tangent = (v) => {
|
|
310
|
+
const q0 = v(k - 1), q1 = v(k), q2 = v(k + 1), q3 = v(k + 2);
|
|
311
|
+
return [
|
|
312
|
+
(q1 - q0) / t01 - (q2 - q0) / (t01 + t12) + (q2 - q1) / t12,
|
|
313
|
+
(q2 - q1) / t12 - (q3 - q1) / (t12 + t23) + (q3 - q2) / t23,
|
|
314
|
+
];
|
|
315
|
+
};
|
|
316
|
+
const [ax, bx] = tangent(px), [ay, by] = tangent(py);
|
|
317
|
+
out.push(hermiteTo(p, k, ax, ay, bx, by, t12));
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** A uniform cubic B-spline, pulled towards the points rather than through them, starting at the first and ending at the last. */
|
|
323
|
+
export class BasisCurve extends StrokeCurve {
|
|
324
|
+
pieces(p) {
|
|
325
|
+
const n = p.length >> 1, out = [];
|
|
326
|
+
const X = (i) => p[2 * Math.min(n - 1, Math.max(0, i))], Y = (i) => p[2 * Math.min(n - 1, Math.max(0, i)) + 1];
|
|
327
|
+
out.push(lineTo((5 * X(0) + X(1)) / 6, (5 * Y(0) + Y(1)) / 6, 0, 0));
|
|
328
|
+
for (let j = 0; j < n - 1; j++) {
|
|
329
|
+
// The control points b, c, e of this piece; it runs from a sixth of the way (a + 4b + c) to the next such place.
|
|
330
|
+
const bx = X(j), by = Y(j), cx = X(j + 1), cy = Y(j + 1), ex = X(j + 2), ey = Y(j + 2);
|
|
331
|
+
out.push(cubicTo((2 * bx + cx) / 3, (2 * by + cy) / 3, (bx + 2 * cx) / 3, (by + 2 * cy) / 3, (bx + 4 * cx + ex) / 6, (by + 4 * cy + ey) / 6, j, j + 1));
|
|
332
|
+
}
|
|
333
|
+
out.push(lineTo(X(n - 1), Y(n - 1), n - 1, n - 1));
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** A level start and end to each segment and an S between: the flow of a bump chart or a sankey. */
|
|
338
|
+
export class BumpCurve extends StrokeCurve {
|
|
339
|
+
pieces(p, series) {
|
|
340
|
+
const n = p.length >> 1, out = [];
|
|
341
|
+
const o = series?.firstAxis.orientation, alongX = (this.attrs.along ?? (o === 'left' || o === 'right' ? 'y' : 'x')) === 'x';
|
|
342
|
+
for (let k = 0; k < n - 1; k++) {
|
|
343
|
+
const x0 = p[2 * k], y0 = p[2 * k + 1], x1 = p[2 * k + 2], y1 = p[2 * k + 3];
|
|
344
|
+
out.push(alongX ? cubicTo((x0 + x1) / 2, y0, (x0 + x1) / 2, y1, x1, y1, k, k + 1) : cubicTo(x0, (y0 + y1) / 2, x1, (y0 + y1) / 2, x1, y1, k, k + 1));
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** Quadratic Béziers with each point as a control point, through the middles of the segments. */
|
|
350
|
+
export class QuadraticCurve extends StrokeCurve {
|
|
351
|
+
pieces(p) {
|
|
352
|
+
const n = p.length >> 1;
|
|
353
|
+
if (n === 2)
|
|
354
|
+
return straight(p);
|
|
355
|
+
const X = (i) => p[2 * i], Y = (i) => p[2 * i + 1], out = [];
|
|
356
|
+
out.push(lineTo((X(0) + X(1)) / 2, (Y(0) + Y(1)) / 2, 0, 0));
|
|
357
|
+
out.push(quadTo(X(1), Y(1), (X(1) + X(2)) / 2, (Y(1) + Y(2)) / 2, 1, 1));
|
|
358
|
+
// A smooth quadratic reflects the last control point, which for midpoints is exactly the next data point.
|
|
359
|
+
for (let k = 2; k < n - 1; k++)
|
|
360
|
+
out.push(smoothTo(X(k), Y(k), (X(k) + X(k + 1)) / 2, (Y(k) + Y(k + 1)) / 2, k, k));
|
|
361
|
+
out.push(lineTo(X(n - 1), Y(n - 1), n - 1, n - 1));
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** A circular arc between each pair of points. */
|
|
366
|
+
export class ArcCurve extends StrokeCurve {
|
|
367
|
+
pieces(p) {
|
|
368
|
+
const n = p.length >> 1, b = this.attrs.bend ?? 0.3, rise = Math.abs(b), out = [];
|
|
369
|
+
for (let k = 0; k < n - 1; k++) {
|
|
370
|
+
const x = p[2 * k + 2], y = p[2 * k + 3], chord = Math.hypot(x - p[2 * k], y - p[2 * k + 1]);
|
|
371
|
+
out.push(!(chord > 1e-9) || rise < 1e-6 ? lineTo(x, y, k, k + 1) : arcTo((chord * (0.25 + rise * rise)) / (2 * rise), rise > 0.5 ? 1 : 0, b > 0 ? 0 : 1, x, y, k, k + 1));
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
}
|
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { type AnyElement, type BaseAttributes, ChartElement } from './element.ts';
|
|
2
|
+
import type { Extent } from './geometry.ts';
|
|
3
|
+
import type { Series } from './series.ts';
|
|
4
|
+
/** Points in pixel space as a series hands them to planes and renderers: `count` rows of `stride` numbers in `data`. */
|
|
5
|
+
export interface PointBuffer {
|
|
6
|
+
data: Float64Array;
|
|
7
|
+
stride: number;
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
/** What a series tells the data about the plot it will draw in, so stages can deliver only what is needed. */
|
|
11
|
+
export interface PlanContext {
|
|
12
|
+
readonly width: number;
|
|
13
|
+
readonly height: number;
|
|
14
|
+
readonly domainRange: Extent;
|
|
15
|
+
/**
|
|
16
|
+
* Extra columns to deliver after the domain and the value, in this order: a point is then `2 + channels.length`
|
|
17
|
+
* numbers wide. A series asks for the channels its renderers bind.
|
|
18
|
+
*/
|
|
19
|
+
readonly channels: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* True when the domain is only the order of the points, as in an XY plot where a path wanders across the plane, and
|
|
22
|
+
* not a position on an axis. Nothing may window or thin such points by pixel column, so a `Decimator` refuses them.
|
|
23
|
+
* Default false.
|
|
24
|
+
*/
|
|
25
|
+
readonly sequence?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `extents` has one entry per column: the domain, the value, then each channel.
|
|
29
|
+
*
|
|
30
|
+
* `rowOf` says which row of the source's own storage each written point is. It is there when the points *are* the
|
|
31
|
+
* source's (in place, or sorted into the managed copy) and left out when something made them up (a decimator's
|
|
32
|
+
* min and max), so that an edit can never be aimed at a row that is not there.
|
|
33
|
+
*/
|
|
34
|
+
export interface FillResult {
|
|
35
|
+
readonly count: number;
|
|
36
|
+
readonly extents: readonly Extent[];
|
|
37
|
+
readonly rowOf?: (index: number) => number;
|
|
38
|
+
}
|
|
39
|
+
/** A promise of points: how much room `fill` may need, and the function that writes them. */
|
|
40
|
+
export interface FillPlan {
|
|
41
|
+
readonly capacity: number;
|
|
42
|
+
fill(out: Float64Array): FillResult;
|
|
43
|
+
}
|
|
44
|
+
/** A read-only look at a source's own storage, so a decimator can scan it without copying. */
|
|
45
|
+
export interface PointView {
|
|
46
|
+
readonly data: ArrayLike<number>;
|
|
47
|
+
readonly count: number;
|
|
48
|
+
readonly stride: number;
|
|
49
|
+
readonly domainOffset: number;
|
|
50
|
+
readonly valueOffset: number;
|
|
51
|
+
/** When set, the domain is `start + step * i` and is never read from `data`. */
|
|
52
|
+
readonly regularDomain: {
|
|
53
|
+
start: number;
|
|
54
|
+
step: number;
|
|
55
|
+
} | null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What the type checker knows of the channels a stage offers, so that a series asking for one by name can be checked
|
|
59
|
+
* against its source. A stage whose channels are not spelled out (`string`) is not checked.
|
|
60
|
+
*/
|
|
61
|
+
export type Offers<Channel extends string> = string extends Channel ? any : {
|
|
62
|
+
[K in Channel]: true;
|
|
63
|
+
};
|
|
64
|
+
/** What every stage of the data pipeline provides, whether it is a source or a transform. */
|
|
65
|
+
export interface DataStage {
|
|
66
|
+
/** Increases whenever the data changes. Fills are cached against it. */
|
|
67
|
+
readonly version: number;
|
|
68
|
+
readonly pointCount: number;
|
|
69
|
+
/** The extent of the domain, or null if there are no points. */
|
|
70
|
+
domain(): Extent | null;
|
|
71
|
+
/** The extent of every value, whatever part of the domain is being drawn. Null if there are none. */
|
|
72
|
+
valueExtent(): Extent | null;
|
|
73
|
+
/** The extent of every value of a channel, whatever part of the domain is being drawn. Null if there are none. */
|
|
74
|
+
channelExtent(name: string): Extent | null;
|
|
75
|
+
/** Points always come out ascending in the domain, whatever the storage order was. */
|
|
76
|
+
plan(context: PlanContext): FillPlan;
|
|
77
|
+
/**
|
|
78
|
+
* The points, ascending in the domain, as storage that can be read in place and is valid until the next change.
|
|
79
|
+
* Null when there is no such storage (generated data, a transform): use `plan` instead.
|
|
80
|
+
*/
|
|
81
|
+
view?(): PointView | null;
|
|
82
|
+
/** The channels it can deliver by name, in order: what to ask for to read all of it. Absent or empty when there are none. */
|
|
83
|
+
readonly channelNames?: readonly string[];
|
|
84
|
+
/** Optional hint for in-place edits: only [start, end) changed. */
|
|
85
|
+
invalidateRange?(start: number, end: number): void;
|
|
86
|
+
}
|
|
87
|
+
export interface DataSourceAttributes extends BaseAttributes {
|
|
88
|
+
/**
|
|
89
|
+
* The stored points already ascend in the domain. Default false.
|
|
90
|
+
*
|
|
91
|
+
* A source always *delivers* ascending points; this says whether it has to arrange that. When false, the source
|
|
92
|
+
* loads the points once per change into a buffer it owns, drops any with a NaN domain, and sorts them if they are
|
|
93
|
+
* not already in order. When true that copy is skipped and the data is read where it is, so set it for large
|
|
94
|
+
* data you know is ordered. Setting it for data that is not ordered draws wrong lines.
|
|
95
|
+
*/
|
|
96
|
+
isSorted?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The domain repeats with this period, and the source delivers one more point: the first, again, one period on.
|
|
99
|
+
* That closes the turn for a plane that wraps (a radar or polar plot) so a line or an area meets itself; `wrap: 6`
|
|
100
|
+
* for six categories at 0 to 5 puts a copy of the first at 6. The period must reach past the last point. A wrapped
|
|
101
|
+
* source is always loaded into its own buffer, so `isSorted` only saves it the sort.
|
|
102
|
+
*/
|
|
103
|
+
wrap?: number;
|
|
104
|
+
}
|
|
105
|
+
/** A source's own sorted copy of its points, valid for one `version`. `extents` has one entry per column. */
|
|
106
|
+
interface ManagedPoints {
|
|
107
|
+
version: number;
|
|
108
|
+
data: Float64Array;
|
|
109
|
+
count: number;
|
|
110
|
+
stride: number;
|
|
111
|
+
extents: readonly Extent[];
|
|
112
|
+
/** The storage row of each sorted point, or null when sorted order is storage order. */
|
|
113
|
+
rows: Uint32Array | null;
|
|
114
|
+
}
|
|
115
|
+
/** The extent of an empty column: min above max, so any real value replaces it and `min <= max` tests emptiness. */
|
|
116
|
+
export declare const NO_POINTS: Extent;
|
|
117
|
+
/** Shared with sources.ts. Missing values become NaN, which every stage treats as a gap. */
|
|
118
|
+
export declare const num: (v: unknown) => number;
|
|
119
|
+
/** The first and last finite values of `n` readings, which for an ascending domain are its extent. */
|
|
120
|
+
export declare function endsOf(n: number, at: (i: number) => number): Extent | null;
|
|
121
|
+
/**
|
|
122
|
+
* Everything the contract needs that does not depend on where the numbers are stored: ordering, the managed buffer,
|
|
123
|
+
* wrapping, extents and caching. A concrete source supplies only `fill`, `scanDomain` and (if it can) `orderedView`.
|
|
124
|
+
*/
|
|
125
|
+
export declare abstract class DataSource<A extends DataSourceAttributes = DataSourceAttributes, Channel extends string = string> extends ChartElement<Series | DataTransform, [], A> implements DataStage {
|
|
126
|
+
#private;
|
|
127
|
+
static attrTypes: {
|
|
128
|
+
isSorted: 3;
|
|
129
|
+
id: 1;
|
|
130
|
+
meta: {
|
|
131
|
+
title: 1;
|
|
132
|
+
description: 1;
|
|
133
|
+
role: 1;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/** Type checking only: the channel names this source offers. Never present at run time. */
|
|
137
|
+
readonly offers?: Offers<Channel>;
|
|
138
|
+
private domainCache;
|
|
139
|
+
private valueCache;
|
|
140
|
+
private channelCache;
|
|
141
|
+
/** Increases whenever the data changes. Cached fills are keyed on it. */
|
|
142
|
+
get version(): number;
|
|
143
|
+
abstract get pointCount(): number;
|
|
144
|
+
/**
|
|
145
|
+
* True when `fill` already writes ascending points, so nothing needs arranging: the data is flagged sorted,
|
|
146
|
+
* or the source generates it in order. A generated source overrides this.
|
|
147
|
+
*/
|
|
148
|
+
protected get ordered(): boolean;
|
|
149
|
+
/** Ordered, and so read where it is stored: not when wrapped, since the extra point has to live somewhere. */
|
|
150
|
+
private get inPlace();
|
|
151
|
+
/** The channels this source can deliver, in the order `managed` stores them. Default none. */
|
|
152
|
+
get channelNames(): readonly string[];
|
|
153
|
+
/**
|
|
154
|
+
* Write every point into `out`, in storage order, as the domain, the value and then each of `context.channels`,
|
|
155
|
+
* and report the extent of every column, NaN skipped. This is the hot loop, so each source writes its own
|
|
156
|
+
* against its own storage.
|
|
157
|
+
*/
|
|
158
|
+
protected abstract fill(out: Float64Array, context: PlanContext): FillResult;
|
|
159
|
+
/**
|
|
160
|
+
* The extent of the domain of an `ordered` source, or null if there are no points. Only the two ends matter,
|
|
161
|
+
* so this never scans. Cached per version by `domain()`.
|
|
162
|
+
*/
|
|
163
|
+
protected abstract scanDomain(): Extent | null;
|
|
164
|
+
/** An `ordered` source that can be read in place offers its storage here. Default: nothing to offer. */
|
|
165
|
+
protected orderedView(): PointView | null;
|
|
166
|
+
/**
|
|
167
|
+
* The points in a buffer this source owns, ascending, rebuilt lazily after each change. An ordered source is
|
|
168
|
+
* only copied; any other has NaN-domain points dropped and is sorted (stably) unless it happens to be in order.
|
|
169
|
+
*/
|
|
170
|
+
protected managed(): ManagedPoints;
|
|
171
|
+
/** The managed buffer as a view. */
|
|
172
|
+
protected managedView(): PointView;
|
|
173
|
+
/** The points ascending in the domain, to read in place; valid until the next change. Null: use `plan`. */
|
|
174
|
+
view(): PointView | null;
|
|
175
|
+
/** Any change to the data bumps the version, which is what discards cached fills. */
|
|
176
|
+
invalidate(): void;
|
|
177
|
+
/** Cached per version; an in-place source only looks at its two ends, a managed one already knows. */
|
|
178
|
+
domain(): Extent | null;
|
|
179
|
+
/**
|
|
180
|
+
* The extent of all the values, gaps skipped. A scan of the stored points, made once per change and only when asked,
|
|
181
|
+
* for anything that needs the whole range while drawing only part of it (the limits of a zoom).
|
|
182
|
+
*/
|
|
183
|
+
valueExtent(): Extent | null;
|
|
184
|
+
/** Like `valueExtent` for one channel: a scan made once per change and name, for anything that needs the whole range. */
|
|
185
|
+
channelExtent(name: string): Extent | null;
|
|
186
|
+
/**
|
|
187
|
+
* In place: fill straight from storage in the requested column order. Managed: copy the sorted buffer, repacking rows
|
|
188
|
+
* only when the series wants a different selection of channels than the source stores.
|
|
189
|
+
*/
|
|
190
|
+
plan(context: PlanContext): FillPlan;
|
|
191
|
+
}
|
|
192
|
+
/** 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. */
|
|
193
|
+
export declare abstract class DataTransform<A extends BaseAttributes = BaseAttributes, Children extends readonly AnyElement[] = [source: DataNode], Channel extends string = string> extends ChartElement<Series | DataTransform, Children, A> implements DataStage {
|
|
194
|
+
/** Type checking only: the channel names this stage offers. Never present at run time. */
|
|
195
|
+
readonly offers?: Offers<Channel>;
|
|
196
|
+
abstract get version(): number;
|
|
197
|
+
abstract get pointCount(): number;
|
|
198
|
+
/** The channels it delivers by name. Default none; a transform that passes its source's through, or makes its own, says so. */
|
|
199
|
+
get channelNames(): readonly string[];
|
|
200
|
+
/** A transform that keeps its output in a typed array may offer it for reading in place. */
|
|
201
|
+
view?(): PointView | null;
|
|
202
|
+
abstract domain(): Extent | null;
|
|
203
|
+
abstract valueExtent(): Extent | null;
|
|
204
|
+
abstract channelExtent(name: string): Extent | null;
|
|
205
|
+
abstract plan(context: PlanContext): FillPlan;
|
|
206
|
+
}
|
|
207
|
+
/** A source or a transform. `DataNode<'x'>` is one the type checker knows offers a channel called x. */
|
|
208
|
+
export type DataNode<Channel extends string = string> = DataSource<DataSourceAttributes, Channel> | DataTransform<BaseAttributes, readonly AnyElement[], Channel>;
|
|
209
|
+
export {};
|