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