inkplot 0.0.1-placeholder.0 → 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 +45 -1
  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 +48 -3
package/dist/series.js ADDED
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Series: a data pipeline, the axes it reads through, and the renderers that draw it.
3
+ *
4
+ * A series is two things. The base class is everything about getting points from a source and into plot pixels; a
5
+ * subclass says which axes the first and second coordinate ride on, and which columns of the data they come from.
6
+ *
7
+ * - `DomainSeries`: the domain goes along one axis and the value along another, as in a line chart, or round and out
8
+ * on a polar plane. Points are windowed to the visible domain.
9
+ * - `XYSeries`: two values, one on each axis, in the order the source holds them. The source's domain is only the
10
+ * order (by default the row's index), so a path can cross itself and a scatter needs none.
11
+ *
12
+ * Whichever it is, renderers are handed the same thing: `points`, laid out `[first, second, ...bound channels]` in plot
13
+ * pixels, so they never need to know.
14
+ *
15
+ * The life of a point, and who owns each step (so a change lands in the right place)
16
+ * ─────────────────────────────────────────────────────────────────────────────────
17
+ * data stage ──fill──▶ raw buffer ──gather columns──▶ points ──axis.normalize──▶ 0..1 ──plane.place──▶ pixels
18
+ * (source, Decimator) (this class; (this class; (this (Axis: scale, (Plane: Cartesian
19
+ * cached by key) `project`) class) inversion) rectangle, polar, radar)
20
+ * The raw buffer is filled once per change of data, window or width (`fill`) and is *not* touched by projection, so
21
+ * resizing the plot or panning a value axis only re-runs the cheap normalise-and-place tail (`project`). Subclasses
22
+ * choose which raw columns are the two coordinates; nothing after the gather step knows how they were chosen.
23
+ */
24
+ /* oxlint-disable typescript/no-non-null-assertion -- stride-addressed rows of the managed buffers: every index is bounded by the row count */
25
+ import { STRING } from './attrs.js';
26
+ import { Axis } from './axis.js';
27
+ import { DataSource, DataTransform } from './data.js';
28
+ import { ChartElement, fail, resolveRef } from './element.js';
29
+ import { writeAria } from './aria.js';
30
+ import { EVENT_ATTACH, EVENT_PAINT } from './events.js';
31
+ import { SeriesRenderer } from './renderer.js';
32
+ /** A plane child that holds series of its own (a stack), or null. Recognised by what it offers, so this file need not import every kind of group. */
33
+ export function groupOf(child) {
34
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- `members` was probed on the line above
35
+ return typeof child === 'object' && child !== null && 'members' in child ? child : null;
36
+ }
37
+ /** The series a plane child stands for: itself, the members of a group, or none. */
38
+ export function seriesOf(child) {
39
+ if (child instanceof Series)
40
+ return [child];
41
+ return groupOf(child)?.members ?? [];
42
+ }
43
+ /** What `points` is before the first projection, so renderers never need a null check. */
44
+ const NO_DRAWN_POINTS = { data: new Float64Array(0), stride: 2, count: 0 };
45
+ /** The contribution of a column the fill did not report (never happens for a valid source); NaN is ignored by axes. */
46
+ const NO_EXTENT = { min: NaN, max: NaN };
47
+ /** The shared pipeline: raw fill (cached), column gather, normalise, place. Subclasses say which columns and axes. */
48
+ export class Series extends ChartElement {
49
+ static attrTypes = { ...ChartElement.attrTypes, name: STRING };
50
+ // The projected points handed to renderers; grown as needed and reused so a redraw allocates nothing.
51
+ #points = null;
52
+ // The unprojected fill, with the key it was made for: data version, window, width and requested columns.
53
+ #raw = new Float64Array(0);
54
+ #rawCount = 0;
55
+ #extents = [];
56
+ #rowOf = null;
57
+ #key = '';
58
+ #stride = 2;
59
+ // For `baseline`: the plane of the last projection, a counter that says whether it moved, and the cached result.
60
+ #plane = null;
61
+ #projections = 0;
62
+ #base = { data: new Float64Array(0), stride: 2, count: 0 };
63
+ #baseKey = '';
64
+ // A stack's say over this series: the shift of its rows, and a table of rows lined up with the others' (else its own).
65
+ #layer = null;
66
+ #aligned = null;
67
+ /** Whether a missing value is drawn as 0 (not in a stack, which has its own say). */
68
+ get #zero() { return this.attrs.gaps === 'zero' && this.#layer === null; }
69
+ /** The plane this series is drawn on, which is its parent unless a stack sits between them. */
70
+ get plane() {
71
+ const p = this.requireParent();
72
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- `members` was probed in the condition: this is the series group's plane
73
+ return 'members' in p ? p.requireParent() : p;
74
+ }
75
+ /** The rows drawn from: a stack's lined-up table when it made one, else the series' own fill. */
76
+ rows() {
77
+ return this.#aligned ?? { raw: this.#raw, count: this.#rawCount, stride: this.#stride, rowOf: this.#rowOf };
78
+ }
79
+ /** Engine protocol for a `Stack`: the series' own fill as it stands (call after `prepare`). */
80
+ ownRows() { return { raw: this.#raw, count: this.#rawCount, stride: this.#stride, rowOf: this.#rowOf }; }
81
+ /** Engine protocol for a `Stack`: take a place in a stack, with rows lined up to its others'. Null for both leaves it. */
82
+ join(layer, rows) { this.#layer = layer; this.#aligned = rows; }
83
+ /** What a stack has made of this series, if it is in one. */
84
+ get layer() { return this.#layer; }
85
+ get data() { return this.child(0, DataSource, DataTransform); }
86
+ /**
87
+ * Every channel its renderers bind or ask for (`channelNames`): what a renderer can ask for by name. In `points` they follow the two coordinates
88
+ * in this order, so a channel's column is `2 + channels.indexOf(name)`.
89
+ */
90
+ get channels() {
91
+ const names = new Set();
92
+ for (const r of this)
93
+ if (r instanceof SeriesRenderer) {
94
+ for (const b of r.bindings)
95
+ names.add(b.channel);
96
+ for (const c of r.channelNames)
97
+ names.add(c);
98
+ }
99
+ return [...names];
100
+ }
101
+ /**
102
+ * The colour this series takes from the theme's palette: by its position among the plane's series, round again when
103
+ * there are more series than colours. It is only a default: a renderer's own colour attribute or any `style` beats it.
104
+ */
105
+ get accentColor() {
106
+ const palette = (this.chart ?? fail('series is not in a chart')).attrs.theme.palette;
107
+ const index = Math.max(0, this.plane.allSeries.indexOf(this));
108
+ return palette[index % palette.length] ?? '#888';
109
+ }
110
+ /** Whether the series is on show: `visible` is not false. One that is not takes no part: it is not drawn, does not set the axes' ranges, and is not hit. */
111
+ get shown() { return this.attrs.visible !== false; }
112
+ /**
113
+ * Draw a sample of what this series looks like into `parent`, `width` by `height` from its top left corner: each renderer's
114
+ * own sample (a line, a box, a marker), one over another in drawing order, in the look the renderer resolves from the
115
+ * theme and its attributes. A legend and a tooltip use it. Nodes are keyed `key/…` in `surface`.
116
+ */
117
+ sample(surface, parent, key, width, height) {
118
+ let n = 0;
119
+ for (const r of this)
120
+ if (r instanceof SeriesRenderer)
121
+ r.sample(surface, parent, `${key}/${n++}`, width, height);
122
+ }
123
+ /**
124
+ * The colour that stands for this series: what its first renderer says (its line, its fill, a marker's colour),
125
+ * else the palette colour. A key or tooltip shows it beside the name.
126
+ */
127
+ get color() {
128
+ for (const r of this)
129
+ if (r instanceof SeriesRenderer) {
130
+ const c = r.swatch;
131
+ if (c && c !== 'none')
132
+ return c;
133
+ }
134
+ return this.accentColor;
135
+ }
136
+ /** What the first renderer that has an opinion says a hit on this series should measure: bars say the shapes they draw. */
137
+ get hitHint() {
138
+ for (const r of this)
139
+ if (r instanceof SeriesRenderer && r.hitMode)
140
+ return r.hitMode;
141
+ return undefined;
142
+ }
143
+ /** The shapes the series draws, as polygons in plot pixels, if a renderer draws any (bars): what a 'shape' hit is inside. */
144
+ shapes() {
145
+ for (const r of this)
146
+ if (r instanceof SeriesRenderer) {
147
+ const g = r.shapes?.();
148
+ if (g)
149
+ return g;
150
+ }
151
+ return null;
152
+ }
153
+ /** The path the series' renderer draws, if it draws other than straight lines through the points (steps, curves): what a line hit or a reading follows. */
154
+ trace() {
155
+ for (const r of this)
156
+ if (r instanceof SeriesRenderer) {
157
+ const t = r.trace?.();
158
+ if (t)
159
+ return t;
160
+ }
161
+ return null;
162
+ }
163
+ /** How many times the series has been projected: a renderer that works something out from the points can tell whether they have changed. */
164
+ get projection() { return this.#projections; }
165
+ /** The last projected points, in plot pixels: `[first, second, ...channels]` in each row of `points.stride` numbers. */
166
+ get points() { return this.#points ?? NO_DRAWN_POINTS; }
167
+ onEvent(ev) {
168
+ switch (ev.type) {
169
+ case EVENT_ATTACH:
170
+ this.surface.wrapper(); // carries the clip
171
+ break;
172
+ case EVENT_PAINT: {
173
+ const clip = this.plane.clipUrl;
174
+ if (clip)
175
+ this.surface.set(this.surface.wrapper(), 'clip-path', clip);
176
+ writeAria(this.surface, this.surface.wrapper(), this.attrs.meta, { role: 'group' });
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ /**
182
+ * The last projection, as the series holds it, for what reads a drawn series back: hit testing (hit.ts), which is
183
+ * kept out of the series so that a chart that never hit-tests does not carry it. Null before the first projection.
184
+ */
185
+ get drawn() {
186
+ const plane = this.#plane, points = this.#points;
187
+ if (!plane || !points)
188
+ return null;
189
+ const rows = this.rows(), layer = this.#layer;
190
+ return {
191
+ plane, points, raw: rows.raw, stride: rows.stride, firstColumn: this.firstColumn, secondColumn: this.secondColumn,
192
+ channels: this.channels, rowOf: rows.rowOf, alongDomain: this.alongDomain,
193
+ base: layer?.base ?? null, top: layer?.top ?? null, slot: layer?.slot ?? { index: 0, count: 1 }, additive: layer?.additive ?? true,
194
+ };
195
+ }
196
+ /** Whether the first coordinate is a position along the line, so that a value can be read between two points. */
197
+ get alongDomain() { return false; }
198
+ // ─── Engine protocol: the plane calls these, in this order, each pass. ───
199
+ /**
200
+ * Fill the raw buffer, or reuse it when nothing it depends on has changed. Returns every column's extent: the
201
+ * domain, the value, then each requested channel.
202
+ */
203
+ fill(ctx) {
204
+ const data = this.data, counters = this.counters;
205
+ const stride = 2 + ctx.channels.length;
206
+ const key = `${data.version}|${ctx.domainRange.min}|${ctx.domainRange.max}|${ctx.width}|${ctx.channels.join()}`;
207
+ if (key === this.#key) {
208
+ counters.cached++;
209
+ }
210
+ else {
211
+ const t0 = performance.now(), d0 = counters.stages.decimate;
212
+ const plan = data.plan(ctx);
213
+ if (this.#raw.length < plan.capacity * stride)
214
+ this.#raw = new Float64Array(plan.capacity * stride);
215
+ const result = plan.fill(this.#raw);
216
+ this.#rawCount = result.count;
217
+ this.#rowOf = result.rowOf ?? null;
218
+ this.#extents = result.extents;
219
+ this.#key = key;
220
+ this.#stride = stride;
221
+ counters.filled++;
222
+ counters.pointsIn += data.pointCount;
223
+ counters.stages.generate += performance.now() - t0 - (counters.stages.decimate - d0);
224
+ }
225
+ return this.#extents;
226
+ }
227
+ /**
228
+ * Ask both axes for the room the renderers draw beyond the places of the points (a marker's radius), before the ranges
229
+ * are settled. It is pixels, which an axis turns into data by its length.
230
+ */
231
+ contributeReach() {
232
+ let px = 0;
233
+ for (const r of this)
234
+ if (r instanceof SeriesRenderer && r.reach > px)
235
+ px = r.reach;
236
+ if (px > 0) {
237
+ this.firstAxis.reach(px);
238
+ this.secondAxis.reach(px);
239
+ }
240
+ }
241
+ /** The largest `spans` of the renderers: how far they draw beyond a point along the domain, as a fraction of the gap between points. */
242
+ get spans() {
243
+ let m = 0;
244
+ for (const r of this)
245
+ if (r instanceof SeriesRenderer && r.spans > m)
246
+ m = r.spans;
247
+ return m;
248
+ }
249
+ /**
250
+ * Widen the range of every mapped dimension a renderer binds to cover the whole of its channel. This is the whole
251
+ * of the data and not the part in view, so colours and sizes stay put while you zoom.
252
+ */
253
+ contributeChannels() {
254
+ if (this.#zero) {
255
+ const { raw, count: n, stride: rs } = this.rows(), c = this.secondColumn;
256
+ for (let i = 0; i < n; i++)
257
+ if (!Number.isFinite(raw[i * rs + c])) {
258
+ this.secondAxis.contribute(0, 0);
259
+ break;
260
+ }
261
+ }
262
+ // Values a renderer needs on the value axis whatever the data says: the baseline bars grow from.
263
+ for (const r of this)
264
+ if (r instanceof SeriesRenderer)
265
+ for (const v of r.includes)
266
+ this.secondAxis.contribute(v, v);
267
+ // What renderers draw beyond the values (error bars) is part of what the axes must show.
268
+ const view = this.spreadView();
269
+ for (const r of this) {
270
+ if (!(r instanceof SeriesRenderer) || !r.spread)
271
+ continue;
272
+ const s = r.spread(view);
273
+ if (s?.first)
274
+ this.firstAxis.contribute(s.first.min, s.first.max);
275
+ if (s?.second)
276
+ this.secondAxis.contribute(s.second.min, s.second.max);
277
+ }
278
+ for (const r of this) {
279
+ if (!(r instanceof SeriesRenderer))
280
+ continue;
281
+ for (const b of r.bindings) {
282
+ const e = this.data.channelExtent(b.channel);
283
+ if (e)
284
+ b.axis.contribute(e.min, e.max);
285
+ }
286
+ }
287
+ }
288
+ /** The rows as a renderer's `spread` sees them. */
289
+ spreadView() {
290
+ const { raw, count, stride } = this.rows(), channels = this.channels, layer = this.#layer;
291
+ const first = channels.length === 0 ? stride : stride - channels.length;
292
+ return {
293
+ raw, count, stride, firstColumn: this.firstColumn, secondColumn: this.secondColumn, top: layer?.top ?? null,
294
+ channelColumn: (name) => { const k = channels.indexOf(name); return k < 0 ? -1 : first + k; },
295
+ };
296
+ }
297
+ /** An axis' natural extent: the data's, widened by what the renderers draw beyond it. Zooming is held to this. */
298
+ widened(axis, base) {
299
+ let e = base;
300
+ if (this.#layer)
301
+ return e;
302
+ for (const r of this) {
303
+ if (!(r instanceof SeriesRenderer) || !r.limit)
304
+ continue;
305
+ const s = r.limit(this), w = axis === this.firstAxis ? s?.first : axis === this.secondAxis ? s?.second : undefined;
306
+ if (w)
307
+ e = e ? { min: Math.min(e.min, w.min), max: Math.max(e.max, w.max) } : w;
308
+ }
309
+ return e;
310
+ }
311
+ /**
312
+ * Normalize and place into plot pixels. The raw buffer is left alone, so a re-projection needs no refill.
313
+ * The result is `[first, second, ...bound channels]` whatever order the data held them in.
314
+ */
315
+ project(plane) {
316
+ const counters = this.counters, first = this.firstAxis, second = this.secondAxis;
317
+ const { raw, count: n, stride: rs } = this.rows(), layer = this.#layer;
318
+ const c0 = this.firstColumn, c1 = this.secondColumn, bound = this.channels.length, stride = 2 + bound;
319
+ if (!this.#points || this.#points.data.length < n * stride)
320
+ this.#points = { data: new Float64Array(n * stride), stride, count: 0 };
321
+ const p = this.#points;
322
+ p.stride = stride;
323
+ p.count = n;
324
+ if (c0 === 0 && c1 === 1 && rs === stride) {
325
+ p.data.set(raw.subarray(0, n * stride));
326
+ }
327
+ else {
328
+ // The coordinates are not the first two columns: gather them, then the bound channels, which come last.
329
+ const channels = rs - bound;
330
+ for (let i = 0; i < n; i++) {
331
+ p.data[i * stride] = raw[i * rs + c0];
332
+ p.data[i * stride + 1] = raw[i * rs + c1];
333
+ for (let k = 0; k < bound; k++)
334
+ p.data[i * stride + 2 + k] = raw[i * rs + channels + k];
335
+ }
336
+ }
337
+ if (this.#zero)
338
+ for (let i = 0; i < n; i++) {
339
+ const v = p.data[i * stride + 1];
340
+ if (!Number.isFinite(v) && Number.isFinite(p.data[i * stride]))
341
+ p.data[i * stride + 1] = 0;
342
+ }
343
+ // In a stack a point is drawn where the stack puts it: the row's own value has already served its purpose.
344
+ if (layer)
345
+ for (let i = 0; i < n; i++) {
346
+ const v = p.data[i * stride + 1];
347
+ p.data[i * stride + 1] = Number.isFinite(v) ? layer.top[i] : NaN;
348
+ }
349
+ let t = performance.now();
350
+ first.normalize(p.data, n, stride, 0);
351
+ second.normalize(p.data, n, stride, 1);
352
+ counters.stages.normalize += performance.now() - t;
353
+ t = performance.now();
354
+ plane.place(p, first, second);
355
+ counters.stages.place += performance.now() - t;
356
+ counters.projected++;
357
+ counters.pointsDrawn += n;
358
+ this.#plane = plane;
359
+ this.#projections++;
360
+ }
361
+ /**
362
+ * The line the series is measured from, as placed points: one for each drawn point, in the same place along the first
363
+ * coordinate, with the second held at `value` (kept inside its axis' range). Gaps stay gaps. The plane places it
364
+ * exactly as it places the data, so a renderer that fills between the two needs to know nothing about the plane:
365
+ * the data forward and this backward is a closed shape on a rectangle and on a circle alike.
366
+ */
367
+ baseline(value) {
368
+ const plane = this.#plane ?? fail('series has not been projected'), { raw, count: n, stride: rs } = this.rows(), layer = this.#layer;
369
+ const key = `${this.#projections}|${value}`;
370
+ if (key === this.#baseKey)
371
+ return this.#base;
372
+ this.#baseKey = key;
373
+ if (this.#base.data.length < n * 2)
374
+ this.#base.data = new Float64Array(n * 2);
375
+ const b = this.#base, c0 = this.firstColumn, c1 = this.secondColumn;
376
+ b.count = n;
377
+ for (let i = 0; i < n; i++) {
378
+ b.data[2 * i] = raw[i * rs + c0];
379
+ // A gap in the data stays a gap in the baseline.
380
+ // In a stack the baseline is the layer below, row by row, and `value` is not asked.
381
+ b.data[2 * i + 1] = Number.isFinite(raw[i * rs + c1]) || (this.#zero && Number.isFinite(raw[i * rs + c0])) ? (layer ? layer.base[i] : value) : NaN;
382
+ }
383
+ this.firstAxis.normalize(b.data, n, 2, 0);
384
+ this.secondAxis.normalize(b.data, n, 2, 1);
385
+ // Keep the baseline inside its axis, so a fill to a value outside the range closes along the edge.
386
+ for (let i = 1; i < 2 * n; i += 2) {
387
+ const v = b.data[i];
388
+ if (v < 0)
389
+ b.data[i] = 0;
390
+ else if (v > 1)
391
+ b.data[i] = 1;
392
+ }
393
+ plane.place(b, this.firstAxis, this.secondAxis);
394
+ return b;
395
+ }
396
+ }
397
+ /**
398
+ * A value at every position along a domain, drawn against two axes. The source delivers ascending in the domain and only
399
+ * the visible part is read, so a view that zooms in refills from the full data, and the value axis fits what is in view.
400
+ */
401
+ export class DomainSeries extends Series {
402
+ static attrTypes = { ...Series.attrTypes, domainAxis: STRING, valueAxis: STRING };
403
+ #domainAxis = null;
404
+ #valueAxis = null;
405
+ get domainAxis() { return this.#domainAxis ?? fail('series has not been laid out'); }
406
+ get valueAxis() { return this.#valueAxis ?? fail('series has not been laid out'); }
407
+ get firstAxis() { return this.domainAxis; }
408
+ get secondAxis() { return this.valueAxis; }
409
+ get firstColumn() { return 0; }
410
+ get secondColumn() { return 1; }
411
+ get alongDomain() { return true; }
412
+ resolveAxes(plane) {
413
+ const a = this.attrs;
414
+ this.#domainAxis = resolveRef(a.domainAxis, this, Axis) ?? plane.defaultAxis('first') ?? fail('plane has no axis for the domain');
415
+ this.#valueAxis = resolveRef(a.valueAxis, this, Axis) ?? plane.defaultAxis('second') ?? fail('plane has no axis for the value');
416
+ }
417
+ contributeDomain() {
418
+ const d = this.data.domain(), n = this.data.pointCount, spans = this.spans;
419
+ if (d) {
420
+ // Bars centred on the first and last points reach half a bar past them: the mean gap is what is known before reading.
421
+ const room = spans > 0 && n > 1 && !('bandwidth' in this.domainAxis.scale) ? (spans * (d.max - d.min)) / (n - 1) : 0;
422
+ this.domainAxis.contribute(d.min - room, d.max + room);
423
+ }
424
+ this.contributeReach();
425
+ }
426
+ resolveDomain() { this.domainAxis.resolve(); }
427
+ load(estimate) {
428
+ const ext = this.fill({ ...estimate, domainRange: this.domainAxis.range, channels: this.channels })[1] ?? NO_EXTENT;
429
+ this.valueAxis.contribute(ext.min, ext.max);
430
+ this.contributeChannels();
431
+ }
432
+ /** Engine protocol for a `Stack`: read the data (the first half of `load`) and give the rows, leaving the extents to the stack. */
433
+ prepare(estimate) {
434
+ this.fill({ ...estimate, domainRange: this.domainAxis.range, channels: this.channels });
435
+ return this.ownRows();
436
+ }
437
+ /** Engine protocol for a `Stack`: the rest of `load`, with the value extent the stack worked out for its stacked values. */
438
+ settle(extent) {
439
+ if (extent)
440
+ this.valueAxis.contribute(extent.min, extent.max);
441
+ this.contributeChannels();
442
+ }
443
+ ownExtent(role) { return role === 'first' ? this.data.domain() : this.data.valueExtent(); }
444
+ extentFor(axis) {
445
+ if (axis === this.valueAxis && this.layer)
446
+ return this.layer.extent;
447
+ return axis === this.domainAxis ? this.widened(axis, this.data.domain()) : axis === this.valueAxis ? this.widened(axis, this.data.valueExtent()) : null;
448
+ }
449
+ }
450
+ /**
451
+ * Two values per point, one on each axis, joined in the order the source holds them: a scatter, a trajectory, a loop.
452
+ *
453
+ * The source's domain is only the sequence (leave `domain` out and it is the row's index), and its `value` is y. x
454
+ * is a channel, named by the `x` attribute. Because order is all the domain means:
455
+ * - every point is always read, whatever the view: there is nothing to window by;
456
+ * - a `Decimator` refuses it, since kernels thin by pixel column of the domain; draw it undecimated (markers already
457
+ * leave out what is off the plot);
458
+ * - an axis fits all the data, and zooming one axis does not refit the other to the points in view: with no order
459
+ * along x, finding those would be a scan of every point on every change.
460
+ */
461
+ export class XYSeries extends Series {
462
+ static attrTypes = { ...Series.attrTypes, xAxis: STRING, yAxis: STRING };
463
+ #xAxis = null;
464
+ #yAxis = null;
465
+ get xAxis() { return this.#xAxis ?? fail('series has not been laid out'); }
466
+ get yAxis() { return this.#yAxis ?? fail('series has not been laid out'); }
467
+ get firstAxis() { return this.xAxis; }
468
+ get secondAxis() { return this.yAxis; }
469
+ // The data is [order, y, x, ...bound channels]: x is asked for ahead of the channels the renderers bind.
470
+ get firstColumn() { return 2; }
471
+ get secondColumn() { return 1; }
472
+ resolveAxes(plane) {
473
+ const a = this.attrs;
474
+ this.#xAxis = resolveRef(a.xAxis, this, Axis) ?? plane.defaultAxis('first') ?? fail('plane has no axis for x');
475
+ this.#yAxis = resolveRef(a.yAxis, this, Axis) ?? plane.defaultAxis('second') ?? fail('plane has no axis for y');
476
+ }
477
+ // An order has no extent to agree on before reading, so there is nothing to do ahead of `load`.
478
+ contributeDomain() { this.contributeReach(); }
479
+ resolveDomain() { }
480
+ load(estimate) {
481
+ const ext = this.fill({
482
+ ...estimate, domainRange: { min: -Infinity, max: Infinity }, sequence: true, channels: [this.attrs.x, ...this.channels],
483
+ });
484
+ const x = ext[2] ?? NO_EXTENT, y = ext[1] ?? NO_EXTENT;
485
+ this.xAxis.contribute(x.min, x.max);
486
+ this.yAxis.contribute(y.min, y.max);
487
+ this.contributeChannels();
488
+ }
489
+ ownExtent(role) { return role === 'first' ? this.data.channelExtent(this.attrs.x) : this.data.valueExtent(); }
490
+ extentFor(axis) {
491
+ return axis === this.xAxis ? this.widened(axis, this.data.channelExtent(this.attrs.x)) : axis === this.yAxis ? this.widened(axis, this.data.valueExtent()) : null;
492
+ }
493
+ }
494
+ /**
495
+ * A sample of `series` in a group of `surface` keyed `key`, set at (x, y), `width` by `height`. A group that last held another
496
+ * series (a tooltip line that now reads another one) is made afresh, so nothing of the old one is left; `cache` is what the
497
+ * caller keeps between calls. Returns the group, for what the caller wants to do to it (dim it).
498
+ */
499
+ export function paintSample(surface, key, series, x, y, width, height, cache) {
500
+ let renderers = 0;
501
+ for (const r of series)
502
+ if (r instanceof SeriesRenderer)
503
+ renderers++;
504
+ const last = cache.get(key);
505
+ if (!last || last.series !== series || last.renderers !== renderers) {
506
+ surface.removePrefix(key);
507
+ cache.set(key, { series, renderers });
508
+ }
509
+ const group = surface.node(key, 'g');
510
+ surface.set(group, 'transform', `translate(${Math.round(x * 100) / 100} ${Math.round(y * 100) / 100})`);
511
+ series.sample(surface, group, key, width, height);
512
+ return group;
513
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shortcuts for the common cases. These only compose the public classes (a series, a renderer, optionally a decimator and a
3
+ * marker), so everything they do can be done, and tuned, with the elements themselves.
4
+ */
5
+ import { type Curve } from './curve.ts';
6
+ import type { Axis } from './axis.ts';
7
+ import type { DataNode } from './data.ts';
8
+ import { type DecimationKernel } from './decimation.ts';
9
+ import type { ElementRef } from './element.ts';
10
+ import { type SeriesRenderer } from './renderer.ts';
11
+ import { DomainSeries } from './series.ts';
12
+ /**
13
+ * The handful of choices a quick line chart needs. A marker and a kernel are given as the classes themselves
14
+ * (`marker: CircleMarker`, `decimate: M4Kernel`), not as names, so that this module only refers to what you pass it: a
15
+ * bundle that never decimates carries no kernel, and one that draws no markers carries no marker. For anything the
16
+ * options do not cover, such as a marker's size or a kernel's settings, build the elements directly.
17
+ */
18
+ export interface LineSeriesOptions {
19
+ stroke?: string;
20
+ strokeWidth?: number;
21
+ domainAxis?: ElementRef<Axis>;
22
+ valueAxis?: ElementRef<Axis>;
23
+ /** Draw a marker of this class at every point: `CircleMarker`. */
24
+ marker?: new (attrs: {
25
+ stroke?: string;
26
+ }) => SeriesRenderer;
27
+ /** The markers' stroke. Default the line's `stroke`, else the series' palette colour. */
28
+ markerStroke?: string;
29
+ /** Wrap the source in a decimator with a kernel of this class: `MinMaxKernel`, `M4Kernel`. */
30
+ decimate?: new () => DecimationKernel;
31
+ /** How the points are joined: a curve of any kind (`new MonotoneCurve()`, `new StepCurve({ mode: 'after' })`). Default straight lines. */
32
+ curve?: Curve;
33
+ }
34
+ /** A series with a line renderer: the common case in one call. */
35
+ export declare function lineSeries(source: DataNode, options?: LineSeriesOptions): DomainSeries;
package/dist/simple.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shortcuts for the common cases. These only compose the public classes (a series, a renderer, optionally a decimator and a
3
+ * marker), so everything they do can be done, and tuned, with the elements themselves.
4
+ */
5
+ import { LinearCurve } from './curve.js';
6
+ import { Decimator } from './decimation.js';
7
+ import { LinePath } from './renderer.js';
8
+ import { DomainSeries } from './series.js';
9
+ /** A series with a line renderer: the common case in one call. */
10
+ export function lineSeries(source, options = {}) {
11
+ const { stroke, strokeWidth, domainAxis, valueAxis, marker, markerStroke, decimate, curve } = options;
12
+ const line = new LinePath({ ...(stroke !== undefined && { stroke }), ...(strokeWidth !== undefined && { strokeWidth }) }, curve ?? new LinearCurve());
13
+ const attrs = {
14
+ ...(domainAxis !== undefined && { domainAxis }),
15
+ ...(valueAxis !== undefined && { valueAxis }),
16
+ };
17
+ const data = decimate ? new Decimator({}, new decimate(), source) : source;
18
+ const mark = markerStroke ?? stroke;
19
+ return marker ? new DomainSeries(attrs, data, line, new marker(mark === undefined ? {} : { stroke: mark })) : new DomainSeries(attrs, data, line);
20
+ }