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
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Elements to and from plain JSON. Attributes are already plain data and children are ordered, so a tree maps directly to
3
+ * `{ type, attrs, children }`, which is what lets adapters, saved configs and an editor share one format.
4
+ *
5
+ * const json = toJSON(chart, registry); // JSON.stringify-able
6
+ * const copy = fromJSON(json, registry); // a new, unattached tree
7
+ * applyJSON(chart, editedJson, registry); // the same tree, changed to match
8
+ *
9
+ * The registry
10
+ * ────────────
11
+ * `type` is the name a class is registered under (`registry.ts` has all of the library's). An element is written under the
12
+ * name its exact class has, so a class of your own is one more entry: `{ ...registry, MyScale }`. Naming a class that is
13
+ * not in the registry is an error that says so, with the place in the tree.
14
+ *
15
+ * What an attribute may hold
16
+ * ──────────────────────────
17
+ * - Plain data goes through as it is: strings, booleans, finite numbers, arrays, plain objects (themes, `style`, bindings).
18
+ * - A number that is not finite (`NaN`, `Infinity`) is `{ "$number": "NaN" }`, since JSON has no such thing.
19
+ * - A typed array is `{ "$typed": "Float64Array", "data": [...] }` with `null` for NaN, so a gap in the data survives.
20
+ * - An element is its `id` (which is what every attribute that takes an element accepts), so it must have one.
21
+ * - Anything else JSON cannot hold, which is chiefly code (a `format` function, an `onClick`) and big or live data (an
22
+ * array of rows you go on editing), is `{ "$ref": "name" }`, and `name` is looked up in the `Refs` you give when reading.
23
+ *
24
+ * Refs
25
+ * ────
26
+ * A `Refs` is a table of names for objects and functions. Writing looks values up in it by identity, so a theme you named
27
+ * `lightTheme` is written as `{ "$ref": "lightTheme" }` rather than spelled out. With `auto: true` it names what it is
28
+ * asked about and does not know (`onFlush`, `format2`…), so any chart can be written; without it, an unnamed function is an
29
+ * error that tells you what to name. Arrays longer than `inlineLimit` (default 1000) count as data too and go by reference:
30
+ * that is the line between a chart's configuration and its data. Reading needs the same table.
31
+ *
32
+ * `applyJSON` changes a live tree to match edited JSON, in place: an attribute that already reads the same is left alone
33
+ * (so a ref'd array keeps its identity, and a decimator its cache), one that differs is set, one that is gone is removed, and
34
+ * children are matched by position and type, replaced if the type changed. It works out everything first and changes
35
+ * nothing if the JSON turns out to be unreadable, so a bad edit leaves the chart as it was.
36
+ */
37
+ import { ChartElement, fail } from './element.js';
38
+ /** Maps `type` names to classes, and back: an element is written under the name its class is registered by. */
39
+ /** The text of anything thrown: a constructor or a refs lookup may throw a value that is not an Error. */
40
+ const messageOf = (e) => e instanceof Error ? e.message : String(e);
41
+ /** Names for objects and functions that JSON cannot hold, so that writing can point at them and reading can find them again. */
42
+ export class Refs {
43
+ #byName = new Map();
44
+ #byValue = new Map();
45
+ /** Name whatever is asked about and not yet named, after the attribute it was found in. */
46
+ auto;
47
+ constructor(values = {}, options = {}) {
48
+ this.auto = options.auto ?? false;
49
+ for (const [name, value] of Object.entries(values))
50
+ this.set(name, value);
51
+ }
52
+ /** Name a value. The name must be free; a value that already has a name keeps it first and answers to both on reading. */
53
+ set(name, value) {
54
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
55
+ fail(`ref "${name}": only objects and functions can be named`);
56
+ if (this.#byName.has(name) && this.#byName.get(name) !== value)
57
+ fail(`ref "${name}" already names something else`);
58
+ this.#byName.set(name, value);
59
+ if (!this.#byValue.has(value))
60
+ this.#byValue.set(value, name);
61
+ return this;
62
+ }
63
+ has(name) { return this.#byName.has(name); }
64
+ get(name) {
65
+ if (!this.#byName.has(name))
66
+ fail(`no ref named "${name}"` + (this.#byName.size ? ` (known: ${[...this.#byName.keys()].slice(0, 12).join(', ')})` : ' (there are none)'));
67
+ return this.#byName.get(name);
68
+ }
69
+ /** The name a value goes by, if it has one. */
70
+ nameOf(value) { return this.#byValue.get(value); }
71
+ /** Give a value a name of its own, built on `hint` and made unique. Returns the one it has already, if so. */
72
+ adopt(value, hint = 'ref') {
73
+ const known = this.nameOf(value);
74
+ if (known !== undefined)
75
+ return known;
76
+ let name = hint, n = 1;
77
+ while (this.#byName.has(name))
78
+ name = hint + ++n;
79
+ this.set(name, value);
80
+ return name;
81
+ }
82
+ get names() { return [...this.#byName.keys()]; }
83
+ }
84
+ // ─── Names ───
85
+ const names = new WeakMap();
86
+ function nameOf(ctor, registry, path) {
87
+ let map = names.get(registry);
88
+ if (!map) {
89
+ map = new Map();
90
+ for (const [name, c] of Object.entries(registry))
91
+ if (!map.has(c))
92
+ map.set(c, name);
93
+ names.set(registry, map);
94
+ }
95
+ const name = map.get(ctor);
96
+ if (name === undefined)
97
+ fail(`${at(path)}${ctor.name || 'an anonymous class'} is not in the registry: add it, as in { ...registry, ${ctor.name || 'Mine'} }`);
98
+ return name;
99
+ }
100
+ const at = (path) => (path ? `${path}: ` : '');
101
+ // ─── Writing ───
102
+ const TYPED = ['Float64Array', 'Float32Array', 'Int8Array', 'Int16Array', 'Int32Array', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray'];
103
+ /** What a value is called when it is the attribute `path` names, for a ref that has to be made up. */
104
+ const hintOf = (path) => /([A-Za-z_]\w*)(?:\[\d+\])*$/.exec(path)?.[1] ?? 'ref';
105
+ function byReference(value, path, w, what) {
106
+ if (w.refs?.auto)
107
+ return { $ref: w.refs.adopt(value, hintOf(path)) };
108
+ fail(`${at(path)}${what}, which JSON cannot hold: name it in \`refs\` (a Refs with \`auto: true\` names whatever it meets)`);
109
+ }
110
+ function encode(v, path, w) {
111
+ if (v !== null && (typeof v === 'object' || typeof v === 'function')) {
112
+ const named = w.refs?.nameOf(v);
113
+ if (named !== undefined)
114
+ return { $ref: named };
115
+ }
116
+ switch (typeof v) {
117
+ case 'string':
118
+ case 'boolean': return v;
119
+ case 'number': return Number.isFinite(v) ? v : { $number: String(v) };
120
+ case 'undefined': return null;
121
+ case 'function': return byReference(v, path, w, 'a function');
122
+ case 'bigint':
123
+ case 'symbol': return fail(`${at(path)}a ${typeof v} cannot be written as JSON`);
124
+ }
125
+ if (v === null)
126
+ return null;
127
+ if (v instanceof ChartElement) {
128
+ const id = v.attrs.id;
129
+ if (typeof id !== 'string')
130
+ fail(`${at(path)}refers to a ${v.constructor.name} that has no id: give it one so JSON can name it`);
131
+ return id;
132
+ }
133
+ if (ArrayBuffer.isView(v)) {
134
+ const type = v.constructor.name, a = v;
135
+ if (!TYPED.includes(type))
136
+ return byReference(v, path, w, `a ${type}`);
137
+ if (a.length > w.limit)
138
+ return byReference(v, path, w, `${type} of ${a.length} numbers (the inline limit is ${w.limit})`);
139
+ return { $typed: type, data: Array.from(a, (n) => (Number.isFinite(n) ? n : n !== n ? null : String(n))) };
140
+ }
141
+ if (Array.isArray(v)) {
142
+ if (v.length > w.limit)
143
+ return byReference(v, path, w, `an array of ${v.length} values (the inline limit is ${w.limit})`);
144
+ return v.map((x, i) => encode(x, `${path}[${i}]`, w));
145
+ }
146
+ const object = v, proto = Object.getPrototypeOf(object);
147
+ if (proto === Object.prototype || proto === null) {
148
+ const out = {};
149
+ for (const key of Object.keys(object)) {
150
+ const x = object[key];
151
+ if (x === undefined)
152
+ continue;
153
+ if (key.startsWith('$'))
154
+ fail(`${at(path)}the key "${key}" is reserved: JSON marks references and special values with a leading $`);
155
+ out[key] = encode(x, path ? `${path}.${key}` : key, w);
156
+ }
157
+ return out;
158
+ }
159
+ return byReference(v, path, w, `a ${v.constructor?.name ?? 'object'}`);
160
+ }
161
+ function write(element, path, w) {
162
+ const json = { type: nameOf(element.constructor, w.registry, path) };
163
+ const attrs = {};
164
+ for (const key of Object.keys(element.attrs)) {
165
+ const value = element.attrs[key];
166
+ if (value !== undefined)
167
+ attrs[key] = encode(value, `${path ? path + '.' : ''}attrs.${key}`, w);
168
+ }
169
+ if (Object.keys(attrs).length)
170
+ json.attrs = attrs;
171
+ const children = [...element].map((c, i) => write(c, `${path ? path + '.' : ''}children[${i}]`, w));
172
+ if (children.length)
173
+ json.children = children;
174
+ return json;
175
+ }
176
+ /** An element and everything below it as JSON. See the top of this file for what an attribute may hold. */
177
+ export function toJSON(element, registry, options = {}) {
178
+ return write(element, '', { registry, refs: options.refs, limit: options.inlineLimit ?? 1000 });
179
+ }
180
+ // ─── Reading ───
181
+ function decode(v, path, refs) {
182
+ if (Array.isArray(v))
183
+ return v.map((x, i) => decode(x, `${path}[${i}]`, refs));
184
+ if (v === null || typeof v !== 'object')
185
+ return v;
186
+ const o = v;
187
+ if ('$ref' in o) {
188
+ if (typeof o.$ref !== 'string')
189
+ fail(`${at(path)}a $ref has to be the name of a ref`);
190
+ if (!refs)
191
+ fail(`${at(path)}refers to "${o.$ref}", and no refs were given to look it up in`);
192
+ try {
193
+ return refs.get(o.$ref);
194
+ }
195
+ catch (e) {
196
+ return fail(`${at(path)}${messageOf(e)}`);
197
+ }
198
+ }
199
+ if ('$number' in o) {
200
+ const n = { NaN, Infinity, '-Infinity': -Infinity }[String(o.$number)];
201
+ if (n === undefined)
202
+ fail(`${at(path)}$number has to be "NaN", "Infinity" or "-Infinity"`);
203
+ return n;
204
+ }
205
+ if ('$typed' in o) {
206
+ const type = String(o.$typed);
207
+ if (!TYPED.includes(type) || !Array.isArray(o.data))
208
+ fail(`${at(path)}a $typed value is { "$typed": one of ${TYPED.join(', ')}, "data": [numbers] }`);
209
+ // oxlint-disable-next-line typescript/no-non-null-assertion -- in range: the index comes from a loop bound or a length checked in the same function
210
+ const Ctor = globalThis[type];
211
+ return new Ctor(o.data.map((n) => (n === null ? NaN : n === 'Infinity' ? Infinity : n === '-Infinity' ? -Infinity : n)));
212
+ }
213
+ const out = {};
214
+ for (const key of Object.keys(o)) {
215
+ if (key.startsWith('$'))
216
+ fail(`${at(path)}unknown marker "${key}"`);
217
+ out[key] = decode(o[key], path ? `${path}.${key}` : key, refs);
218
+ }
219
+ return out;
220
+ }
221
+ function lookup(type, registry, path) {
222
+ if (typeof type !== 'string')
223
+ fail(`${at(path)}every element needs a "type", the name of a class`);
224
+ if (!Object.hasOwn(registry, type)) {
225
+ const near = Object.keys(registry).filter((n) => n.toLowerCase().includes(type.toLowerCase()) || type.toLowerCase().includes(n.toLowerCase()));
226
+ fail(`${at(path)}unknown type "${type}"` + (near.length ? ` (did you mean ${near.slice(0, 4).join(', ')}?)` : ` (the registry has ${Object.keys(registry).length} types)`));
227
+ }
228
+ // oxlint-disable-next-line typescript/no-non-null-assertion -- in range: the index comes from a loop bound or a length checked in the same function
229
+ return registry[type];
230
+ }
231
+ function build(json, registry, refs, path) {
232
+ if (json === null || typeof json !== 'object')
233
+ fail(`${at(path)}an element is an object { type, attrs?, children? }`);
234
+ const Ctor = lookup(json.type, registry, path);
235
+ const attrs = json.attrs === undefined ? {} : decode(json.attrs, `${path ? path + '.' : ''}attrs`, refs);
236
+ if (attrs === null || typeof attrs !== 'object' || Array.isArray(attrs))
237
+ fail(`${at(path)}"attrs" has to be an object`);
238
+ if (json.children !== undefined && !Array.isArray(json.children))
239
+ fail(`${at(path)}"children" has to be an array`);
240
+ const children = (json.children ?? []).map((c, i) => build(c, registry, refs, `${path ? path + '.' : ''}children[${i}]`));
241
+ try {
242
+ return new Ctor(attrs, ...children);
243
+ }
244
+ catch (e) {
245
+ return fail(`${at(path)}${json.type}: ${messageOf(e)}`);
246
+ }
247
+ }
248
+ /** A new, unattached element tree from JSON. `refs` is where the `$ref`s are found. */
249
+ export function fromJSON(json, registry, options = {}) {
250
+ return build(json, registry, options.refs, '');
251
+ }
252
+ // ─── Changing a live tree ───
253
+ /** Deep equality of two JSON values, however their keys are ordered. */
254
+ function same(a, b) {
255
+ if (a === b)
256
+ return true;
257
+ if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object')
258
+ return false;
259
+ if (Array.isArray(a) !== Array.isArray(b))
260
+ return false;
261
+ const ka = Object.keys(a), kb = Object.keys(b);
262
+ if (ka.length !== kb.length)
263
+ return false;
264
+ return ka.every((k) => Object.hasOwn(b, k) && same(a[k], b[k]));
265
+ }
266
+ function plan(el, json, path, w, ops) {
267
+ const here = path ? path + '.' : '';
268
+ const want = json.attrs === undefined ? {} : decode(json.attrs, `${here}attrs`, w.refs);
269
+ const wantJson = json.attrs ?? {}, have = el.attrs, patch = {};
270
+ let changed = false;
271
+ for (const key of Object.keys(wantJson)) {
272
+ // Compare in JSON form, so an inline value that is unchanged is not replaced by a fresh copy of itself.
273
+ const now = have[key] === undefined ? undefined : encode(have[key], `${here}attrs.${key}`, w);
274
+ if (now !== undefined && same(now, wantJson[key]))
275
+ continue;
276
+ patch[key] = want[key];
277
+ changed = true;
278
+ }
279
+ for (const key of Object.keys(have))
280
+ if (have[key] !== undefined && !Object.hasOwn(wantJson, key)) {
281
+ patch[key] = undefined;
282
+ changed = true;
283
+ }
284
+ if (changed)
285
+ ops.push(() => el.setAttributes(patch));
286
+ const kids = [...el], wanted = json.children ?? [];
287
+ if (!Array.isArray(wanted))
288
+ fail(`${at(path)}"children" has to be an array`);
289
+ wanted.forEach((child, i) => {
290
+ const at = `${here}children[${i}]`, existing = kids[i];
291
+ if (existing && typeof child === 'object' && child && nameOf(existing.constructor, w.registry, at) === child.type) {
292
+ plan(existing, child, at, w, ops);
293
+ return;
294
+ }
295
+ const fresh = build(child, w.registry, w.refs, at);
296
+ if (existing)
297
+ ops.push(() => { el.insertBefore(fresh, existing); el.remove(existing); });
298
+ else
299
+ ops.push(() => el.insertBefore(fresh, null));
300
+ });
301
+ // oxlint-disable-next-line typescript/no-non-null-assertion -- in range: the index comes from a loop bound or a length checked in the same function
302
+ for (let i = wanted.length; i < kids.length; i++) {
303
+ const gone = kids[i];
304
+ ops.push(() => el.remove(gone));
305
+ }
306
+ }
307
+ /**
308
+ * Change a live tree to match JSON, in place, so state it holds (a zoom, a data source's identity) is kept wherever the JSON
309
+ * does not say otherwise. Nothing is changed unless all of the JSON reads; the root's type has to be the same.
310
+ */
311
+ export function applyJSON(element, json, registry, options = {}) {
312
+ const w = { registry, refs: options.refs, limit: options.inlineLimit ?? 1000 };
313
+ if (json === null || typeof json !== 'object')
314
+ fail('an element is an object { type, attrs?, children? }');
315
+ if (nameOf(element.constructor, registry, '') !== json.type)
316
+ fail(`the root is a ${element.constructor.name}: JSON for a "${String(json.type)}" cannot be applied to it`);
317
+ const ops = [];
318
+ plan(element, json, '', w, ops);
319
+ for (const op of ops)
320
+ op();
321
+ }
@@ -0,0 +1,304 @@
1
+ import { Axis } from './axis.ts';
2
+ import { type DataNode, type PlanContext, type PointBuffer } from './data.ts';
3
+ import { type AnyElement, type BaseAttributes, ChartElement, type ElementRef } from './element.ts';
4
+ import { type ChartEvent } from './events.ts';
5
+ import type { Extent, Size } from './geometry.ts';
6
+ import type { Plane } from './plane.ts';
7
+ import type { PlatformNode } from './platform.ts';
8
+ import type { Surface } from './surface.ts';
9
+ import { type AnySeriesRenderer, type PolygonSet, type Trace } from './renderer.ts';
10
+ import type { HitOptions, HitMode } from './hit.ts';
11
+ export interface SeriesAttributes extends BaseAttributes {
12
+ /** What the series is called where it is named: a tooltip's heading. */
13
+ name?: string;
14
+ /**
15
+ * How pointer hits find this series: a mode name, options, or `false` for a series that should never be hit (a
16
+ * selection overlay, a reference line). Only the series knows what its renderers draw, so this is set here and not
17
+ * guessed: a line series that should be grabbable anywhere along the line says `'line'`, a filled one `'area'`.
18
+ */
19
+ hit?: false | HitMode | HitOptions;
20
+ /**
21
+ * What a missing value (NaN, or infinite) means. 'break' (default): nothing is there, so a line stops and starts again
22
+ * and an area splits. 'connect': the line goes straight across to the next value there is. 'zero': the value is 0 (and
23
+ * the axis reaches it). Markers and bars draw nothing at a gap whichever; and the readings of a tooltip and a drag
24
+ * stay the data's own. In a `Stack`, 'zero' is not applied: a gap there leaves its layer out.
25
+ */
26
+ gaps?: 'break' | 'connect' | 'zero';
27
+ }
28
+ /**
29
+ * Where a `Stack` puts one of its series: the value each row starts from and the one it is plotted at, in the value
30
+ * axis' own units, and which place it takes beside the others when they sit side by side. Rows line up with the series'
31
+ * own rows. `additive` says the plotted value is the series' own plus a shift, so an edit can be undone back to the own.
32
+ */
33
+ export interface StackLayer {
34
+ readonly base: Float64Array;
35
+ readonly top: Float64Array;
36
+ readonly slot: {
37
+ readonly index: number;
38
+ readonly count: number;
39
+ };
40
+ readonly additive: boolean;
41
+ /** The extent of everything the stack plots in the value axis' units, for what is limited by the data's extent. */
42
+ readonly extent: Extent | null;
43
+ }
44
+ /** The rows a series draws from: its own fill, or what a stack made of them to line them up with the others. */
45
+ export interface Rows {
46
+ readonly raw: Float64Array;
47
+ readonly count: number;
48
+ readonly stride: number;
49
+ readonly rowOf: ((index: number) => number) | null;
50
+ }
51
+ /** A plane child that holds series of its own (a `Stack`): the plane sees through it, and hands it their loading. */
52
+ export interface SeriesGroup {
53
+ readonly members: readonly Series[];
54
+ load(estimate: Size): void;
55
+ }
56
+ /** 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. */
57
+ export declare function groupOf(child: unknown): (SeriesGroup & AnyElement) | null;
58
+ /** The series a plane child stands for: itself, the members of a group, or none. */
59
+ export declare function seriesOf(child: unknown): readonly Series[];
60
+ /** One data source, then any number of renderers. */
61
+ export type SeriesChildren<Channel extends string = string> = [data: DataNode<Channel>, ...renderers: AnySeriesRenderer[]];
62
+ /**
63
+ * What `Series.drawn` hands out: the plane, the points in plot pixels (`points`), and the data values they were made
64
+ * from (`raw`, `stride` numbers to a row, the two coordinates at `firstColumn` and `secondColumn`, the bound channels
65
+ * last, in the order of `channels`). `rowOf` names the source row of a drawn point, when they are the source's.
66
+ */
67
+ export interface Drawn {
68
+ readonly plane: Plane;
69
+ readonly points: Readonly<PointBuffer>;
70
+ readonly raw: Float64Array;
71
+ readonly stride: number;
72
+ readonly firstColumn: number;
73
+ readonly secondColumn: number;
74
+ readonly channels: readonly string[];
75
+ readonly rowOf: ((index: number) => number) | null;
76
+ readonly alongDomain: boolean;
77
+ /** In a stack: where each row starts and is plotted, and the series' place beside the others. Null otherwise. */
78
+ readonly base: Float64Array | null;
79
+ readonly top: Float64Array | null;
80
+ readonly slot: {
81
+ readonly index: number;
82
+ readonly count: number;
83
+ };
84
+ /** The plotted value is the own value plus a shift (see `StackLayer`). */
85
+ readonly additive: boolean;
86
+ }
87
+ /** The shared pipeline: raw fill (cached), column gather, normalise, place. Subclasses say which columns and axes. */
88
+ export declare abstract class Series<A extends SeriesAttributes = SeriesAttributes, Channel extends string = string> extends ChartElement<Plane | (SeriesGroup & AnyElement), SeriesChildren<Channel>, A> {
89
+ #private;
90
+ static attrTypes: {
91
+ name: 1;
92
+ id: 1;
93
+ meta: {
94
+ title: 1;
95
+ description: 1;
96
+ role: 1;
97
+ };
98
+ };
99
+ /** The plane this series is drawn on, which is its parent unless a stack sits between them. */
100
+ get plane(): Plane;
101
+ /** The rows drawn from: a stack's lined-up table when it made one, else the series' own fill. */
102
+ protected rows(): Rows;
103
+ /** Engine protocol for a `Stack`: the series' own fill as it stands (call after `prepare`). */
104
+ ownRows(): Rows;
105
+ /** Engine protocol for a `Stack`: take a place in a stack, with rows lined up to its others'. Null for both leaves it. */
106
+ join(layer: StackLayer | null, rows: Rows | null): void;
107
+ /** What a stack has made of this series, if it is in one. */
108
+ get layer(): StackLayer | null;
109
+ get data(): DataNode<Channel>;
110
+ /**
111
+ * Every channel its renderers bind or ask for (`channelNames`): what a renderer can ask for by name. In `points` they follow the two coordinates
112
+ * in this order, so a channel's column is `2 + channels.indexOf(name)`.
113
+ */
114
+ get channels(): readonly string[];
115
+ /**
116
+ * The colour this series takes from the theme's palette: by its position among the plane's series, round again when
117
+ * there are more series than colours. It is only a default: a renderer's own colour attribute or any `style` beats it.
118
+ */
119
+ get accentColor(): string;
120
+ /** 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. */
121
+ get shown(): boolean;
122
+ /**
123
+ * Draw a sample of what this series looks like into `parent`, `width` by `height` from its top left corner: each renderer's
124
+ * own sample (a line, a box, a marker), one over another in drawing order, in the look the renderer resolves from the
125
+ * theme and its attributes. A legend and a tooltip use it. Nodes are keyed `key/…` in `surface`.
126
+ */
127
+ sample(surface: Surface, parent: PlatformNode, key: string, width: number, height: number): void;
128
+ /**
129
+ * The colour that stands for this series: what its first renderer says (its line, its fill, a marker's colour),
130
+ * else the palette colour. A key or tooltip shows it beside the name.
131
+ */
132
+ get color(): string;
133
+ /** What the first renderer that has an opinion says a hit on this series should measure: bars say the shapes they draw. */
134
+ get hitHint(): HitMode | undefined;
135
+ /** The shapes the series draws, as polygons in plot pixels, if a renderer draws any (bars): what a 'shape' hit is inside. */
136
+ shapes(): PolygonSet | null;
137
+ /** 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. */
138
+ trace(): Trace | null;
139
+ /** How many times the series has been projected: a renderer that works something out from the points can tell whether they have changed. */
140
+ get projection(): number;
141
+ /** The last projected points, in plot pixels: `[first, second, ...channels]` in each row of `points.stride` numbers. */
142
+ get points(): Readonly<PointBuffer>;
143
+ onEvent(ev: ChartEvent): void;
144
+ /**
145
+ * The last projection, as the series holds it, for what reads a drawn series back: hit testing (hit.ts), which is
146
+ * kept out of the series so that a chart that never hit-tests does not carry it. Null before the first projection.
147
+ */
148
+ get drawn(): Drawn | null;
149
+ /** Whether the first coordinate is a position along the line, so that a value can be read between two points. */
150
+ protected get alongDomain(): boolean;
151
+ /** The axis the first coordinate rides on, which a plane places along its first role. */
152
+ abstract get firstAxis(): Axis;
153
+ /** The axis the second coordinate rides on. */
154
+ abstract get secondAxis(): Axis;
155
+ /** Which column of the data (domain 0, value 1, channels from 2) is the first coordinate. */
156
+ protected abstract get firstColumn(): number;
157
+ /** Which column of the data is the second coordinate. */
158
+ protected abstract get secondColumn(): number;
159
+ /** Pick the axes, from attributes or else the plane's. */
160
+ abstract resolveAxes(plane: Plane): void;
161
+ /** Report what is known of the domain before any data is read, so that fills can depend on it. */
162
+ abstract contributeDomain(): void;
163
+ /** Settle whatever `contributeDomain` fed, once every series has contributed. */
164
+ abstract resolveDomain(): void;
165
+ /** Read the data, and report every extent it has to the axes. `estimate` is the room the plot is expected to have. */
166
+ abstract load(estimate: Size): void;
167
+ /** The extent all of the data asks of an axis, not the part in view: what zooming is limited by. Null if it uses none of it. */
168
+ abstract extentFor(axis: Axis): Extent | null;
169
+ /**
170
+ * Fill the raw buffer, or reuse it when nothing it depends on has changed. Returns every column's extent: the
171
+ * domain, the value, then each requested channel.
172
+ */
173
+ protected fill(ctx: PlanContext): readonly Extent[];
174
+ /**
175
+ * Ask both axes for the room the renderers draw beyond the places of the points (a marker's radius), before the ranges
176
+ * are settled. It is pixels, which an axis turns into data by its length.
177
+ */
178
+ protected contributeReach(): void;
179
+ /** The largest `spans` of the renderers: how far they draw beyond a point along the domain, as a fraction of the gap between points. */
180
+ protected get spans(): number;
181
+ /**
182
+ * Widen the range of every mapped dimension a renderer binds to cover the whole of its channel. This is the whole
183
+ * of the data and not the part in view, so colours and sizes stay put while you zoom.
184
+ */
185
+ protected contributeChannels(): void;
186
+ /** The rows as a renderer's `spread` sees them. */
187
+ private spreadView;
188
+ /** The extent the data alone asks of the first (`'first'`) or second axis, over all of it: no renderer's spread in it. */
189
+ abstract ownExtent(role: 'first' | 'second'): Extent | null;
190
+ /** An axis' natural extent: the data's, widened by what the renderers draw beyond it. Zooming is held to this. */
191
+ protected widened(axis: Axis, base: Extent | null): Extent | null;
192
+ /**
193
+ * Normalize and place into plot pixels. The raw buffer is left alone, so a re-projection needs no refill.
194
+ * The result is `[first, second, ...bound channels]` whatever order the data held them in.
195
+ */
196
+ project(plane: Plane): void;
197
+ /**
198
+ * The line the series is measured from, as placed points: one for each drawn point, in the same place along the first
199
+ * coordinate, with the second held at `value` (kept inside its axis' range). Gaps stay gaps. The plane places it
200
+ * exactly as it places the data, so a renderer that fills between the two needs to know nothing about the plane:
201
+ * the data forward and this backward is a closed shape on a rectangle and on a circle alike.
202
+ */
203
+ baseline(value: number): Readonly<PointBuffer>;
204
+ }
205
+ export interface DomainSeriesAttributes extends SeriesAttributes {
206
+ /** Default: the plane's first axis (a Cartesian plane's horizontal one, a polar plane's angle). */
207
+ domainAxis?: ElementRef<Axis>;
208
+ /** Default: the plane's second axis (the vertical one, the radius). */
209
+ valueAxis?: ElementRef<Axis>;
210
+ }
211
+ /**
212
+ * A value at every position along a domain, drawn against two axes. The source delivers ascending in the domain and only
213
+ * 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.
214
+ */
215
+ export declare class DomainSeries extends Series<DomainSeriesAttributes> {
216
+ #private;
217
+ static attrTypes: {
218
+ name: 1;
219
+ domainAxis: 1;
220
+ valueAxis: 1;
221
+ id: 1;
222
+ meta: {
223
+ title: 1;
224
+ description: 1;
225
+ role: 1;
226
+ };
227
+ };
228
+ get domainAxis(): Axis;
229
+ get valueAxis(): Axis;
230
+ get firstAxis(): Axis;
231
+ get secondAxis(): Axis;
232
+ protected get firstColumn(): number;
233
+ protected get secondColumn(): number;
234
+ protected get alongDomain(): boolean;
235
+ resolveAxes(plane: Plane): void;
236
+ contributeDomain(): void;
237
+ resolveDomain(): void;
238
+ load(estimate: Size): void;
239
+ /** Engine protocol for a `Stack`: read the data (the first half of `load`) and give the rows, leaving the extents to the stack. */
240
+ prepare(estimate: Size): Rows;
241
+ /** Engine protocol for a `Stack`: the rest of `load`, with the value extent the stack worked out for its stacked values. */
242
+ settle(extent: Extent | null): void;
243
+ ownExtent(role: 'first' | 'second'): Extent | null;
244
+ extentFor(axis: Axis): Extent | null;
245
+ }
246
+ export interface XYSeriesAttributes<Channel extends string = string> extends SeriesAttributes {
247
+ /**
248
+ * The channel of the source that is x. The source's `value` is y, and its domain is only the order the points are
249
+ * drawn in. The type checker confirms the source offers this channel when its channels are spelled out.
250
+ */
251
+ x: Channel;
252
+ /** Default: the plane's first axis (a Cartesian plane's horizontal one). */
253
+ xAxis?: ElementRef<Axis>;
254
+ /** Default: the plane's second axis (the vertical one). */
255
+ yAxis?: ElementRef<Axis>;
256
+ }
257
+ /**
258
+ * Two values per point, one on each axis, joined in the order the source holds them: a scatter, a trajectory, a loop.
259
+ *
260
+ * The source's domain is only the sequence (leave `domain` out and it is the row's index), and its `value` is y. x
261
+ * is a channel, named by the `x` attribute. Because order is all the domain means:
262
+ * - every point is always read, whatever the view: there is nothing to window by;
263
+ * - a `Decimator` refuses it, since kernels thin by pixel column of the domain; draw it undecimated (markers already
264
+ * leave out what is off the plot);
265
+ * - an axis fits all the data, and zooming one axis does not refit the other to the points in view: with no order
266
+ * along x, finding those would be a scan of every point on every change.
267
+ */
268
+ export declare class XYSeries<Channel extends string = string> extends Series<XYSeriesAttributes<Channel>, Channel> {
269
+ #private;
270
+ static attrTypes: {
271
+ name: 1;
272
+ xAxis: 1;
273
+ yAxis: 1;
274
+ id: 1;
275
+ meta: {
276
+ title: 1;
277
+ description: 1;
278
+ role: 1;
279
+ };
280
+ };
281
+ get xAxis(): Axis;
282
+ get yAxis(): Axis;
283
+ get firstAxis(): Axis;
284
+ get secondAxis(): Axis;
285
+ protected get firstColumn(): number;
286
+ protected get secondColumn(): number;
287
+ resolveAxes(plane: Plane): void;
288
+ contributeDomain(): void;
289
+ resolveDomain(): void;
290
+ load(estimate: Size): void;
291
+ ownExtent(role: 'first' | 'second'): Extent | null;
292
+ extentFor(axis: Axis): Extent | null;
293
+ }
294
+ /** What `paintSample` remembers of each group it draws: whose sample it holds and of how many renderers. */
295
+ export type SampleCache = Map<string, {
296
+ series: Series;
297
+ renderers: number;
298
+ }>;
299
+ /**
300
+ * A sample of `series` in a group of `surface` keyed `key`, set at (x, y), `width` by `height`. A group that last held another
301
+ * series (a tooltip line that now reads another one) is made afresh, so nothing of the old one is left; `cache` is what the
302
+ * caller keeps between calls. Returns the group, for what the caller wants to do to it (dim it).
303
+ */
304
+ export declare function paintSample(surface: Surface, key: string, series: Series, x: number, y: number, width: number, height: number, cache: SampleCache): PlatformNode;