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/errors.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error bars and bands: how far each value may be from what was measured.
|
|
3
|
+
*
|
|
4
|
+
* <DomainSeries>
|
|
5
|
+
* <ObjectDataSource source={rows} domain="t" value="mean" channels={{ sd: 'sd' }} />
|
|
6
|
+
* <LinePath><LinearCurve /></LinePath>
|
|
7
|
+
* <ErrorBars error="sd" />
|
|
8
|
+
* </DomainSeries>
|
|
9
|
+
*
|
|
10
|
+
* Both are renderers that take a **channel** of the data beside each point: `error` is how far above the value, and
|
|
11
|
+
* `negativeError` how far below (left out, it is the same distance, so one channel gives a symmetric bar). `mode` says what
|
|
12
|
+
* the number means: 'absolute' (the default: in the unit of the value) or 'relative' (a fraction of the value, so 0.1 is
|
|
13
|
+
* plus or minus 10%). A gap in the value or in either error leaves that point without a bar.
|
|
14
|
+
*
|
|
15
|
+
* `ErrorBars` draws a whisker with a cap at each end; `ErrorBand` fills the region between the lower and upper ends, with
|
|
16
|
+
* straight edges, which suits a dense series. They work on any plane (the whisker is a spoke on a polar one), sit on top of a
|
|
17
|
+
* stack's layers (an error in the stack's unit; not with a 'normalize' stack, which has no unit) and on bars in a group,
|
|
18
|
+
* and the axes leave room for them, so the range shows every bar. Neither is hit, and neither can be on a series with a
|
|
19
|
+
* `Decimator` (channels cannot go through one).
|
|
20
|
+
*
|
|
21
|
+
* `along` is which way the error runs: 'value' (the default), or 'domain', which on an `XYSeries` is horizontal error
|
|
22
|
+
* (the axis for x is fitted to it; on a `DomainSeries` the domain is settled first, so give that axis a range).
|
|
23
|
+
*/
|
|
24
|
+
/* 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 */
|
|
25
|
+
import { NUMBER, STRING } from './attrs.js';
|
|
26
|
+
import { barShift } from './bars.js';
|
|
27
|
+
import { f2 } from './geometry.js';
|
|
28
|
+
import { SeriesRenderer } from './renderer.js';
|
|
29
|
+
/** How far the normalised position may stray outside the axis before it is held there: only to keep the numbers sane, the clip does the rest. */
|
|
30
|
+
const REACH = 2;
|
|
31
|
+
/** The step along the cap's direction, as a fraction of its axis, used to find which way that is on the plane. */
|
|
32
|
+
const TANGENT = 1e-4;
|
|
33
|
+
/** The error of one row: above and below, as non-negative distances, or NaN where either is missing. */
|
|
34
|
+
function distances(view, i, error, negative, relative, value) {
|
|
35
|
+
const row = i * view.stride, up = view.raw[row + view.channelColumn(error)], down = view.raw[row + view.channelColumn(negative)];
|
|
36
|
+
const k = relative ? Math.abs(value) : 1;
|
|
37
|
+
return [Math.abs(up) * k, Math.abs(down) * k];
|
|
38
|
+
}
|
|
39
|
+
/** What error renderers share: the channels, the rows' spread, and the placed ends. */
|
|
40
|
+
class ErrorRenderer extends SeriesRenderer {
|
|
41
|
+
get channelNames() {
|
|
42
|
+
const a = this.attrs;
|
|
43
|
+
return a.negativeError && a.negativeError !== a.error ? [a.error, a.negativeError] : [a.error];
|
|
44
|
+
}
|
|
45
|
+
get negative() { return this.attrs.negativeError ?? this.attrs.error; }
|
|
46
|
+
spread(view) {
|
|
47
|
+
const a = this.attrs, relative = a.mode === 'relative';
|
|
48
|
+
let lo = Infinity, hi = -Infinity;
|
|
49
|
+
for (let i = 0; i < view.count; i++) {
|
|
50
|
+
const row = i * view.stride, own = view.raw[row + (a.along === 'domain' ? view.firstColumn : view.secondColumn)];
|
|
51
|
+
const at = view.top && a.along !== 'domain' ? view.top[i] : own;
|
|
52
|
+
const [up, down] = distances(view, i, a.error, this.negative, relative, view.raw[row + view.secondColumn]);
|
|
53
|
+
if (!Number.isFinite(at) || !Number.isFinite(up) || !Number.isFinite(down))
|
|
54
|
+
continue;
|
|
55
|
+
if (at - down < lo)
|
|
56
|
+
lo = at - down;
|
|
57
|
+
if (at + up > hi)
|
|
58
|
+
hi = at + up;
|
|
59
|
+
}
|
|
60
|
+
if (lo > hi)
|
|
61
|
+
return null;
|
|
62
|
+
return a.along === 'domain' ? { first: { min: lo, max: hi } } : { second: { min: lo, max: hi } };
|
|
63
|
+
}
|
|
64
|
+
limit(series) {
|
|
65
|
+
const a = this.attrs, relative = a.mode === 'relative', domain = a.along === 'domain';
|
|
66
|
+
const own = series.ownExtent(domain ? 'first' : 'second'), up = series.data.channelExtent(a.error), down = series.data.channelExtent(this.negative);
|
|
67
|
+
const values = series.ownExtent('second');
|
|
68
|
+
if (!own || !up || !down || !values)
|
|
69
|
+
return null;
|
|
70
|
+
const most = Math.max(Math.abs(values.min), Math.abs(values.max)), k = relative ? most : 1;
|
|
71
|
+
const e = { min: own.min - Math.max(Math.abs(down.min), Math.abs(down.max)) * k, max: own.max + Math.max(Math.abs(up.min), Math.abs(up.max)) * k };
|
|
72
|
+
return domain ? { first: e } : { second: e };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The places in plot pixels of the lower end, the upper end, and a point a little further along the cap's direction from
|
|
76
|
+
* each (which says which way the cap runs on this plane), for every row: `[lo, hi, loAlong, hiAlong]` of one row after another,
|
|
77
|
+
* NaN for a row with no bar.
|
|
78
|
+
*/
|
|
79
|
+
ends(series) {
|
|
80
|
+
const d = series.drawn;
|
|
81
|
+
if (!d)
|
|
82
|
+
return null;
|
|
83
|
+
const a = this.attrs, relative = a.mode === 'relative', domain = a.along === 'domain', first = series.firstAxis, second = series.secondAxis;
|
|
84
|
+
const n = d.points.count, rs = d.stride, bound = d.channels.length, base = rs - bound;
|
|
85
|
+
const colError = base + d.channels.indexOf(a.error), colNeg = base + d.channels.indexOf(this.negative);
|
|
86
|
+
const pts = new Float64Array(n * 8), held = new Uint8Array(n);
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
const row = i * rs, x = d.raw[row + d.firstColumn], own = d.raw[row + d.secondColumn];
|
|
89
|
+
const centre = domain ? x : d.top ? d.top[i] : own;
|
|
90
|
+
const k = relative ? Math.abs(own) : 1, up = Math.abs(d.raw[row + colError]) * k, down = Math.abs(d.raw[row + colNeg]) * k;
|
|
91
|
+
const bad = !Number.isFinite(centre) || !Number.isFinite(up) || !Number.isFinite(down) || (domain ? !Number.isFinite(own) : !Number.isFinite(x));
|
|
92
|
+
const lo = bad ? NaN : centre - down, hi = bad ? NaN : centre + up, k8 = i * 8;
|
|
93
|
+
// A lower end at or below zero has no place on a log axis: it is held at the edge (below).
|
|
94
|
+
if (!bad && lo <= 0 && centre > 0)
|
|
95
|
+
held[i] = 1;
|
|
96
|
+
// Four points, each as first coordinate then second.
|
|
97
|
+
const put = (slot, f, sc) => { pts[k8 + slot * 2] = f; pts[k8 + slot * 2 + 1] = sc; };
|
|
98
|
+
if (domain) {
|
|
99
|
+
put(0, lo, own);
|
|
100
|
+
put(1, hi, own);
|
|
101
|
+
put(2, lo, own);
|
|
102
|
+
put(3, hi, own);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
put(0, x, lo);
|
|
106
|
+
put(1, x, hi);
|
|
107
|
+
put(2, x, lo);
|
|
108
|
+
put(3, x, hi);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
first.normalize(pts, n * 4, 2, 0);
|
|
112
|
+
second.normalize(pts, n * 4, 2, 1);
|
|
113
|
+
const shift = domain ? 0 : barShift(series);
|
|
114
|
+
const clamp = (v) => (v < -REACH ? -REACH : v > 1 + REACH ? 1 + REACH : v);
|
|
115
|
+
for (let i = 0; i < n; i++) {
|
|
116
|
+
for (const slot of [0, 2]) {
|
|
117
|
+
const k = i * 8 + slot * 2 + (domain ? 0 : 1);
|
|
118
|
+
if (held[i] && !Number.isFinite(pts[k]))
|
|
119
|
+
pts[k] = -REACH;
|
|
120
|
+
}
|
|
121
|
+
for (let slot = 0; slot < 4; slot++) {
|
|
122
|
+
const k = i * 8 + slot * 2;
|
|
123
|
+
pts[k] = clamp(pts[k]);
|
|
124
|
+
pts[k + 1] = clamp(pts[k + 1]);
|
|
125
|
+
if (!domain)
|
|
126
|
+
pts[k] = pts[k] + shift;
|
|
127
|
+
}
|
|
128
|
+
// The two along points step the cap's way: along the domain for a value error, along the value for a domain one.
|
|
129
|
+
for (const slot of [2, 3]) {
|
|
130
|
+
const k = i * 8 + slot * 2;
|
|
131
|
+
if (domain)
|
|
132
|
+
pts[k + 1] = pts[k + 1] + TANGENT;
|
|
133
|
+
else
|
|
134
|
+
pts[k] = pts[k] + TANGENT;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
d.plane.place({ data: pts, stride: 2, count: n * 4 }, first, second);
|
|
138
|
+
return { n, pts };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** A whisker from the lower end to the upper, with a cap across each, as one path. */
|
|
142
|
+
export class ErrorBars extends ErrorRenderer {
|
|
143
|
+
static attrTypes = { ...SeriesRenderer.attrTypes, error: STRING, negativeError: STRING, capWidth: NUMBER, mode: STRING, along: STRING };
|
|
144
|
+
/** Neutral by default (the theme's line colour), so that it can be seen on the bars it goes with. */
|
|
145
|
+
look() {
|
|
146
|
+
const a = this.attrs;
|
|
147
|
+
return this.styleFor(['errorBar', 'line'], { color: a.stroke, width: a.strokeWidth, opacity: a.opacity });
|
|
148
|
+
}
|
|
149
|
+
get reach() { return (this.attrs.capWidth ?? 8) / 2; }
|
|
150
|
+
sample(s, parent, key, width, height) {
|
|
151
|
+
const look = this.look(), n = s.node(key, 'path', { parent }), x = width / 2, cap = Math.min(width, this.attrs.capWidth ?? 8) / 2;
|
|
152
|
+
s.set(n, 'd', `M${x} 1V${height - 1}M${x - cap} 1H${x + cap}M${x - cap} ${height - 1}H${x + cap}`);
|
|
153
|
+
s.set(n, 'fill', 'none');
|
|
154
|
+
s.set(n, 'stroke', look.color);
|
|
155
|
+
s.set(n, 'stroke-width', look.width);
|
|
156
|
+
s.set(n, 'opacity', look.opacity);
|
|
157
|
+
}
|
|
158
|
+
draw() {
|
|
159
|
+
const s = this.surface, series = this.requireParent(), look = this.look(), cap = (this.attrs.capWidth ?? 8) / 2;
|
|
160
|
+
const e = this.ends(series);
|
|
161
|
+
let d = '';
|
|
162
|
+
if (e) {
|
|
163
|
+
const p = e.pts;
|
|
164
|
+
for (let i = 0; i < e.n; i++) {
|
|
165
|
+
const k = i * 8, lx = p[k], ly = p[k + 1], hx = p[k + 2], hy = p[k + 3];
|
|
166
|
+
if (!(Number.isFinite(lx) && Number.isFinite(ly) && Number.isFinite(hx) && Number.isFinite(hy)))
|
|
167
|
+
continue;
|
|
168
|
+
d += `M${f2(lx)} ${f2(ly)}L${f2(hx)} ${f2(hy)}`;
|
|
169
|
+
if (cap > 0) {
|
|
170
|
+
for (const [x, y, ax, ay] of [[lx, ly, p[k + 4], p[k + 5]], [hx, hy, p[k + 6], p[k + 7]]]) {
|
|
171
|
+
const tx = ax - x, ty = ay - y, len = Math.hypot(tx, ty);
|
|
172
|
+
if (!(len > 0))
|
|
173
|
+
continue;
|
|
174
|
+
const ux = (tx / len) * cap, uy = (ty / len) * cap;
|
|
175
|
+
d += `M${f2(x - ux)} ${f2(y - uy)}L${f2(x + ux)} ${f2(y + uy)}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const n = s.node('bars', 'path');
|
|
181
|
+
s.set(n, 'd', d);
|
|
182
|
+
s.set(n, 'fill', 'none');
|
|
183
|
+
s.set(n, 'stroke', look.color);
|
|
184
|
+
s.set(n, 'stroke-width', look.width);
|
|
185
|
+
s.set(n, 'stroke-linecap', 'butt');
|
|
186
|
+
s.set(n, 'opacity', look.opacity);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** The region between the lower and the upper ends, as one path; a gap splits it. */
|
|
190
|
+
export class ErrorBand extends ErrorRenderer {
|
|
191
|
+
static attrTypes = { ...SeriesRenderer.attrTypes, error: STRING, negativeError: STRING, mode: STRING, along: STRING };
|
|
192
|
+
look() {
|
|
193
|
+
const a = this.attrs;
|
|
194
|
+
return this.styleFor(['errorBand', 'shape'], { fill: a.fill, stroke: a.stroke, strokeWidth: a.strokeWidth, opacity: a.opacity }, { fill: this.requireParent().accentColor });
|
|
195
|
+
}
|
|
196
|
+
get swatch() { return this.look().fill; }
|
|
197
|
+
sample(s, parent, key, width, height) {
|
|
198
|
+
const look = this.look(), n = s.node(key, 'path', { parent });
|
|
199
|
+
s.set(n, 'd', `M0 ${height * 0.2}H${width}V${height * 0.8}H0Z`);
|
|
200
|
+
s.set(n, 'fill', look.fill);
|
|
201
|
+
s.set(n, 'fill-opacity', look.opacity);
|
|
202
|
+
s.set(n, 'stroke', look.stroke);
|
|
203
|
+
s.set(n, 'stroke-width', look.strokeWidth);
|
|
204
|
+
}
|
|
205
|
+
draw() {
|
|
206
|
+
const s = this.surface, series = this.requireParent(), look = this.look();
|
|
207
|
+
const e = this.ends(series);
|
|
208
|
+
let d = '';
|
|
209
|
+
if (e) {
|
|
210
|
+
const p = e.pts, ok = (i) => { const k = i * 8; return Number.isFinite(p[k]) && Number.isFinite(p[k + 1]) && Number.isFinite(p[k + 2]) && Number.isFinite(p[k + 3]); };
|
|
211
|
+
for (let i = 0; i < e.n;) {
|
|
212
|
+
if (!ok(i)) {
|
|
213
|
+
i++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
let j = i;
|
|
217
|
+
while (j < e.n && ok(j))
|
|
218
|
+
j++;
|
|
219
|
+
// The run i..j-1: forward along the upper ends, back along the lower.
|
|
220
|
+
d += `M${f2(p[i * 8 + 2])} ${f2(p[i * 8 + 3])}`;
|
|
221
|
+
for (let r = i + 1; r < j; r++)
|
|
222
|
+
d += `L${f2(p[r * 8 + 2])} ${f2(p[r * 8 + 3])}`;
|
|
223
|
+
for (let r = j - 1; r >= i; r--)
|
|
224
|
+
d += `L${f2(p[r * 8])} ${f2(p[r * 8 + 1])}`;
|
|
225
|
+
d += 'Z';
|
|
226
|
+
i = j;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const n = s.node('band', 'path');
|
|
230
|
+
s.set(n, 'd', d);
|
|
231
|
+
s.set(n, 'fill', look.fill);
|
|
232
|
+
s.set(n, 'fill-opacity', look.opacity);
|
|
233
|
+
s.set(n, 'stroke', look.stroke);
|
|
234
|
+
s.set(n, 'stroke-width', look.strokeWidth);
|
|
235
|
+
s.set(n, 'stroke-linejoin', 'round');
|
|
236
|
+
}
|
|
237
|
+
}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The events an element can receive, and the hit a pointer query returns.
|
|
3
|
+
*
|
|
4
|
+
* Why events rather than methods: the tree is heterogeneous and open (anyone can subclass an element), so the base class
|
|
5
|
+
* offers one `onEvent` and each subclass switches on what it cares about. Flow of one frame:
|
|
6
|
+
* ATTACH once, when the element joins a chart: create wrappers, subscribe.
|
|
7
|
+
* MEASURE parent → child, asking for a size range (the answer is written onto the event).
|
|
8
|
+
* ARRANGE parent → child, giving it its final rectangle.
|
|
9
|
+
* PAINT broadcast to everyone after layout: draw through the surface.
|
|
10
|
+
* POINTER platform → chart → elements under the pointer (in their own coordinates); `handled` suppresses browser defaults.
|
|
11
|
+
* HOVER chart → elements that asked (`Chart.watchPointer`), after each POINTER move, with the pointer's place in the
|
|
12
|
+
* plane it is over, or that it has left. For things outside the plane that follow the pointer (a tooltip);
|
|
13
|
+
* what is under it is theirs to ask (hit.ts), so the chart does no hit testing.
|
|
14
|
+
* KEY platform → chart → the element that owns the focused node (see `Chart.focusable`). `handled` keeps the browser
|
|
15
|
+
* from also acting on the key (scrolling the page on an arrow, say).
|
|
16
|
+
* FOCUS chart → the owner of a node that gained or lost focus, and whether a ring should show (keyboard focus does).
|
|
17
|
+
*/
|
|
18
|
+
import type { Axis } from './axis.ts';
|
|
19
|
+
import type { AnyElement } from './element.ts';
|
|
20
|
+
import type { LayoutRequest, Rect, Size } from './geometry.ts';
|
|
21
|
+
import type { Platform, PlatformNode } from './platform.ts';
|
|
22
|
+
import type { Plane } from './plane.ts';
|
|
23
|
+
import type { Series } from './series.ts';
|
|
24
|
+
/** Numeric (not string) types so dispatch is a cheap `switch` and the event union discriminates on `type`. */
|
|
25
|
+
export declare const EVENT_ATTACH = 1;
|
|
26
|
+
export declare const EVENT_MEASURE = 2;
|
|
27
|
+
export declare const EVENT_ARRANGE = 3;
|
|
28
|
+
export declare const EVENT_PAINT = 4;
|
|
29
|
+
export declare const EVENT_POINTER = 5;
|
|
30
|
+
export declare const EVENT_HOVER = 6;
|
|
31
|
+
export declare const EVENT_KEY = 7;
|
|
32
|
+
export declare const EVENT_FOCUS = 8;
|
|
33
|
+
/** What every event has. Concrete events are this plus the fields of their interface, copied on with Object.assign. */
|
|
34
|
+
export declare class EventBase<T extends number = number> {
|
|
35
|
+
readonly type: T;
|
|
36
|
+
readonly target: AnyElement;
|
|
37
|
+
constructor(target: AnyElement);
|
|
38
|
+
}
|
|
39
|
+
export interface AttachEvent extends EventBase<typeof EVENT_ATTACH> {
|
|
40
|
+
readonly platform: Platform;
|
|
41
|
+
}
|
|
42
|
+
export interface MeasureEvent extends EventBase<typeof EVENT_MEASURE> {
|
|
43
|
+
readonly available: Size;
|
|
44
|
+
/** The element writes its answer here. Defaults to "needs nothing". */
|
|
45
|
+
request: LayoutRequest;
|
|
46
|
+
}
|
|
47
|
+
export interface ArrangeEvent extends EventBase<typeof EVENT_ARRANGE> {
|
|
48
|
+
readonly rect: Rect;
|
|
49
|
+
}
|
|
50
|
+
/** An element draws through its own surface, so the event carries nothing. */
|
|
51
|
+
export interface PaintEvent extends EventBase<typeof EVENT_PAINT> {
|
|
52
|
+
}
|
|
53
|
+
export interface ChartPointerEvent extends EventBase<typeof EVENT_POINTER> {
|
|
54
|
+
readonly kind: 'move' | 'down' | 'up' | 'leave' | 'wheel' | 'pinch';
|
|
55
|
+
/** Coordinates in the receiving element's own space (a plane receives plot-area pixels). */
|
|
56
|
+
readonly x: number;
|
|
57
|
+
readonly y: number;
|
|
58
|
+
readonly buttons: number;
|
|
59
|
+
readonly deltaY: number;
|
|
60
|
+
/** Sideways wheel travel in pixels. */
|
|
61
|
+
readonly deltaX: number;
|
|
62
|
+
/** For `pinch`: the factor by which the fingers spread since the last event. 1 otherwise. */
|
|
63
|
+
readonly scale: number;
|
|
64
|
+
/** Which finger or device this is: 0 for a mouse. */
|
|
65
|
+
readonly id: number;
|
|
66
|
+
readonly pointerType: 'mouse' | 'touch' | 'pen';
|
|
67
|
+
/** The axis the pointer is over, for a plane: set where it is on the strip an axis takes beside the plot. Null over the plot and everywhere else. */
|
|
68
|
+
readonly axis: Axis | null;
|
|
69
|
+
/** True when the pointer is beside the plot (over an axis) and not on it. Elements that work on what is plotted ignore such events. */
|
|
70
|
+
readonly outside: boolean;
|
|
71
|
+
readonly shift: boolean;
|
|
72
|
+
readonly ctrl: boolean;
|
|
73
|
+
/** An element sets this when it used the input, which keeps the browser from also acting on it. */
|
|
74
|
+
handled: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface HoverEvent extends EventBase<typeof EVENT_HOVER> {
|
|
77
|
+
/** The plane the pointer is over (or has just left, when `x` is NaN). */
|
|
78
|
+
readonly plane: Plane;
|
|
79
|
+
/** The pointer in the plane's plot pixels; NaN once it has left. */
|
|
80
|
+
readonly x: number;
|
|
81
|
+
readonly y: number;
|
|
82
|
+
readonly buttons: number;
|
|
83
|
+
}
|
|
84
|
+
export interface ChartKeyEvent extends EventBase<typeof EVENT_KEY> {
|
|
85
|
+
readonly kind: 'down' | 'up';
|
|
86
|
+
/** The key as the browser names it (`KeyboardEvent.key`): 'ArrowLeft', 'Enter', ' ', '+'. */
|
|
87
|
+
readonly key: string;
|
|
88
|
+
readonly shift: boolean;
|
|
89
|
+
readonly ctrl: boolean;
|
|
90
|
+
readonly alt: boolean;
|
|
91
|
+
/** True for the repeats a held key sends after the first. */
|
|
92
|
+
readonly repeat: boolean;
|
|
93
|
+
/** The focused node, which an element with several focus targets tells apart. */
|
|
94
|
+
readonly node: PlatformNode;
|
|
95
|
+
handled: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface FocusEvent extends EventBase<typeof EVENT_FOCUS> {
|
|
98
|
+
readonly node: PlatformNode;
|
|
99
|
+
readonly focused: boolean;
|
|
100
|
+
/** True when focus should be shown: it came by keyboard, not by a click. */
|
|
101
|
+
readonly visible: boolean;
|
|
102
|
+
}
|
|
103
|
+
export type ChartEvent = AttachEvent | MeasureEvent | ArrangeEvent | PaintEvent | ChartPointerEvent | HoverEvent | ChartKeyEvent | FocusEvent;
|
|
104
|
+
/** `Omit` applied to each member of a union, so the init of every event keeps its own fields. */
|
|
105
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
106
|
+
export type ChartEventInit = DistributiveOmit<ChartEvent, keyof EventBase>;
|
|
107
|
+
/** One allocation per event: the init fields are copied onto a fresh base, so subclasses add fields without classes. */
|
|
108
|
+
export declare function makeEvent(init: ChartEventInit, target: AnyElement): ChartEvent;
|
|
109
|
+
/**
|
|
110
|
+
* What a series reports when the pointer is near it. A hit is always one of the series' drawn points, whatever the
|
|
111
|
+
* `hit` mode measured the distance against: for a line it is the end of the nearest segment that is nearer.
|
|
112
|
+
*/
|
|
113
|
+
export interface Hit {
|
|
114
|
+
readonly series: Series;
|
|
115
|
+
readonly plane: Plane;
|
|
116
|
+
/** Index in the drawn points (the series' `points`). */
|
|
117
|
+
readonly index: number;
|
|
118
|
+
/**
|
|
119
|
+
* The point's row in the source's own storage, which stays the same when edits re-sort the points: edit by this.
|
|
120
|
+
* Absent when the drawn points are not the source's (a `Decimator` made them), so such a hit can be shown but not edited.
|
|
121
|
+
*/
|
|
122
|
+
readonly row?: number;
|
|
123
|
+
/** The point's two coordinates in data terms: what the series' first and second axis read. */
|
|
124
|
+
readonly first: number;
|
|
125
|
+
readonly second: number;
|
|
126
|
+
/** Its bound channels' values, by name. */
|
|
127
|
+
readonly channels: Readonly<Record<string, number>>;
|
|
128
|
+
/** Where the point is drawn, in plot pixels. */
|
|
129
|
+
readonly px: number;
|
|
130
|
+
readonly py: number;
|
|
131
|
+
/**
|
|
132
|
+
* In a stack: how far the point is plotted from its own value (the layers below it), so a pointer's value less this
|
|
133
|
+
* is the layer's own. Absent outside a stack, and where the stack scales the values (normalized), which an edit cannot undo.
|
|
134
|
+
*/
|
|
135
|
+
readonly lift?: number;
|
|
136
|
+
/** How far the pointer is, in pixels, by the series' `hit` mode (0 inside an area, and for an interpolated hit, which is on the line by construction). */
|
|
137
|
+
readonly distance: number;
|
|
138
|
+
/**
|
|
139
|
+
* Set on a hit made by `Series.interpolate`: `first` and `second` are a place on the line, not a point of the data,
|
|
140
|
+
* and `index` and `row` are those of the nearer of the two points it lies between.
|
|
141
|
+
*/
|
|
142
|
+
readonly interpolated?: boolean;
|
|
143
|
+
}
|
|
144
|
+
export {};
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Numeric (not string) types so dispatch is a cheap `switch` and the event union discriminates on `type`. */
|
|
2
|
+
export const EVENT_ATTACH = 1;
|
|
3
|
+
export const EVENT_MEASURE = 2;
|
|
4
|
+
export const EVENT_ARRANGE = 3;
|
|
5
|
+
export const EVENT_PAINT = 4;
|
|
6
|
+
export const EVENT_POINTER = 5;
|
|
7
|
+
export const EVENT_HOVER = 6;
|
|
8
|
+
export const EVENT_KEY = 7;
|
|
9
|
+
export const EVENT_FOCUS = 8;
|
|
10
|
+
/** What every event has. Concrete events are this plus the fields of their interface, copied on with Object.assign. */
|
|
11
|
+
export class EventBase {
|
|
12
|
+
target;
|
|
13
|
+
constructor(target) { this.target = target; }
|
|
14
|
+
}
|
|
15
|
+
/** One allocation per event: the init fields are copied onto a fresh base, so subclasses add fields without classes. */
|
|
16
|
+
export function makeEvent(init, target) {
|
|
17
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the event is built up from a base and an `init` of the event type's fields, which the event union cannot express generically
|
|
18
|
+
return Object.assign(new EventBase(target), init);
|
|
19
|
+
}
|
package/dist/focus.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard focus, shared by everything that can be reached with Tab: the ring that shows where focus is, and the keys a
|
|
3
|
+
* button answers to.
|
|
4
|
+
*
|
|
5
|
+
* An element that wants focus does three things: give a node a `tabindex` and tell the chart it owns it
|
|
6
|
+
* (`chart.focusable(this, node)`), draw the ring while it has focus (EVENT_FOCUS says when, and whether a ring is wanted:
|
|
7
|
+
* only for keyboard focus, as the browser's own controls do), and answer EVENT_KEY.
|
|
8
|
+
*/
|
|
9
|
+
import { type ChartKeyEvent } from './events.ts';
|
|
10
|
+
import type { Rect } from './geometry.ts';
|
|
11
|
+
import type { PlatformNode } from './platform.ts';
|
|
12
|
+
import type { LineProps } from './theme.ts';
|
|
13
|
+
import type { Surface } from './surface.ts';
|
|
14
|
+
/** Draw the focus ring round `rect`, or take it away when `rect` is null. The `look` is the theme's `focusRing`. */
|
|
15
|
+
export declare function drawFocusRing(surface: Surface, key: string, look: LineProps, rect: Rect | null, parent?: PlatformNode): void;
|
|
16
|
+
/** Make a node a tab stop. The browser's own focus outline is turned off because the ring is drawn instead. */
|
|
17
|
+
export declare function makeTabStop(surface: Surface, node: PlatformNode, order?: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a key event presses a button, as a browser's own button does: Enter on the way down, Space on the way up (so it can
|
|
20
|
+
* be cancelled by moving away, and the page does not scroll). The keys are marked handled, and a held key does not repeat.
|
|
21
|
+
*/
|
|
22
|
+
export declare function pressesButton(ev: ChartKeyEvent): boolean;
|
package/dist/focus.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard focus, shared by everything that can be reached with Tab: the ring that shows where focus is, and the keys a
|
|
3
|
+
* button answers to.
|
|
4
|
+
*
|
|
5
|
+
* An element that wants focus does three things: give a node a `tabindex` and tell the chart it owns it
|
|
6
|
+
* (`chart.focusable(this, node)`), draw the ring while it has focus (EVENT_FOCUS says when, and whether a ring is wanted:
|
|
7
|
+
* only for keyboard focus, as the browser's own controls do), and answer EVENT_KEY.
|
|
8
|
+
*/
|
|
9
|
+
import {} from './events.js';
|
|
10
|
+
/** Room between the thing and its ring, in pixels. */
|
|
11
|
+
const RING_GAP = 2;
|
|
12
|
+
/** Draw the focus ring round `rect`, or take it away when `rect` is null. The `look` is the theme's `focusRing`. */
|
|
13
|
+
export function drawFocusRing(surface, key, look, rect, parent) {
|
|
14
|
+
const ring = surface.node(key, 'rect', { above: true, ...(parent && { parent }) });
|
|
15
|
+
if (!rect) {
|
|
16
|
+
surface.set(ring, 'display', 'none');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
surface.set(ring, 'display', 'inline');
|
|
20
|
+
surface.set(ring, 'x', rect.x - RING_GAP);
|
|
21
|
+
surface.set(ring, 'y', rect.y - RING_GAP);
|
|
22
|
+
surface.set(ring, 'width', rect.width + 2 * RING_GAP);
|
|
23
|
+
surface.set(ring, 'height', rect.height + 2 * RING_GAP);
|
|
24
|
+
surface.set(ring, 'rx', 3);
|
|
25
|
+
surface.set(ring, 'fill', 'none');
|
|
26
|
+
surface.set(ring, 'stroke', look.color);
|
|
27
|
+
surface.set(ring, 'stroke-width', look.width);
|
|
28
|
+
surface.set(ring, 'pointer-events', 'none');
|
|
29
|
+
}
|
|
30
|
+
/** Make a node a tab stop. The browser's own focus outline is turned off because the ring is drawn instead. */
|
|
31
|
+
export function makeTabStop(surface, node, order = 0) {
|
|
32
|
+
surface.set(node, 'tabindex', order);
|
|
33
|
+
surface.set(node, 'style', 'outline:none');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether a key event presses a button, as a browser's own button does: Enter on the way down, Space on the way up (so it can
|
|
37
|
+
* be cancelled by moving away, and the page does not scroll). The keys are marked handled, and a held key does not repeat.
|
|
38
|
+
*/
|
|
39
|
+
export function pressesButton(ev) {
|
|
40
|
+
if (ev.ctrl || ev.alt)
|
|
41
|
+
return false;
|
|
42
|
+
if (ev.key === 'Enter') {
|
|
43
|
+
ev.handled = true;
|
|
44
|
+
return ev.kind === 'down' && !ev.repeat;
|
|
45
|
+
}
|
|
46
|
+
if (ev.key === ' ' || ev.key === 'Spacebar') {
|
|
47
|
+
ev.handled = true;
|
|
48
|
+
return ev.kind === 'up';
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Chart } from './chart.ts';
|
|
2
|
+
import type { Cursor, CursorChild } from './cursor.ts';
|
|
3
|
+
import { type BaseAttributes, ChartElement } from './element.ts';
|
|
4
|
+
import { type ChartEvent, type Hit } from './events.ts';
|
|
5
|
+
/** What a `ChartTitle` is: the heading, the line under it, or the note at the foot. */
|
|
6
|
+
export type TitleRole = 'title' | 'subtitle' | 'caption';
|
|
7
|
+
export interface ChartTitleAttributes extends BaseAttributes {
|
|
8
|
+
/** The words. A '\n' starts another line. */
|
|
9
|
+
text: string;
|
|
10
|
+
/** What it is, which decides its look and (unless `side` says) where it goes. Default 'title'. */
|
|
11
|
+
role?: TitleRole;
|
|
12
|
+
/** The edge it spans. Default: the top, except for a caption, which goes at the bottom. */
|
|
13
|
+
side?: 'top' | 'bottom';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Text across the top or bottom of the chart. One element serves every role: a title, a subtitle, a caption or a
|
|
17
|
+
* footnote differ only in their `role` (and so their look) and `side`. Several on one edge stack outwards from it in the
|
|
18
|
+
* order they were added, so a title followed by a subtitle puts the subtitle underneath.
|
|
19
|
+
*
|
|
20
|
+
* Where the text sits along its band is the style property `align` (`style: { chartTitle: { align: 'middle' } }`).
|
|
21
|
+
*/
|
|
22
|
+
export declare class ChartTitle extends ChartElement<Chart, [], ChartTitleAttributes> {
|
|
23
|
+
#private;
|
|
24
|
+
static attrTypes: {
|
|
25
|
+
text: 1;
|
|
26
|
+
id: 1;
|
|
27
|
+
meta: {
|
|
28
|
+
title: 1;
|
|
29
|
+
description: 1;
|
|
30
|
+
role: 1;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
private get role();
|
|
34
|
+
private get side();
|
|
35
|
+
private get look();
|
|
36
|
+
onEvent(ev: ChartEvent): void;
|
|
37
|
+
}
|
|
38
|
+
export interface ChartTooltipAttributes extends BaseAttributes {
|
|
39
|
+
/**
|
|
40
|
+
* Whether the box moves with the cursor, with no arm, or is set beside what is read, with an arm to it. Default: moves with
|
|
41
|
+
* the cursor when the cursor is 'shared' (there is no one point to be beside), else set beside the point.
|
|
42
|
+
*/
|
|
43
|
+
follow?: boolean;
|
|
44
|
+
/** A sample of the series (its line, box or marker, as drawn on the plot) before its name, or without one before its first line. Default false. */
|
|
45
|
+
swatch?: boolean;
|
|
46
|
+
/** The text for one hit. Default: the series name, then each coordinate (with its axis' title if it has one) and the channels. */
|
|
47
|
+
format?: (hit: Hit) => string;
|
|
48
|
+
/**
|
|
49
|
+
* Tell a screen reader what the box says when the cursor is moved by keyboard (see `Platform.announce`), so it can be read
|
|
50
|
+
* without seeing it. Default true. The text is the box's own, which this element does not translate: `format` words it.
|
|
51
|
+
*/
|
|
52
|
+
announce?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Shows what the cursor reads, in a box styled like a callout. It is a child of a `Cursor`, which finds the reading (the
|
|
56
|
+
* nearest point, or every series at the cursor's place) from the pointer or the keyboard; this only draws it, above whatever
|
|
57
|
+
* comes before it in the cursor, so put a `Crosshair` first to have the lines under the box. It takes no room of its own.
|
|
58
|
+
*
|
|
59
|
+
* It looks like a callout (`tooltipBox`, `tooltipText` and `tooltip` metrics, each falling back to the callout's own), so a
|
|
60
|
+
* theme that styles callouts styles tooltips too. Touch has no hover, so it shows while a finger is down. It hides while
|
|
61
|
+
* a button is held, which is when something is being dragged.
|
|
62
|
+
*/
|
|
63
|
+
export declare class ChartTooltip extends ChartElement<Cursor, [], ChartTooltipAttributes> implements CursorChild {
|
|
64
|
+
#private;
|
|
65
|
+
/** Moves with the cursor when it has no arm. */
|
|
66
|
+
get follows(): boolean;
|
|
67
|
+
onEvent(ev: ChartEvent): void;
|
|
68
|
+
private look;
|
|
69
|
+
private paint;
|
|
70
|
+
}
|