chartist 2.0.0-rc.2 → 2.0.0

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/dist/live.js ADDED
@@ -0,0 +1,767 @@
1
+ import { SvgPath, applyAttribute, domRenderer, keySplinesOf, namespaces, polarToCartesian, renderElement, sampleEasing } from "./index.js";
2
+ //#region src/live/keyframes.ts
3
+ /**
4
+ * The keyframes `smilAnimations()` brings for the built-in nodes that
5
+ * don't move well number by number: lines, areas and bands whose values
6
+ * change their keys or count, and pie slices and their labels.
7
+ */
8
+ const lerp = (from, to, progress) => (1 - progress) * from + progress * to;
9
+ const between = (from, to, progress) => ({
10
+ x: lerp(from.x, to.x, progress),
11
+ y: lerp(from.y, to.y, progress)
12
+ });
13
+ /**
14
+ * The vertices of a path: one per run of commands that share their
15
+ * value's data. A step line draws a corner and the point with the same
16
+ * data, so the point is the first or the last command of its run.
17
+ */
18
+ function verticesOf(commands, last) {
19
+ const vertices = [];
20
+ let previous;
21
+ for (const { x, y, data } of commands) {
22
+ if (data && data !== previous) vertices.push({
23
+ x,
24
+ y,
25
+ data
26
+ });
27
+ else if (data && last) vertices[vertices.length - 1] = {
28
+ x,
29
+ y,
30
+ data
31
+ };
32
+ previous = data;
33
+ }
34
+ return vertices;
35
+ }
36
+ /**
37
+ * Draws a path through points with the interpolation the layer used, a
38
+ * hole between values whose indexes aren't next to each other.
39
+ */
40
+ function draw(smoothing, vertices, points) {
41
+ const coordinates = [];
42
+ const data = [];
43
+ vertices.forEach((vertex, index) => {
44
+ const valueIndex = vertex.data.valueIndex;
45
+ if (index && valueIndex - vertices[index - 1].data.valueIndex > 1) {
46
+ coordinates.push(0, 0);
47
+ data.push({
48
+ value: void 0,
49
+ valueIndex: valueIndex - 1
50
+ });
51
+ }
52
+ coordinates.push(points[index].x, points[index].y);
53
+ data.push(vertex.data);
54
+ });
55
+ return smoothing(coordinates, data);
56
+ }
57
+ /** Draws an outline as the line, area and band layers do. */
58
+ function drawOutline(role, { edges, smoothing }, points, base) {
59
+ const [first, second] = edges.map((edge, index) => draw(smoothing, edge, points[index]));
60
+ const commands = first.pathElements;
61
+ const start = commands[0];
62
+ const end = commands[commands.length - 1];
63
+ if (role === "area") return first.clone(true).position(0).remove(1).move(start.x, base).line(start.x, start.y, false, start.data).position(commands.length + 1).line(end.x, base).stringify();
64
+ if (role === "band") {
65
+ const lower = second.pathElements;
66
+ const last = lower[lower.length - 1];
67
+ const back = new SvgPath().line(last.x, last.y, false, last.data);
68
+ for (let index = lower.length - 1; index > 0; index--) {
69
+ const command = lower[index];
70
+ const target = lower[index - 1];
71
+ if (command.command === "C") back.curve(command.x2, command.y2, command.x1, command.y1, target.x, target.y, false, target.data);
72
+ else back.line(target.x, target.y, false, target.data);
73
+ }
74
+ return SvgPath.join([first, back], true).stringify();
75
+ }
76
+ return first.stringify();
77
+ }
78
+ /**
79
+ * Reads a line, area or band back into its values, when drawing them
80
+ * again gives the node's `d`: nothing for paths a listener changed or an
81
+ * interpolation that can't be read back.
82
+ */
83
+ function outlineOf(node) {
84
+ const { path, smoothing } = node.ctx;
85
+ const commands = path.pathElements;
86
+ for (const last of [false, true]) {
87
+ const vertices = verticesOf(node.role === "area" ? commands.slice(1, -1) : commands, last);
88
+ const half = vertices.length / 2;
89
+ const outline = {
90
+ edges: node.role !== "band" ? [vertices] : Number.isInteger(half) ? [vertices.slice(0, half), vertices.slice(half).reverse()] : [],
91
+ base: commands[0]?.y ?? 0,
92
+ smoothing
93
+ };
94
+ if (outline.edges.length && outline.edges.every((edge) => edge.length) && drawOutline(node.role, outline, outline.edges, outline.base) === String(node.attrs.d)) return outline;
95
+ }
96
+ }
97
+ /** The series a node's values belong to; for a band, the one it follows. */
98
+ function seriesIndexOf(node) {
99
+ if (node.role !== "band") return node.ctx.seriesIndex;
100
+ const edge = node.ctx.high ?? node.ctx.low;
101
+ return edge && "seriesIndex" in edge ? edge.seriesIndex : node.ctx.series?.[0] ?? 0;
102
+ }
103
+ /** The value keys of each edge, a repeated key numbered in its order. */
104
+ function keysOf(node, { edges }, context, scene) {
105
+ const seriesIndex = seriesIndexOf(node);
106
+ return edges.map((edge) => {
107
+ const seen = /* @__PURE__ */ new Map();
108
+ return edge.map(({ data }) => {
109
+ const key = context.valueKey({
110
+ seriesIndex,
111
+ valueIndex: data.valueIndex,
112
+ value: data.value,
113
+ meta: data.meta
114
+ }, scene);
115
+ const count = (seen.get(key) ?? 0) + 1;
116
+ seen.set(key, count);
117
+ return count > 1 ? `${key}#${count}` : key;
118
+ });
119
+ });
120
+ }
121
+ /**
122
+ * Keeps the direction of X between neighbors as in the new path, so
123
+ * interpolations that break the line where X turns back (monotone cubic)
124
+ * draw every frame with the new path's commands.
125
+ */
126
+ function keepDirection(points, edge) {
127
+ for (let index = 1; index < points.length; index++) {
128
+ const direction = Math.sign(edge[index].x - edge[index - 1].x);
129
+ const previous = points[index - 1].x;
130
+ if (Math.sign(points[index].x - previous) !== direction) points[index] = {
131
+ x: previous + direction / 1e3,
132
+ y: points[index].y
133
+ };
134
+ }
135
+ return points;
136
+ }
137
+ /**
138
+ * Lines, areas and bands whose values changed keys or count: each value
139
+ * starts where its key was, a new one where its nearest kept neighbor was
140
+ * (the one before it, if any), and every frame is drawn through the
141
+ * layer's interpolation.
142
+ */
143
+ function pathKeyframes(previous, next, progress, context) {
144
+ const from = outlineOf(previous);
145
+ const to = outlineOf(next);
146
+ if (!from || !to || from.edges.length !== to.edges.length) return;
147
+ let fromKeys;
148
+ let toKeys;
149
+ try {
150
+ fromKeys = keysOf(previous, from, context, "previous");
151
+ toKeys = keysOf(next, to, context, "next");
152
+ } catch {
153
+ return;
154
+ }
155
+ if (fromKeys.join("\n") === toKeys.join("\n")) return;
156
+ const starts = [];
157
+ for (const [edgeIndex, keys] of toKeys.entries()) {
158
+ const positions = new Map(fromKeys[edgeIndex].map((key, index) => [key, from.edges[edgeIndex][index]]));
159
+ const kept = keys.map((key) => positions.get(key));
160
+ const edgeStarts = [];
161
+ for (let index = 0; index < keys.length; index++) {
162
+ let start;
163
+ for (let before = index; before >= 0 && !start; before--) start = kept[before];
164
+ for (let after = index + 1; after < keys.length && !start; after++) start = kept[after];
165
+ if (!start) return;
166
+ edgeStarts.push(start);
167
+ }
168
+ starts.push(edgeStarts);
169
+ }
170
+ return progress.map((at) => ({ d: drawOutline(next.role, to, to.edges.map((edge, edgeIndex) => keepDirection(edge.map((vertex, index) => between(starts[edgeIndex][index], vertex, at)), edge)), lerp(from.base, to.base, at)) }));
171
+ }
172
+ /** The angle of a point around a center, in degrees clockwise from the top. */
173
+ const angleOf = (center, point) => Math.atan2(point.y - center.y, point.x - center.x) * 180 / Math.PI + 90;
174
+ /**
175
+ * A slice at interpolated angles, center and radius, drawn in the pie
176
+ * layer's command layout: from its end around to its start, to the center
177
+ * for a pie. The slice's overlap with the one before it is read from its
178
+ * path.
179
+ */
180
+ function sliceKeyframes(previous, next, progress) {
181
+ const { center, radius, startAngle, endAngle, path } = next.ctx;
182
+ const pie = String(next.attrs.d).endsWith("Z");
183
+ const commands = path.pathElements;
184
+ const overlap = ((startAngle - angleOf(center, commands[commands.length - 1 - +pie])) % 360 + 540) % 360 - 180;
185
+ return progress.map((at) => {
186
+ const middle = between(previous.ctx.center, center, at);
187
+ const r = lerp(previous.ctx.radius, radius, at);
188
+ const start = lerp(previous.ctx.startAngle, startAngle, at);
189
+ const end = lerp(previous.ctx.endAngle, endAngle, at);
190
+ const overlappingStart = Math.max(0, start - overlap);
191
+ const fullCircle = end - start >= 359.99;
192
+ const arcEnd = Math.min(end, overlappingStart + 359.99);
193
+ const from = polarToCartesian(middle.x, middle.y, r, overlappingStart);
194
+ const frame = new SvgPath(pie);
195
+ if (fullCircle) {
196
+ const opposite = polarToCartesian(middle.x, middle.y, r, overlappingStart + 180);
197
+ frame.move(from.x, from.y).arc(r, r, 0, 0, 0, opposite.x, opposite.y);
198
+ } else {
199
+ const to = polarToCartesian(middle.x, middle.y, r, arcEnd);
200
+ frame.move(to.x, to.y);
201
+ }
202
+ frame.arc(r, r, 0, Number(!fullCircle && end - start > 180), 0, from.x, from.y);
203
+ if (pie) frame.line(middle.x, middle.y);
204
+ return { d: frame.stringify() };
205
+ });
206
+ }
207
+ /** The slice with the label's key, in a scene. */
208
+ function sliceOf(scene, key) {
209
+ if (scene.role === "slice" && scene.key === key) return scene;
210
+ for (const child of scene.children) {
211
+ const slice = sliceOf(child, key);
212
+ if (slice) return slice;
213
+ }
214
+ }
215
+ /**
216
+ * A pie label moving around the pie with its slice: at the interpolated
217
+ * middle angle of the slice, its distance from the center interpolated,
218
+ * and a label moved away from that spot (to keep labels apart) moved by
219
+ * as much.
220
+ */
221
+ function labelKeyframes(previous, next, progress, context) {
222
+ const fromSlice = sliceOf(context.update.previousScene, previous.key);
223
+ const toSlice = sliceOf(context.scene, next.key);
224
+ if (!fromSlice || !toSlice || previous.ctx.kind !== "pie" || next.ctx.kind !== "pie") return;
225
+ const place = ({ ctx }, label) => {
226
+ const angle = (ctx.startAngle + ctx.endAngle) / 2;
227
+ const radius = Math.hypot(label.x - ctx.center.x, label.y - ctx.center.y);
228
+ const spot = polarToCartesian(ctx.center.x, ctx.center.y, radius, angle);
229
+ return {
230
+ angle,
231
+ radius,
232
+ center: ctx.center,
233
+ shift: {
234
+ x: label.x - spot.x,
235
+ y: label.y - spot.y
236
+ }
237
+ };
238
+ };
239
+ const from = place(fromSlice, previous.ctx);
240
+ const to = place(toSlice, next.ctx);
241
+ return progress.map((at) => {
242
+ const center = between(from.center, to.center, at);
243
+ const shift = between(from.shift, to.shift, at);
244
+ const spot = polarToCartesian(center.x, center.y, lerp(from.radius, to.radius, at), lerp(from.angle, to.angle, at));
245
+ const frame = {};
246
+ for (const name of [
247
+ "x",
248
+ "y",
249
+ "dx",
250
+ "dy"
251
+ ]) {
252
+ const value = next.attrs[name];
253
+ if (typeof value === "number") {
254
+ const axis = name.endsWith("x") ? "x" : "y";
255
+ frame[name] = value + spot[axis] + shift[axis] - next.ctx[axis];
256
+ }
257
+ }
258
+ return frame;
259
+ });
260
+ }
261
+ /**
262
+ * The built-in keyframes of an updated node, or nothing when it moves
263
+ * well number by number.
264
+ */
265
+ function builtInKeyframes(previous, next, progress, context) {
266
+ if (previous.role !== next.role) return;
267
+ switch (next.role) {
268
+ case "line":
269
+ case "area":
270
+ case "band": return pathKeyframes(previous, next, progress, context);
271
+ case "slice": return sliceKeyframes(previous, next, progress);
272
+ case "label": return labelKeyframes(previous, next, progress, context);
273
+ default: return;
274
+ }
275
+ }
276
+ //#endregion
277
+ //#region src/live/animations.ts
278
+ /** The class of the `animate` elements of update animations. */
279
+ const ANIMATION_CLASS = "ct-update-animation";
280
+ const NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
281
+ /** The CSS easings `--ct-update-easing` takes; anything else is `ease-out`. */
282
+ const CSS_EASING = /^(linear|ease(-in|-out|-in-out)?|(cubic-bezier|linear|steps)\(.*\))$/;
283
+ /** Without a duration in the CSS, updates take this long, in ms. */
284
+ const DEFAULT_DURATION = 300;
285
+ /** A CSS time (`400ms`, `0.4s`) in ms; the default for anything else. */
286
+ function durationOf(value) {
287
+ const [, amount, unit] = value?.trim().match(/^(\d*\.?\d+(?:e[-+]?\d+)?)(m?s)$/i) ?? [];
288
+ return unit ? Number(amount) * (unit.toLowerCase() === "s" ? 1e3 : 1) : DEFAULT_DURATION;
289
+ }
290
+ /** A path's `d` with absolute commands, unrounded. */
291
+ function absolutePath(d) {
292
+ const path = new SvgPath(false, { accuracy: false }).parse(d);
293
+ let x = 0;
294
+ let y = 0;
295
+ let startX = 0;
296
+ let startY = 0;
297
+ for (const command of path.pathElements) {
298
+ const letter = String(command.command);
299
+ if (letter !== letter.toUpperCase()) {
300
+ for (const name of Object.keys(command)) if (name[0] === "x" && name !== "xAr") command[name] = Number(command[name]) + x;
301
+ else if (name[0] === "y") command[name] = Number(command[name]) + y;
302
+ }
303
+ command.command = letter.toUpperCase();
304
+ if (command.command === "Z") {
305
+ x = startX;
306
+ y = startY;
307
+ } else {
308
+ x = "x" in command ? Number(command.x) : x;
309
+ y = "y" in command ? Number(command.y) : y;
310
+ }
311
+ if (command.command === "M") {
312
+ startX = x;
313
+ startY = y;
314
+ }
315
+ }
316
+ return path.stringify() + (/z\s*$/i.test(d) ? "Z" : "");
317
+ }
318
+ /**
319
+ * The shape of an attribute's value, for a number or a path's `d`;
320
+ * nothing for other values.
321
+ */
322
+ function shapeOf(name, value) {
323
+ let text = String(value);
324
+ if (name === "d") try {
325
+ text = absolutePath(text);
326
+ } catch {
327
+ return;
328
+ }
329
+ else if (value === void 0 || !text.trim() || !isFinite(Number(text))) return;
330
+ return {
331
+ text: text.replace(NUMBER, "#"),
332
+ numbers: (text.match(NUMBER) ?? []).map(Number)
333
+ };
334
+ }
335
+ /** A shape's text with the given numbers, rounded to 3 decimals. */
336
+ function fill(text, numbers) {
337
+ let index = 0;
338
+ return text.replace(/#/g, () => `${Math.round(numbers[index++] * 1e3) / 1e3}`);
339
+ }
340
+ /**
341
+ * Whether an attribute can animate as an offset added to its value: a
342
+ * number, or a path with absolute commands and no arcs, whose flags
343
+ * can't be added.
344
+ */
345
+ const isAdditive = (name, value, shape) => name !== "d" || !/[mlhvcsqtaz]/.test(String(value)) && !shape.text.includes("A");
346
+ /** Removes an element's update animations that have ended. */
347
+ function removeEnded(element) {
348
+ for (const child of Array.from(element.children)) if (child.classList.contains(ANIMATION_CLASS)) {
349
+ const animation = child;
350
+ try {
351
+ if (animation.getStartTime() + animation.getSimpleDuration() <= animation.getCurrentTime()) child.remove();
352
+ } catch {
353
+ if (typeof animation.getStartTime === "function") child.remove();
354
+ }
355
+ }
356
+ }
357
+ /** Adds an `animate` element to an element and starts it. */
358
+ function write(element, name, values, additive, timing, splines) {
359
+ const animate = element.ownerDocument.createElementNS(namespaces.svg, "animate");
360
+ const attributes = {
361
+ class: ANIMATION_CLASS,
362
+ attributeName: name,
363
+ begin: "indefinite",
364
+ dur: `${timing.duration}ms`,
365
+ fill: "remove",
366
+ values: values.join(";"),
367
+ keyTimes: values.map((_, index) => Math.round(index / (values.length - 1) * 1e4) / 1e4).join(";"),
368
+ calcMode: splines ? "spline" : timing.calcMode,
369
+ keySplines: splines,
370
+ additive: additive ? "sum" : void 0
371
+ };
372
+ for (const [attribute, value] of Object.entries(attributes)) if (value !== void 0) animate.setAttribute(attribute, value);
373
+ animate.addEventListener("endEvent", () => animate.remove());
374
+ element.appendChild(animate);
375
+ try {
376
+ animate.beginElement();
377
+ animate.getStartTime();
378
+ } catch {
379
+ if (typeof animate.getStartTime === "function") animate.remove();
380
+ }
381
+ }
382
+ /**
383
+ * Animates an attribute number by number from its previous value: an
384
+ * offset from `previous − next` to 0 when it can be added, else the
385
+ * values themselves. Paths with different commands, or with arcs, don't
386
+ * animate.
387
+ */
388
+ function animateNumbers(element, name, previous, next, timing) {
389
+ const from = shapeOf(name, previous);
390
+ const to = shapeOf(name, next);
391
+ if (!from || !to || from.text !== to.text || to.text.includes("A")) return;
392
+ const additive = isAdditive(name, next, to);
393
+ const at = (progress) => fill(to.text, to.numbers.map((value, index) => additive ? (from.numbers[index] - value) * (1 - progress) : (1 - progress) * from.numbers[index] + progress * value));
394
+ write(element, name, (timing.splines ? [0, 1] : timing.progress).map(at), additive, timing, timing.splines);
395
+ }
396
+ /**
397
+ * Animates an attribute through keyframes: as offsets from the new value
398
+ * when every frame has its shape, so an interruption continues from where
399
+ * the element is drawn, else as the frames' values.
400
+ */
401
+ function animateFrames(element, name, frames, next, timing) {
402
+ const to = shapeOf(name, next);
403
+ const offsets = [];
404
+ if (to && isAdditive(name, String(next), to)) for (const frame of frames) {
405
+ const shape = shapeOf(name, frame);
406
+ if (shape?.text !== to.text) break;
407
+ offsets.push(fill(to.text, shape.numbers.map((value, index) => value - to.numbers[index])));
408
+ }
409
+ const additive = offsets.length === frames.length;
410
+ write(element, name, additive ? offsets : frames.map(String), additive, timing);
411
+ }
412
+ /**
413
+ * Animates the changes of an update with SMIL, timed by the chart's CSS:
414
+ * each updated node's changed numbers and paths move from their previous
415
+ * value to their new one over `--ct-update-duration` (a CSS time, 300 ms
416
+ * by default; `0s` turns the animations off) with the easing in
417
+ * `--ct-update-easing` (a CSS easing, `ease-out` by default). The
418
+ * attributes hold their new values from the start, and an update during an
419
+ * animation continues from where each element is drawn.
420
+ *
421
+ * It animates updates (`update()`, `setOptions()`) and changes of the
422
+ * matching `responsive` options, not resizes, and nothing while the
423
+ * visitor's system asks for reduced motion. Lines and areas follow their
424
+ * values' keys, and pie slices turn around the pie; layers and plugins can
425
+ * give keyframes of their own.
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * import { lineChart } from 'chartist';
430
+ * import { liveDomRenderer, smilAnimations } from 'chartist/live';
431
+ *
432
+ * // 300 ms with ease-out, unless the CSS sets --ct-update-duration
433
+ * // and --ct-update-easing.
434
+ * const chart = lineChart(
435
+ * '#chart',
436
+ * { labels: ['Mon', 'Tue', 'Wed'], series: [[5, 2, 4]] },
437
+ * { renderer: liveDomRenderer({ animations: smilAnimations() }) }
438
+ * );
439
+ *
440
+ * chart.update({ data: { labels: ['Mon', 'Tue', 'Wed'], series: [[3, 6, 1]] } });
441
+ * ```
442
+ * @returns The animations, for `liveDomRenderer({ animations })`
443
+ * @experimental
444
+ */
445
+ function smilAnimations() {
446
+ return { animate(context) {
447
+ const { update, container, measureStyle } = context;
448
+ const options = {
449
+ container,
450
+ rootClass: context.scene.classes.join(" ")
451
+ };
452
+ const duration = durationOf(measureStyle?.("--ct-update-duration", options));
453
+ if (update.cause !== "update" && update.cause !== "responsive" || !(duration > 0) || container.ownerDocument.defaultView?.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
454
+ const css = measureStyle?.("--ct-update-easing", options)?.trim() ?? "";
455
+ const easing = CSS_EASING.test(css) ? css : "ease-out";
456
+ const timing = {
457
+ duration,
458
+ easing,
459
+ progress: sampleEasing(easing, duration),
460
+ splines: keySplinesOf(easing),
461
+ calcMode: easing.startsWith("steps(") ? "discrete" : "linear"
462
+ };
463
+ for (const { op, node, previous } of context.changes) {
464
+ const element = context.elementOf(node);
465
+ if (op !== "update" || !previous || !element) continue;
466
+ removeEnded(element);
467
+ const changed = Object.keys(node.attrs).filter((name) => !name.includes(":") && previous.attrs[name] !== void 0 && String(previous.attrs[name]) !== String(node.attrs[name]));
468
+ if (!changed.length) continue;
469
+ const frames = update.keyframes(previous, node, timing.progress) ?? builtInKeyframes(previous, node, timing.progress, context) ?? [];
470
+ const framed = Object.keys(frames[0] ?? {}).filter((name) => frames.every((frame) => frame[name] !== void 0));
471
+ for (const name of framed) animateFrames(element, name, frames.map((frame) => frame[name]), node.attrs[name], timing);
472
+ for (const name of changed) if (!framed.includes(name)) animateNumbers(element, name, previous.attrs[name], node.attrs[name], timing);
473
+ }
474
+ return {
475
+ duration,
476
+ easing
477
+ };
478
+ } };
479
+ }
480
+ //#endregion
481
+ //#region src/live/diff.ts
482
+ /** The value key function of a `key` option. */
483
+ function valueKeyOf(key = "index") {
484
+ return key === "index" ? (value, index = value.valueIndex) => String(index) : key === "label" ? ({ label }) => String(label) : (value) => String(key(value));
485
+ }
486
+ /** What a node stands for, before repeats among its siblings are counted. */
487
+ function identityOf(current, valueKey, labels) {
488
+ const ctx = current.ctx ?? {};
489
+ const { seriesIndex, valueIndex, axis, index } = ctx;
490
+ const role = current.role ?? current.name;
491
+ if (typeof seriesIndex === "number") {
492
+ const own = current.key ?? "";
493
+ const end = own.indexOf("/");
494
+ const layer = end < 0 ? own : own.slice(0, end);
495
+ const slice = current.role === "slice" || ctx.kind === "pie" ? ctx : current.role === "series" && current.children.length === 1 && current.children[0].role === "slice" ? current.children[0].ctx : void 0;
496
+ if (slice) return `v:${layer}/pie/${valueKey({
497
+ seriesIndex,
498
+ valueIndex: 0,
499
+ value: slice.value,
500
+ label: labels[seriesIndex],
501
+ meta: slice.meta
502
+ }, seriesIndex)}:${role}`;
503
+ if (typeof valueIndex !== "number") return `s:${layer}/${seriesIndex}:${role}`;
504
+ return `v:${layer}/${seriesIndex}/${valueKey({
505
+ seriesIndex,
506
+ valueIndex,
507
+ value: ctx.value,
508
+ label: labels[valueIndex],
509
+ meta: ctx.meta
510
+ })}:${role}`;
511
+ }
512
+ if (axis && typeof index === "number") return `t:${axis.id ?? axis.dimension}:${role}:${String(axis.ticks[index])}`;
513
+ if (current.key !== void 0) return `k:${current.key}`;
514
+ return `p:${current.name}.${current.classes[0] ?? ""}`;
515
+ }
516
+ /** The identities of sibling nodes, with `#2`, `#3` for repeats. */
517
+ function identities(nodes, valueKey, labels) {
518
+ const seen = /* @__PURE__ */ new Map();
519
+ return nodes.map((current) => {
520
+ const identity = identityOf(current, valueKey, labels);
521
+ const count = (seen.get(identity) ?? 0) + 1;
522
+ seen.set(identity, count);
523
+ return count > 1 ? `${identity}#${count}` : identity;
524
+ });
525
+ }
526
+ /** Whether a node's classes, attributes or text changed. */
527
+ function differs(previous, next) {
528
+ if (previous.content !== next.content || previous.classes.length !== next.classes.length || previous.classes.some((name, i) => next.classes[i] !== name)) return true;
529
+ const names = Object.keys(next.attrs);
530
+ return names.length !== Object.keys(previous.attrs).length || names.some((name) => previous.attrs[name] !== next.attrs[name]);
531
+ }
532
+ /** Lists a node and every node below it, in document order. */
533
+ function every(op, current, changes) {
534
+ changes.push({
535
+ op,
536
+ node: current
537
+ });
538
+ current.children.forEach((child) => every(op, child, changes));
539
+ }
540
+ /**
541
+ * Matches two scenes per parent and lists the changes in the new scene's
542
+ * document order, `remove` entries after their former parent's kept
543
+ * children. Roots with different element names aren't matched.
544
+ */
545
+ function matchScenes(previous, next, options = {}) {
546
+ const valueKey = valueKeyOf(options.key);
547
+ const labels = options.labels ?? [];
548
+ const previousLabels = options.previousLabels ?? labels;
549
+ const changes = [];
550
+ const matched = /* @__PURE__ */ new Map();
551
+ const visit = (before, after) => {
552
+ matched.set(after, before);
553
+ if (differs(before, after)) changes.push({
554
+ op: "update",
555
+ node: after,
556
+ previous: before
557
+ });
558
+ const kept = /* @__PURE__ */ new Map();
559
+ identities(before.children, valueKey, previousLabels).forEach((identity, i) => kept.set(identity, before.children[i]));
560
+ const removed = new Set(before.children);
561
+ identities(after.children, valueKey, labels).forEach((identity, i) => {
562
+ const child = after.children[i];
563
+ const match = kept.get(identity);
564
+ if (match && match.name === child.name) {
565
+ removed.delete(match);
566
+ visit(match, child);
567
+ } else every("add", child, changes);
568
+ });
569
+ removed.forEach((child) => every("remove", child, changes));
570
+ };
571
+ if (previous.name === next.name) visit(previous, next);
572
+ else {
573
+ every("add", next, changes);
574
+ every("remove", previous, changes);
575
+ }
576
+ return {
577
+ changes,
578
+ matched
579
+ };
580
+ }
581
+ /**
582
+ * The changes between two scenes, matched by what each node stands for:
583
+ * `add`, `update` (with the previous node) and `remove`, in the new
584
+ * scene's document order, with the `remove` entries after their former
585
+ * parent's kept children. Every node of an added or removed subtree has
586
+ * its own entry. The live renderer patches its SVG with them; other
587
+ * renderers that change their output in place can use them too.
588
+ *
589
+ * Nodes are matched among the children of matched parents: values by
590
+ * their layer, series and key (see `key`), series by layer and index,
591
+ * grid lines and axis labels by axis and tick, nodes with their own
592
+ * `key` by it, and the rest by element name and first class, in order.
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * import { lineChartConfig, toScene } from 'chartist';
597
+ * import { diffScenes } from 'chartist/live';
598
+ *
599
+ * const size = { width: 600, height: 300 };
600
+ * const labels = ['Mo', 'Tu', 'We'];
601
+ * const before = toScene(lineChartConfig({ labels, series: [[1, 2, 3]] }), size);
602
+ * const after = toScene(lineChartConfig({ labels, series: [[1, 5, 3]] }), size);
603
+ *
604
+ * for (const { op, node } of diffScenes(before, after, { labels })) {
605
+ * console.log(op, node.role, node.key);
606
+ * }
607
+ * ```
608
+ * @param previous The previous scene
609
+ * @param next The new scene
610
+ * @param options How values are identified, and the data's labels
611
+ * @returns The changes, in the new scene's document order
612
+ * @experimental
613
+ */
614
+ function diffScenes(previous, next, options = {}) {
615
+ return matchScenes(previous, next, options).changes;
616
+ }
617
+ //#endregion
618
+ //#region src/live/renderer.ts
619
+ /** Sets the text of a childless element, keeping elements a listener added. */
620
+ function setText(element, content) {
621
+ const text = Array.from(element.childNodes).find((child) => child.nodeType === 3);
622
+ if (!content) text?.remove();
623
+ else if (text) text.nodeValue = content;
624
+ else element.insertBefore(element.ownerDocument.createTextNode(content), element.firstChild);
625
+ }
626
+ /** Changes a kept element from its previous node to its new one. */
627
+ function patchElement(element, previous, next) {
628
+ for (const name of Object.keys(previous.attrs)) if (!(name in next.attrs)) element.removeAttribute(name);
629
+ for (const [name, value] of Object.entries(next.attrs)) if (previous.attrs[name] !== value) applyAttribute(element, name, value);
630
+ const removed = previous.classes.filter((name) => !next.classes.includes(name));
631
+ const added = next.classes.filter((name) => !previous.classes.includes(name));
632
+ if (removed.length) {
633
+ element.classList.remove(...removed);
634
+ if (!element.classList.length) element.removeAttribute("class");
635
+ }
636
+ if (added.length) element.classList.add(...added);
637
+ if (!next.children.length && previous.content !== next.content) setText(element, next.content);
638
+ }
639
+ /**
640
+ * Puts the elements of a node's children in their order, from the end:
641
+ * an element moves only when it isn't before its successor already.
642
+ * Elements the scene doesn't know are skipped, so they stay after their
643
+ * previous sibling.
644
+ */
645
+ function order(parent, elements) {
646
+ const known = new Set(elements);
647
+ let successor = null;
648
+ for (let i = elements.length - 1; i >= 0; i--) {
649
+ const element = elements[i];
650
+ let following = element.nextSibling;
651
+ while (following && !known.has(following)) following = following.nextSibling;
652
+ if (element.parentNode !== parent || following !== successor) parent.insertBefore(element, successor);
653
+ successor = element;
654
+ }
655
+ }
656
+ /**
657
+ * Creates a DOM renderer that keeps a chart's SVG and changes it in place
658
+ * on every later render, instead of drawing a new one: elements that
659
+ * stand for the same thing as before stay, with only their changed
660
+ * attributes, classes and text set, elements for new nodes are created
661
+ * and elements of nodes that are gone are removed. What listeners added
662
+ * to kept elements stays, and `mount` gets the `changes` of the render.
663
+ *
664
+ * Its first render, and every render without the previous one (1.x
665
+ * charts), draws a new SVG as {@link domRenderer} does. The renderer
666
+ * keeps no state of its own, so one instance can serve many charts.
667
+ *
668
+ * @example
669
+ * ```ts
670
+ * import { lineChart } from 'chartist';
671
+ * import { liveDomRenderer } from 'chartist/live';
672
+ *
673
+ * const labels = ['Mo', 'Tu', 'We', 'Th', 'Fr'];
674
+ * const values = [3, 5, 4, 6, 5];
675
+ * const chart = lineChart(
676
+ * '#chart',
677
+ * { labels, series: [values] },
678
+ * // Points and labels follow their day as the window slides.
679
+ * { renderer: liveDomRenderer({ key: 'label' }) }
680
+ * );
681
+ *
682
+ * let day = 0;
683
+ * setInterval(() => {
684
+ * labels.push(`Day ${++day}`);
685
+ * labels.shift();
686
+ * values.push(Math.round(Math.random() * 10));
687
+ * values.shift();
688
+ * chart.update({ labels: [...labels], series: [[...values]] });
689
+ * }, 1000);
690
+ * ```
691
+ * @param options How values are identified (`key`), and update
692
+ * animations, such as `smilAnimations()`
693
+ * @returns The renderer, for `renderer` in `chart()` and the presets
694
+ * @experimental
695
+ */
696
+ function liveDomRenderer(options = {}) {
697
+ const dom = domRenderer();
698
+ const { key, animations } = options;
699
+ const valueKey = valueKeyOf(key);
700
+ return {
701
+ ...dom,
702
+ render(scene, container, update) {
703
+ const svg = update?.previous.elementOf(update.previousScene);
704
+ if (!update || !svg || svg.parentNode !== container || scene.name !== update.previousScene.name) return dom.render(scene, container);
705
+ const { labels, previousLabels } = update;
706
+ const document = container.ownerDocument;
707
+ const { changes, matched } = matchScenes(update.previousScene, scene, {
708
+ key,
709
+ labels,
710
+ previousLabels
711
+ });
712
+ const elements = /* @__PURE__ */ new WeakMap();
713
+ const created = (current, element) => elements.set(current, element);
714
+ const removed = /* @__PURE__ */ new Set();
715
+ for (const change of changes) if (change.op === "remove") removed.add(change.node);
716
+ const patch = (previous, next, element, html) => {
717
+ elements.set(next, element);
718
+ patchElement(element, previous, next);
719
+ for (const child of previous.children) if (removed.has(child)) update.previous.elementOf(child)?.remove();
720
+ const inner = html || next.name === "foreignObject";
721
+ order(element, next.children.map((child) => {
722
+ const kept = matched.get(child);
723
+ const childElement = kept && update.previous.elementOf(kept);
724
+ if (kept && childElement) {
725
+ patch(kept, child, childElement, inner);
726
+ return childElement;
727
+ }
728
+ return renderElement(child, document, {
729
+ html: inner,
730
+ created
731
+ });
732
+ }));
733
+ };
734
+ patch(update.previousScene, scene, svg, false);
735
+ const elementOf = (node) => elements.get(node);
736
+ const rootClass = scene.classes.join(" ");
737
+ const animation = animations?.animate({
738
+ changes,
739
+ elementOf,
740
+ update,
741
+ container,
742
+ scene,
743
+ measureStyle: (property, measure) => dom.measureStyle?.(property, {
744
+ container,
745
+ rootClass,
746
+ ...measure
747
+ }),
748
+ valueKey: (value, which) => valueKey({
749
+ ...value,
750
+ label: (which === "previous" ? previousLabels : labels)[value.valueIndex]
751
+ })
752
+ });
753
+ return {
754
+ svg,
755
+ elementOf,
756
+ remove: () => svg.remove(),
757
+ changes,
758
+ patched: true,
759
+ ...animation && { animation }
760
+ };
761
+ }
762
+ };
763
+ }
764
+ //#endregion
765
+ export { diffScenes, liveDomRenderer, smilAnimations };
766
+
767
+ //# sourceMappingURL=live.js.map