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/README.md +4 -4
- package/dist/chartist.min.js +2 -2
- package/dist/compat.cjs +7 -0
- package/dist/compat.cjs.map +1 -1
- package/dist/compat.js +7 -0
- package/dist/compat.js.map +1 -1
- package/dist/compat.min.js +1 -1
- package/dist/compat.min.js.map +1 -1
- package/dist/compat.umd.js +1 -1
- package/dist/compat.umd.js.map +1 -1
- package/dist/index.cjs +387 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +384 -128
- package/dist/index.d.ts +384 -128
- package/dist/index.js +384 -85
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +2 -2
- package/dist/index.min.js.map +1 -1
- package/dist/index.umd.js +2 -2
- package/dist/index.umd.js.map +1 -1
- package/dist/live.cjs +770 -0
- package/dist/live.cjs.map +1 -0
- package/dist/live.d.cts +203 -0
- package/dist/live.d.ts +202 -0
- package/dist/live.js +767 -0
- package/dist/live.js.map +1 -0
- package/dist/live.min.js +4 -0
- package/dist/live.min.js.map +1 -0
- package/dist/live.umd.js +4 -0
- package/dist/live.umd.js.map +1 -0
- package/package.json +12 -1
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
|