inkplot 0.0.0-stage → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -2
- package/dist/adapt.d.ts +10 -0
- package/dist/adapt.js +34 -0
- package/dist/annotation.d.ts +120 -0
- package/dist/annotation.js +267 -0
- package/dist/aria.d.ts +24 -0
- package/dist/aria.js +31 -0
- package/dist/attrs.d.ts +81 -0
- package/dist/attrs.js +84 -0
- package/dist/axis.d.ts +137 -0
- package/dist/axis.js +185 -0
- package/dist/bars.d.ts +80 -0
- package/dist/bars.js +255 -0
- package/dist/boxplot.d.ts +57 -0
- package/dist/boxplot.js +177 -0
- package/dist/boxzoom.d.ts +39 -0
- package/dist/boxzoom.js +138 -0
- package/dist/brush.d.ts +60 -0
- package/dist/brush.js +185 -0
- package/dist/bucket.d.ts +22 -0
- package/dist/bucket.js +62 -0
- package/dist/callout.d.ts +30 -0
- package/dist/callout.js +98 -0
- package/dist/chart.d.ts +126 -0
- package/dist/chart.js +257 -0
- package/dist/crosshair.d.ts +28 -0
- package/dist/crosshair.js +78 -0
- package/dist/cursor.d.ts +76 -0
- package/dist/cursor.js +344 -0
- package/dist/curve.d.ts +50 -0
- package/dist/curve.js +87 -0
- package/dist/curves.d.ts +104 -0
- package/dist/curves.js +375 -0
- package/dist/data.d.ts +209 -0
- package/dist/data.js +276 -0
- package/dist/decimation.d.ts +86 -0
- package/dist/decimation.js +410 -0
- package/dist/density.d.ts +55 -0
- package/dist/density.js +252 -0
- package/dist/dimension.d.ts +103 -0
- package/dist/dimension.js +205 -0
- package/dist/dom-platform.d.ts +53 -0
- package/dist/dom-platform.js +190 -0
- package/dist/element.d.ts +232 -0
- package/dist/element.js +414 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.js +237 -0
- package/dist/events.d.ts +144 -0
- package/dist/events.js +19 -0
- package/dist/focus.d.ts +22 -0
- package/dist/focus.js +51 -0
- package/dist/furniture.d.ts +70 -0
- package/dist/furniture.js +166 -0
- package/dist/geometry.d.ts +40 -0
- package/dist/geometry.js +7 -0
- package/dist/heatmap.d.ts +57 -0
- package/dist/heatmap.js +207 -0
- package/dist/hit.d.ts +103 -0
- package/dist/hit.js +428 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +73 -0
- package/dist/input.d.ts +42 -0
- package/dist/input.js +166 -0
- package/dist/jsx-dev-runtime.d.ts +9 -0
- package/dist/jsx-dev-runtime.js +13 -0
- package/dist/jsx-runtime.d.ts +59 -0
- package/dist/jsx-runtime.js +81 -0
- package/dist/key.d.ts +118 -0
- package/dist/key.js +199 -0
- package/dist/legend.d.ts +36 -0
- package/dist/legend.js +220 -0
- package/dist/mapped.d.ts +80 -0
- package/dist/mapped.js +106 -0
- package/dist/marker.d.ts +143 -0
- package/dist/marker.js +213 -0
- package/dist/panzoom.d.ts +119 -0
- package/dist/panzoom.js +467 -0
- package/dist/plane.d.ts +173 -0
- package/dist/plane.js +370 -0
- package/dist/platform.d.ts +113 -0
- package/dist/platform.js +80 -0
- package/dist/png.d.ts +44 -0
- package/dist/png.js +71 -0
- package/dist/pointer.d.ts +54 -0
- package/dist/pointer.js +132 -0
- package/dist/quantile.d.ts +3 -0
- package/dist/quantile.js +11 -0
- package/dist/radial.d.ts +116 -0
- package/dist/radial.js +336 -0
- package/dist/react.d.ts +441 -0
- package/dist/react.js +252 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +51 -0
- package/dist/renderer.d.ts +191 -0
- package/dist/renderer.js +164 -0
- package/dist/scale.d.ts +37 -0
- package/dist/scale.js +19 -0
- package/dist/scales.d.ts +69 -0
- package/dist/scales.js +134 -0
- package/dist/serialize.d.ts +77 -0
- package/dist/serialize.js +321 -0
- package/dist/series.d.ts +304 -0
- package/dist/series.js +513 -0
- package/dist/simple.d.ts +35 -0
- package/dist/simple.js +20 -0
- package/dist/sources.d.ts +116 -0
- package/dist/sources.js +237 -0
- package/dist/stack.d.ts +61 -0
- package/dist/stack.js +172 -0
- package/dist/stats.d.ts +38 -0
- package/dist/stats.js +159 -0
- package/dist/steps.d.ts +50 -0
- package/dist/steps.js +154 -0
- package/dist/stub.d.ts +3 -0
- package/dist/stub.js +5 -0
- package/dist/style-names.d.ts +1 -0
- package/dist/style-names.js +6 -0
- package/dist/summary.d.ts +44 -0
- package/dist/summary.js +58 -0
- package/dist/surface.d.ts +94 -0
- package/dist/surface.js +145 -0
- package/dist/table.d.ts +39 -0
- package/dist/table.js +37 -0
- package/dist/text.d.ts +23 -0
- package/dist/text.js +44 -0
- package/dist/theme.d.ts +174 -0
- package/dist/theme.js +73 -0
- package/dist/themes.d.ts +9 -0
- package/dist/themes.js +56 -0
- package/dist/time-intl.d.ts +194 -0
- package/dist/time-intl.js +144 -0
- package/dist/time-temporal.d.ts +236 -0
- package/dist/time-temporal.js +58 -0
- package/dist/time.d.ts +216 -0
- package/dist/time.js +154 -0
- package/dist/trend.d.ts +151 -0
- package/dist/trend.js +500 -0
- package/dist/web-components.d.ts +62 -0
- package/dist/web-components.js +264 -0
- package/package.json +53 -3
|
@@ -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
|
+
}
|
package/dist/series.d.ts
ADDED
|
@@ -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;
|