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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/adapt.d.ts +10 -0
  4. package/dist/adapt.js +34 -0
  5. package/dist/annotation.d.ts +120 -0
  6. package/dist/annotation.js +267 -0
  7. package/dist/aria.d.ts +24 -0
  8. package/dist/aria.js +31 -0
  9. package/dist/attrs.d.ts +81 -0
  10. package/dist/attrs.js +84 -0
  11. package/dist/axis.d.ts +137 -0
  12. package/dist/axis.js +185 -0
  13. package/dist/bars.d.ts +80 -0
  14. package/dist/bars.js +255 -0
  15. package/dist/boxplot.d.ts +57 -0
  16. package/dist/boxplot.js +177 -0
  17. package/dist/boxzoom.d.ts +39 -0
  18. package/dist/boxzoom.js +138 -0
  19. package/dist/brush.d.ts +60 -0
  20. package/dist/brush.js +185 -0
  21. package/dist/bucket.d.ts +22 -0
  22. package/dist/bucket.js +62 -0
  23. package/dist/callout.d.ts +30 -0
  24. package/dist/callout.js +98 -0
  25. package/dist/chart.d.ts +126 -0
  26. package/dist/chart.js +257 -0
  27. package/dist/crosshair.d.ts +28 -0
  28. package/dist/crosshair.js +78 -0
  29. package/dist/cursor.d.ts +76 -0
  30. package/dist/cursor.js +344 -0
  31. package/dist/curve.d.ts +50 -0
  32. package/dist/curve.js +87 -0
  33. package/dist/curves.d.ts +104 -0
  34. package/dist/curves.js +375 -0
  35. package/dist/data.d.ts +209 -0
  36. package/dist/data.js +276 -0
  37. package/dist/decimation.d.ts +86 -0
  38. package/dist/decimation.js +410 -0
  39. package/dist/density.d.ts +55 -0
  40. package/dist/density.js +252 -0
  41. package/dist/dimension.d.ts +103 -0
  42. package/dist/dimension.js +205 -0
  43. package/dist/dom-platform.d.ts +53 -0
  44. package/dist/dom-platform.js +190 -0
  45. package/dist/element.d.ts +232 -0
  46. package/dist/element.js +414 -0
  47. package/dist/errors.d.ts +85 -0
  48. package/dist/errors.js +237 -0
  49. package/dist/events.d.ts +144 -0
  50. package/dist/events.js +19 -0
  51. package/dist/focus.d.ts +22 -0
  52. package/dist/focus.js +51 -0
  53. package/dist/furniture.d.ts +70 -0
  54. package/dist/furniture.js +166 -0
  55. package/dist/geometry.d.ts +40 -0
  56. package/dist/geometry.js +7 -0
  57. package/dist/heatmap.d.ts +57 -0
  58. package/dist/heatmap.js +207 -0
  59. package/dist/hit.d.ts +103 -0
  60. package/dist/hit.js +428 -0
  61. package/dist/index.d.ts +73 -0
  62. package/dist/index.js +73 -0
  63. package/dist/input.d.ts +42 -0
  64. package/dist/input.js +166 -0
  65. package/dist/jsx-dev-runtime.d.ts +9 -0
  66. package/dist/jsx-dev-runtime.js +13 -0
  67. package/dist/jsx-runtime.d.ts +59 -0
  68. package/dist/jsx-runtime.js +81 -0
  69. package/dist/key.d.ts +118 -0
  70. package/dist/key.js +199 -0
  71. package/dist/legend.d.ts +36 -0
  72. package/dist/legend.js +220 -0
  73. package/dist/mapped.d.ts +80 -0
  74. package/dist/mapped.js +106 -0
  75. package/dist/marker.d.ts +143 -0
  76. package/dist/marker.js +213 -0
  77. package/dist/panzoom.d.ts +119 -0
  78. package/dist/panzoom.js +467 -0
  79. package/dist/plane.d.ts +173 -0
  80. package/dist/plane.js +370 -0
  81. package/dist/platform.d.ts +113 -0
  82. package/dist/platform.js +80 -0
  83. package/dist/png.d.ts +44 -0
  84. package/dist/png.js +71 -0
  85. package/dist/pointer.d.ts +54 -0
  86. package/dist/pointer.js +132 -0
  87. package/dist/quantile.d.ts +3 -0
  88. package/dist/quantile.js +11 -0
  89. package/dist/radial.d.ts +116 -0
  90. package/dist/radial.js +336 -0
  91. package/dist/react.d.ts +441 -0
  92. package/dist/react.js +252 -0
  93. package/dist/registry.d.ts +2 -0
  94. package/dist/registry.js +51 -0
  95. package/dist/renderer.d.ts +191 -0
  96. package/dist/renderer.js +164 -0
  97. package/dist/scale.d.ts +37 -0
  98. package/dist/scale.js +19 -0
  99. package/dist/scales.d.ts +69 -0
  100. package/dist/scales.js +134 -0
  101. package/dist/serialize.d.ts +77 -0
  102. package/dist/serialize.js +321 -0
  103. package/dist/series.d.ts +304 -0
  104. package/dist/series.js +513 -0
  105. package/dist/simple.d.ts +35 -0
  106. package/dist/simple.js +20 -0
  107. package/dist/sources.d.ts +116 -0
  108. package/dist/sources.js +237 -0
  109. package/dist/stack.d.ts +61 -0
  110. package/dist/stack.js +172 -0
  111. package/dist/stats.d.ts +38 -0
  112. package/dist/stats.js +159 -0
  113. package/dist/steps.d.ts +50 -0
  114. package/dist/steps.js +154 -0
  115. package/dist/stub.d.ts +3 -0
  116. package/dist/stub.js +5 -0
  117. package/dist/style-names.d.ts +1 -0
  118. package/dist/style-names.js +6 -0
  119. package/dist/summary.d.ts +44 -0
  120. package/dist/summary.js +58 -0
  121. package/dist/surface.d.ts +94 -0
  122. package/dist/surface.js +145 -0
  123. package/dist/table.d.ts +39 -0
  124. package/dist/table.js +37 -0
  125. package/dist/text.d.ts +23 -0
  126. package/dist/text.js +44 -0
  127. package/dist/theme.d.ts +174 -0
  128. package/dist/theme.js +73 -0
  129. package/dist/themes.d.ts +9 -0
  130. package/dist/themes.js +56 -0
  131. package/dist/time-intl.d.ts +194 -0
  132. package/dist/time-intl.js +144 -0
  133. package/dist/time-temporal.d.ts +236 -0
  134. package/dist/time-temporal.js +58 -0
  135. package/dist/time.d.ts +216 -0
  136. package/dist/time.js +154 -0
  137. package/dist/trend.d.ts +151 -0
  138. package/dist/trend.js +500 -0
  139. package/dist/web-components.d.ts +62 -0
  140. package/dist/web-components.js +264 -0
  141. package/package.json +53 -3
package/dist/cursor.js ADDED
@@ -0,0 +1,344 @@
1
+ /**
2
+ * The cursor: where the user is pointing in a plane, and what is there. It draws nothing itself. It works out, from the
3
+ * mouse or from the keyboard, a `CursorState`, and the elements inside it (a `Crosshair`, a `ChartTooltip`, anything of
4
+ * your own) draw from that:
5
+ *
6
+ * new Chart({}, plane, new Cursor({ mode: 'shared' }, new Crosshair({ lines: 'first' }), new ChartTooltip()))
7
+ *
8
+ * Keeping the finding apart from the drawing is what lets them be mixed freely: a crosshair with no tooltip, a tooltip with
9
+ * no crosshair, two tooltips, one at the point and one in the corner, all reading the same cursor.
10
+ *
11
+ * Two ways to move it
12
+ * ───────────────────
13
+ * With the pointer, it is wherever the pointer is over a plane. With the keyboard, the plot is a tab stop: Left and Right step
14
+ * from point to point of a series (skipping gaps), Up and Down move to the previous or next series, Home and End go to
15
+ * the first and last point, PageUp and PageDown jump a tenth of the series, and Escape puts the cursor away. The keys are
16
+ * configurable (`keys`, as for `PanZoom`), and `keys: false` leaves the cursor to the pointer alone and takes it out of the
17
+ * tab order. Whichever moved last has the cursor. Name the plot with `meta.title`: it is what a screen reader says on arriving.
18
+ *
19
+ * What it finds
20
+ * ─────────────
21
+ * - 'nearest' (the default): the one point nearest the pointer, as the series' `hit` settings measure it (hit.ts); with the
22
+ * keyboard, the point stepped to.
23
+ * - 'shared': every series at the cursor's place along the first axis, so a line chart is read across all its lines at once.
24
+ * It is measured along the axis and not in pixels, so on a polar plane it is every series at the pointer's angle.
25
+ * `interpolate` reads the line between its points where the pointer is, in place of the nearest point.
26
+ *
27
+ * It holds no words: what a tooltip says is the tooltip's, and a screen reader hears the tooltip's text for a cursor moved
28
+ * by keyboard (see `Platform.announce`).
29
+ */
30
+ /* 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 */
31
+ import { AUTO, STRING } from './attrs.js';
32
+ import { writeAria } from './aria.js';
33
+ import { ChartElement, resolveRef } from './element.js';
34
+ import { inputOf } from './input.js';
35
+ import { EVENT_ATTACH, EVENT_FOCUS, EVENT_HOVER, EVENT_KEY, EVENT_PAINT } from './events.js';
36
+ import { drawFocusRing, makeTabStop } from './focus.js';
37
+ import { hitAcross, hitPlane, interpolate, interpolateAll, pointHit } from './hit.js';
38
+ import { Plane } from './plane.js';
39
+ export const DEFAULT_CURSOR_KEYS = {
40
+ next: ['ArrowRight'], previous: ['ArrowLeft'], nextSeries: ['ArrowDown'], previousSeries: ['ArrowUp'],
41
+ first: ['Home'], last: ['End'], jumpNext: ['PageDown'], jumpPrevious: ['PageUp'], clear: ['Escape'],
42
+ };
43
+ const JUMP = 0.1;
44
+ export class Cursor extends ChartElement {
45
+ static takesInput = inputOf;
46
+ static attrTypes = { ...ChartElement.attrTypes, plane: STRING, keys: { nextSeries: AUTO, previousSeries: AUTO, jumpNext: AUTO, jumpPrevious: AUTO } };
47
+ #pointer = null;
48
+ #by = 'pointer';
49
+ #focused = false;
50
+ #ring = false;
51
+ #node = null;
52
+ /** The point the keyboard is on. */
53
+ #at = null;
54
+ #version = 0;
55
+ #cache = null;
56
+ #signature = '';
57
+ #lastKeys = '';
58
+ /** The keys in force: the defaults with the ones asked for laid over them, or none. */
59
+ get keyMap() {
60
+ const k = this.attrs.keys;
61
+ return k === false ? null : { ...DEFAULT_CURSOR_KEYS, ...k };
62
+ }
63
+ /**
64
+ * Where the cursor is and what is there, or null when it is nowhere: the pointer is off the plot (or on its corner, for a
65
+ * round plot), a button is held (a drag), or the keyboard has put it away. Worked out afresh after each flush, since
66
+ * the data may have been edited, or the view panned, under a pointer that has not moved.
67
+ */
68
+ get state() {
69
+ const flush = this.chart?.flushCount ?? 0, cache = this.#cache;
70
+ if (cache && cache.version === this.#version && cache.flush === flush)
71
+ return cache.state;
72
+ const state = this.#compute();
73
+ this.#cache = { version: this.#version, flush, state };
74
+ return state;
75
+ }
76
+ /** The plane the cursor is tied to by `plane`, else the first. */
77
+ get plane() {
78
+ const ref = this.attrs.plane;
79
+ if (ref)
80
+ return resolveRef(ref, this, Plane);
81
+ for (const c of this.requireParent())
82
+ if (c instanceof Plane)
83
+ return c;
84
+ return null;
85
+ }
86
+ /** Put the keyboard cursor on a point of a series, as if stepped to, and show it. */
87
+ select(series, index) {
88
+ this.#at = { series, index };
89
+ this.#by = 'keyboard';
90
+ this.#changed(true);
91
+ }
92
+ onEvent(ev) {
93
+ switch (ev.type) {
94
+ case EVENT_ATTACH:
95
+ inputOf(this.requireParent()).watchPointer(this);
96
+ break;
97
+ case EVENT_HOVER: {
98
+ const pinned = this.attrs.plane ? this.plane : null;
99
+ if (pinned && ev.plane !== pinned)
100
+ break;
101
+ this.#pointer = ev.x === ev.x ? { plane: ev.plane, x: ev.x, y: ev.y, buttons: ev.buttons } : null;
102
+ // The pointer leaving ends only what the pointer was doing: a cursor the keyboard put there stays.
103
+ if (this.#pointer)
104
+ this.#by = 'pointer';
105
+ this.#changed(false);
106
+ break;
107
+ }
108
+ case EVENT_KEY:
109
+ this.#key(ev);
110
+ break;
111
+ case EVENT_FOCUS:
112
+ if (ev.node !== this.#node)
113
+ break;
114
+ this.#focused = ev.focused;
115
+ this.#ring = ev.focused && ev.visible;
116
+ if (ev.focused) {
117
+ this.#by = 'keyboard';
118
+ this.#begin();
119
+ }
120
+ else if (this.#by === 'keyboard')
121
+ this.#by = 'pointer';
122
+ this.#changed(true);
123
+ break;
124
+ case EVENT_PAINT:
125
+ this.#paintFocus();
126
+ break;
127
+ }
128
+ }
129
+ /** The keyboard's point: nothing yet means the first point of the first series. */
130
+ #begin() {
131
+ if (this.#at && this.#valid(this.#at))
132
+ return;
133
+ const plane = this.plane;
134
+ this.#at = null;
135
+ if (!plane)
136
+ return;
137
+ for (const series of this.#steppable(plane)) {
138
+ const index = this.#step(series, -1, 1);
139
+ if (index >= 0) {
140
+ this.#at = { series, index };
141
+ return;
142
+ }
143
+ }
144
+ }
145
+ /** Series the keyboard can stop on: shown, drawn, and taking hits. */
146
+ #steppable(plane) {
147
+ return plane.series.filter((s) => s.attrs.hit !== false && (s.drawn?.points.count ?? 0) > 0);
148
+ }
149
+ #valid(at) {
150
+ const plane = this.plane, d = at.series.drawn;
151
+ return !!plane && at.series.shown && at.series.plane === plane && !!d && at.index < d.points.count && this.#real(at.series, at.index);
152
+ }
153
+ /** Whether drawn point `index` is a point and not a gap. */
154
+ #real(series, index) {
155
+ const p = series.drawn?.points;
156
+ if (!p)
157
+ return false;
158
+ return p.data[index * p.stride] === p.data[index * p.stride] && p.data[index * p.stride + 1] === p.data[index * p.stride + 1];
159
+ }
160
+ /** The point `by` steps from `from` that is not a gap, or -1 when there is none that way. */
161
+ #step(series, from, by) {
162
+ const n = series.drawn?.points.count ?? 0;
163
+ for (let k = from + by; k >= 0 && k < n; k += by)
164
+ if (this.#real(series, k))
165
+ return k;
166
+ return -1;
167
+ }
168
+ #key(ev) {
169
+ const map = this.keyMap;
170
+ if (!map || ev.kind !== 'down' || ev.node !== this.#node || ev.ctrl || ev.alt || ev.shift)
171
+ return;
172
+ let action;
173
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- `for…in` gives `string`; the keys of `map` are `CursorAction`s
174
+ for (const name in map)
175
+ if (map[name].includes(ev.key)) {
176
+ action = name;
177
+ break;
178
+ }
179
+ if (!action)
180
+ return;
181
+ const plane = this.plane;
182
+ if (!plane)
183
+ return;
184
+ if (action === 'clear') {
185
+ if (this.state) {
186
+ ev.handled = true;
187
+ this.#at = null;
188
+ this.#by = 'keyboard';
189
+ this.#changed(true);
190
+ }
191
+ return;
192
+ }
193
+ ev.handled = true;
194
+ this.#by = 'keyboard';
195
+ if (!this.#at || !this.#valid(this.#at))
196
+ this.#begin();
197
+ const at = this.#at;
198
+ if (!at) {
199
+ this.#changed(true);
200
+ return;
201
+ }
202
+ const n = at.series.drawn?.points.count ?? 0;
203
+ const jump = Math.max(1, Math.round(n * JUMP));
204
+ let index = -1;
205
+ switch (action) {
206
+ case 'next':
207
+ index = this.#step(at.series, at.index, 1);
208
+ break;
209
+ case 'previous':
210
+ index = this.#step(at.series, at.index, -1);
211
+ break;
212
+ case 'jumpNext':
213
+ index = this.#reach(at, jump, 1);
214
+ break;
215
+ case 'jumpPrevious':
216
+ index = this.#reach(at, jump, -1);
217
+ break;
218
+ case 'first':
219
+ index = this.#step(at.series, -1, 1);
220
+ break;
221
+ case 'last':
222
+ index = this.#step(at.series, n, -1);
223
+ break;
224
+ case 'nextSeries':
225
+ case 'previousSeries': {
226
+ const list = this.#steppable(plane), at0 = list.indexOf(at.series), other = list[at0 + (action === 'nextSeries' ? 1 : -1)];
227
+ if (other)
228
+ this.#at = { series: other, index: this.#alongTo(at, other) };
229
+ this.#changed(true);
230
+ return;
231
+ }
232
+ }
233
+ if (index >= 0)
234
+ this.#at = { series: at.series, index };
235
+ this.#changed(true);
236
+ }
237
+ /** `count` real points on from a point, or the last one reached before running out. */
238
+ #reach(at, count, by) {
239
+ let index = at.index;
240
+ for (let k = 0; k < count; k++) {
241
+ const next = this.#step(at.series, index, by);
242
+ if (next < 0)
243
+ break;
244
+ index = next;
245
+ }
246
+ return index === at.index ? -1 : index;
247
+ }
248
+ /** The point of `to` nearest the point `from` is on, along the first axis, so moving to another series keeps one's place in the domain. */
249
+ #alongTo(from, to) {
250
+ const a = from.series.drawn, b = to.drawn;
251
+ if (!a || !b)
252
+ return this.#step(to, -1, 1);
253
+ const at = from.index * a.stride + a.firstColumn;
254
+ const axis = to.firstAxis, target = axis.fractionOf(a.raw[at]);
255
+ let best = this.#step(to, -1, 1), bestD = Infinity;
256
+ for (let k = 0; k < b.points.count; k++) {
257
+ if (!this.#real(to, k))
258
+ continue;
259
+ const d = Math.abs(axis.fractionOf(b.raw[k * b.stride + b.firstColumn]) - target);
260
+ if (d < bestD) {
261
+ bestD = d;
262
+ best = k;
263
+ }
264
+ }
265
+ return best;
266
+ }
267
+ /** The state for the input as it stands. */
268
+ #compute() {
269
+ const a = this.attrs, shared = a.mode === 'shared';
270
+ if (this.#by === 'keyboard') {
271
+ const at = this.#at;
272
+ if (!at || !this.#focused || !this.#valid(at))
273
+ return null;
274
+ const hit = pointHit(at.series, at.index);
275
+ if (!hit)
276
+ return null;
277
+ const plane = at.series.plane;
278
+ const hits = shared ? this.#shared(plane, hit.px, hit.py, hit) : [hit];
279
+ return { plane, x: hit.px, y: hit.py, source: 'keyboard', hits };
280
+ }
281
+ const p = this.#pointer;
282
+ if (!p || p.plane.chart !== this.chart || (p.buttons & 1) || !p.plane.covers(p.x, p.y))
283
+ return null;
284
+ const { plane, x, y } = p;
285
+ if (shared)
286
+ return { plane, x, y, source: 'pointer', hits: this.#shared(plane, x, y, null) };
287
+ const hit = hitPlane(plane, x, y);
288
+ // Reading the line under the pointer; the nearest point was only what said it was near.
289
+ const read = hit && a.interpolate === true ? interpolate(hit.series, plane.unplace(x, y, hit.series.firstAxis, hit.series.secondAxis)[0]) ?? hit : hit;
290
+ return { plane, x, y, source: 'pointer', hits: read ? [read] : [] };
291
+ }
292
+ /** Every series at a place: the line read there, or the point nearest in the first axis. The keyboard's own point leads for a series it is on. */
293
+ #shared(plane, x, y, own) {
294
+ const hits = this.attrs.interpolate === true ? interpolateAll(plane, x, y) : hitAcross(plane, x, y);
295
+ if (own) {
296
+ const i = hits.findIndex((h) => h.series === own.series);
297
+ if (i >= 0)
298
+ hits[i] = own;
299
+ }
300
+ return hits;
301
+ }
302
+ /** Redraw when something that shows changed. `force`: the keyboard moved, which always does. */
303
+ #changed(force) {
304
+ this.#version++;
305
+ const state = this.state;
306
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- a type guard in effect: probing a child for the optional `follows`
307
+ const moves = [...this].some((c) => c.attrs.visible !== false && c.follows === true);
308
+ const sig = !state ? '' : [
309
+ state.source, state.hits.map((h) => `${h.series.attrs.name ?? ''}:${h.row ?? h.index}:${h.first}:${h.second}`).join('|'),
310
+ moves || state.hits.length === 0 ? `@${state.x},${state.y}` : '',
311
+ ].join('#');
312
+ if (force || sig !== this.#signature) {
313
+ this.#signature = sig;
314
+ this.invalidate();
315
+ }
316
+ }
317
+ /** The plot as a tab stop, in the chart's pixels, with the ring when keyboard focus is on it. */
318
+ #paintFocus() {
319
+ const s = this.surface, plane = this.plane, map = this.keyMap;
320
+ if (!map || !plane) {
321
+ s.remove('focus');
322
+ s.remove('ring');
323
+ this.#node = null;
324
+ return;
325
+ }
326
+ const r = plane.rect, node = s.node('focus', 'rect');
327
+ s.set(node, 'x', r.x);
328
+ s.set(node, 'y', r.y);
329
+ s.set(node, 'width', r.width);
330
+ s.set(node, 'height', r.height);
331
+ s.set(node, 'fill', 'none');
332
+ s.set(node, 'pointer-events', 'none');
333
+ makeTabStop(s, node);
334
+ writeAria(s, node, this.attrs.meta ?? (plane.attrs.meta?.title !== undefined ? { title: plane.attrs.meta.title } : undefined), { role: 'application' });
335
+ const shortcuts = [...new Set(Object.values(map).flat())].join(' ');
336
+ if (shortcuts !== this.#lastKeys) {
337
+ this.#lastKeys = shortcuts;
338
+ s.set(node, 'aria-keyshortcuts', shortcuts);
339
+ }
340
+ inputOf(this.requireParent()).focusable(this, node);
341
+ this.#node = node;
342
+ drawFocusRing(s, 'ring', this.styleFor(['focusRing', 'line']), this.#ring ? { x: r.x + 3, y: r.y + 3, width: Math.max(0, r.width - 6), height: Math.max(0, r.height - 6) } : null);
343
+ }
344
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Curves: how a line, or the edge of an area, joins the points of a series.
3
+ *
4
+ * A `Curve` is the one child of a `LinePath` or an `AreaPath`, and all it does is turn a series that has been projected
5
+ * (its points in plot pixels, and through them the plane and the data) into path text: `line` for the stroke, `area` for
6
+ * the shapes between the line and a baseline. So the renderers know nothing of how points are joined, and a new way of
7
+ * joining them is one new class that nothing else has to know about, nor any bundle that does not use it carry.
8
+ *
9
+ * `LinearCurve` here is the plain one, straight lines, and the only one every chart needs. The others are in `curves.ts`
10
+ * (splines, arcs, bumps) and `steps.ts` (levels and jumps worked out in the plane's own terms). To write one of your own,
11
+ * extend `Curve` and give `line` and `area`, or extend `StrokeCurve` (curves.ts) and say only what pieces a run of points
12
+ * is made of.
13
+ *
14
+ * A curve that draws something other than straight lines through the points also gives its path as a `Trace`, which is
15
+ * what hits and tooltip readings are measured against, so that they land on what is on the screen.
16
+ */
17
+ import type { PointBuffer } from './data.ts';
18
+ import { type BaseAttributes, ChartElement } from './element.ts';
19
+ import type { AnySeriesRenderer, Trace } from './renderer.ts';
20
+ import type { Series } from './series.ts';
21
+ export interface CurveAttributes extends BaseAttributes {
22
+ }
23
+ /** Joins the points of a series. The child of a line or an area renderer. */
24
+ export declare abstract class Curve<A extends CurveAttributes = CurveAttributes> extends ChartElement<AnySeriesRenderer, [], A> {
25
+ /** The path of the series' line: each run of it (a stretch between gaps, or the whole of it when the series' `gaps` are 'connect') starts with an M. */
26
+ abstract line(series: Series): string;
27
+ /** The path of the shapes between the series' line and its baseline, `baseline` being a value (in a stack, the layer below is used instead). */
28
+ abstract area(series: Series, baseline: number): string;
29
+ /**
30
+ * What `line` (with a null `baseline`) or `area` draws, as the vertices a pointer is measured against and the point each
31
+ * belongs to, or null when it is straight lines through the points, which hits measure by themselves. Default null.
32
+ */
33
+ trace(_series: Series, _baseline: number | null): Trace | null;
34
+ }
35
+ /**
36
+ * The path text through placed points, lifting the pen at each gap (or, with `connect`, going across it). Built as
37
+ * compactly as SVG allows (an implicit lineto after the first pair) because for large series its size is the cost.
38
+ */
39
+ export declare function polyline(points: Readonly<PointBuffer>, connect: boolean): string;
40
+ /**
41
+ * The closed shapes between a line and its baseline: the line forward and the baseline backward, one shape for each run
42
+ * of rows where both are real (or, with `connect`, a single shape of all the real ones). Both buffers are placed points
43
+ * in the same order, so row `i` of one is the foot of row `i` of the other.
44
+ */
45
+ export declare function areaOutline(top: Readonly<PointBuffer>, base: Readonly<PointBuffer>, connect: boolean): string;
46
+ /** Straight lines from point to point: what a line chart is. A gap lifts the pen, or is crossed if the series says its `gaps` are 'connect'. */
47
+ export declare class LinearCurve extends Curve {
48
+ line(series: Series): string;
49
+ area(series: Series, baseline: number): string;
50
+ }
package/dist/curve.js ADDED
@@ -0,0 +1,87 @@
1
+ import { ChartElement } from './element.js';
2
+ import { f2 } from './geometry.js';
3
+ /** Joins the points of a series. The child of a line or an area renderer. */
4
+ export class Curve extends ChartElement {
5
+ /**
6
+ * What `line` (with a null `baseline`) or `area` draws, as the vertices a pointer is measured against and the point each
7
+ * belongs to, or null when it is straight lines through the points, which hits measure by themselves. Default null.
8
+ */
9
+ trace(_series, _baseline) { return null; }
10
+ }
11
+ /**
12
+ * The path text through placed points, lifting the pen at each gap (or, with `connect`, going across it). Built as
13
+ * compactly as SVG allows (an implicit lineto after the first pair) because for large series its size is the cost.
14
+ */
15
+ export function polyline(points, connect) {
16
+ const { data, count, stride } = points;
17
+ // Pen state: 0 up, 1 just moved, 2 drawing (implicit lineto pairs). A gap puts the pen up.
18
+ let d = '', pen = 0;
19
+ for (let i = 0; i < count; i++) {
20
+ const x = data[stride * i], y = data[stride * i + 1];
21
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
22
+ if (!connect)
23
+ pen = 0;
24
+ continue;
25
+ }
26
+ const xy = f2(x) + ' ' + f2(y);
27
+ switch (pen) {
28
+ case 0:
29
+ d += 'M' + xy;
30
+ pen = 1;
31
+ break;
32
+ case 1:
33
+ d += 'L' + xy;
34
+ pen = 2;
35
+ break;
36
+ default: d += ' ' + xy;
37
+ }
38
+ }
39
+ return d;
40
+ }
41
+ /**
42
+ * The closed shapes between a line and its baseline: the line forward and the baseline backward, one shape for each run
43
+ * of rows where both are real (or, with `connect`, a single shape of all the real ones). Both buffers are placed points
44
+ * in the same order, so row `i` of one is the foot of row `i` of the other.
45
+ */
46
+ export function areaOutline(top, base, connect) {
47
+ const { data, count, stride } = top, b = base.data, bs = base.stride;
48
+ const ok = (i) => {
49
+ const x = data[i * stride], y = data[i * stride + 1], bx = b[bs * i], by = b[bs * i + 1];
50
+ return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(bx) && Number.isFinite(by);
51
+ };
52
+ let d = '';
53
+ for (let i = 0; i < count;) {
54
+ if (!ok(i)) {
55
+ i++;
56
+ continue;
57
+ }
58
+ let end = i;
59
+ if (connect) {
60
+ for (let k = i + 1; k < count; k++)
61
+ if (ok(k))
62
+ end = k;
63
+ }
64
+ else
65
+ while (end + 1 < count && ok(end + 1))
66
+ end++;
67
+ d += `M${f2(data[i * stride])} ${f2(data[i * stride + 1])}L`;
68
+ let first = true;
69
+ for (let k = i + 1; k <= end; k++) {
70
+ if (connect && !ok(k))
71
+ continue;
72
+ d += (first ? '' : ' ') + f2(data[k * stride]) + ' ' + f2(data[k * stride + 1]);
73
+ first = false;
74
+ }
75
+ for (let k = end; k >= i; k--)
76
+ if (!connect || ok(k))
77
+ d += ' ' + f2(b[bs * k]) + ' ' + f2(b[bs * k + 1]);
78
+ d += 'Z';
79
+ i = end + 1;
80
+ }
81
+ return d;
82
+ }
83
+ /** Straight lines from point to point: what a line chart is. A gap lifts the pen, or is crossed if the series says its `gaps` are 'connect'. */
84
+ export class LinearCurve extends Curve {
85
+ line(series) { return polyline(series.points, series.attrs.gaps === 'connect'); }
86
+ area(series, baseline) { return areaOutline(series.points, series.baseline(baseline), series.attrs.gaps === 'connect'); }
87
+ }
@@ -0,0 +1,104 @@
1
+ import { Curve, type CurveAttributes } from './curve.ts';
2
+ import type { Trace } from './renderer.ts';
3
+ import type { Series } from './series.ts';
4
+ /** Receives the vertices of a sampled piece: where, and how far along the piece (0 to 1), which says which end owns it. */
5
+ export type Push = (x: number, y: number, t: number) => void;
6
+ /**
7
+ * One segment of a curve: where it ends (`x`, `y`), the (run-local) indices of the two points it runs between (`a`, `b`), so
8
+ * that a vertex on it can be given an owner, its path text beginning with the command letter, and a way to cut it into
9
+ * vertices for tracing from where the pen was.
10
+ */
11
+ export interface Piece {
12
+ readonly cmd: string;
13
+ readonly d: string;
14
+ readonly x: number;
15
+ readonly y: number;
16
+ readonly a: number;
17
+ readonly b: number;
18
+ sample(x0: number, y0: number, push: Push): void;
19
+ }
20
+ /** A straight line to (x, y). */
21
+ export declare function lineTo(x: number, y: number, a: number, b: number): Piece;
22
+ /** A cubic Bézier with control points (x1, y1) and (x2, y2), ending at (x, y). */
23
+ export declare function cubicTo(x1: number, y1: number, x2: number, y2: number, x: number, y: number, a: number, b: number): Piece;
24
+ /**
25
+ * One Hermite segment of a run as a cubic: from point `k` to `k + 1` of `p`, with the given tangents (derivatives with
26
+ * respect to a parameter that spans `h` over the segment) at its two ends.
27
+ */
28
+ export declare function hermiteTo(p: ArrayLike<number>, k: number, lx: number, ly: number, rx: number, ry: number, h: number): Piece;
29
+ /** A quadratic Bézier with control point (x1, y1), ending at (x, y). */
30
+ export declare function quadTo(x1: number, y1: number, x: number, y: number, a: number, b: number): Piece;
31
+ /** 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. */
32
+ export declare function smoothTo(cx: number, cy: number, x: number, y: number, a: number, b: number): Piece;
33
+ /** 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). */
34
+ export declare function arcTo(r: number, large: 0 | 1, sweep: 0 | 1, x: number, y: number, a: number, b: number): Piece;
35
+ /**
36
+ * A curve made of pieces. It says what the pieces through a run of points are (`pieces`), and this does the rest: the path
37
+ * text of a line and of an area, and the trace. Every run has at least two points; a single point draws nothing.
38
+ */
39
+ export declare abstract class StrokeCurve<A extends CurveAttributes = CurveAttributes> extends Curve<A> {
40
+ #private;
41
+ /** The pieces of the curve through `p` (x, y pairs, two or more points) after its first point. `series` is given when drawing, for a curve that depends on its axes. */
42
+ protected abstract pieces(p: ArrayLike<number>, series?: Series): Piece[];
43
+ /**
44
+ * The path text through `p`, starting with `lead`: 'M' to begin a path at the first point, or 'L' to carry on from
45
+ * wherever the pen is (for the way back round an area).
46
+ */
47
+ path(p: ArrayLike<number>, lead: 'M' | 'L', series?: Series): string;
48
+ /** 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. */
49
+ polyline(p: ArrayLike<number>, series?: Series): {
50
+ pts: number[];
51
+ owner: number[];
52
+ };
53
+ line(series: Series): string;
54
+ area(series: Series, baseline: number): string;
55
+ trace(series: Series, baseline: number | null): Trace | null;
56
+ }
57
+ /** A smooth line that never overshoots the data: a monotone cubic (Fritsch–Carlson). */
58
+ export declare class MonotoneCurve extends StrokeCurve {
59
+ protected pieces(p: ArrayLike<number>): Piece[];
60
+ }
61
+ /** The natural cubic spline: the smoothest line through the points, which swings past them. */
62
+ export declare class NaturalCurve extends StrokeCurve {
63
+ protected pieces(p: ArrayLike<number>): Piece[];
64
+ }
65
+ export interface CardinalCurveAttributes extends CurveAttributes {
66
+ /** How far the curve is tightened towards straight lines, 0 (the Catmull-Rom spline) to 1. Default 0. */
67
+ tension?: number;
68
+ }
69
+ /** A cardinal spline: through every point, with a tangent along the neighbours that `tension` shortens. */
70
+ export declare class CardinalCurve extends StrokeCurve<CardinalCurveAttributes> {
71
+ protected pieces(p: ArrayLike<number>): Piece[];
72
+ }
73
+ export interface CatmullRomCurveAttributes extends CurveAttributes {
74
+ /** The spacing of the knots: 0 even, 0.5 centripetal, 1 chordal. Default 0.5. */
75
+ alpha?: number;
76
+ }
77
+ /** The Catmull–Rom spline: through every point, centripetal by default so that it neither loops nor cusps when the spacing is uneven. */
78
+ export declare class CatmullRomCurve extends StrokeCurve<CatmullRomCurveAttributes> {
79
+ protected pieces(p: ArrayLike<number>): Piece[];
80
+ }
81
+ /** A uniform cubic B-spline, pulled towards the points rather than through them, starting at the first and ending at the last. */
82
+ export declare class BasisCurve extends StrokeCurve {
83
+ protected pieces(p: ArrayLike<number>): Piece[];
84
+ }
85
+ export interface BumpCurveAttributes extends CurveAttributes {
86
+ /** Whether the level ends are along x (a line level at each point) or y. Default: along the series' first axis. */
87
+ along?: 'x' | 'y';
88
+ }
89
+ /** A level start and end to each segment and an S between: the flow of a bump chart or a sankey. */
90
+ export declare class BumpCurve extends StrokeCurve<BumpCurveAttributes> {
91
+ protected pieces(p: ArrayLike<number>, series?: Series): Piece[];
92
+ }
93
+ /** Quadratic Béziers with each point as a control point, through the middles of the segments. */
94
+ export declare class QuadraticCurve extends StrokeCurve {
95
+ protected pieces(p: ArrayLike<number>): Piece[];
96
+ }
97
+ export interface ArcCurveAttributes extends CurveAttributes {
98
+ /** The rise of each arc as a fraction of its chord, positive bulging to the left of the way along. Default 0.3. */
99
+ bend?: number;
100
+ }
101
+ /** A circular arc between each pair of points. */
102
+ export declare class ArcCurve extends StrokeCurve<ArcCurveAttributes> {
103
+ protected pieces(p: ArrayLike<number>): Piece[];
104
+ }