@voila.dev/chart 5.0.113 → 5.0.114
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/core/motion/motion-store.d.ts +19 -0
- package/dist/core/motion/motion-store.d.ts.map +1 -0
- package/dist/core/motion/motion-store.js +174 -0
- package/dist/core/motion/motion-store.js.map +7 -0
- package/dist/core/motion/timing.d.ts +30 -0
- package/dist/core/motion/timing.d.ts.map +1 -0
- package/dist/core/motion/timing.js +25 -0
- package/dist/core/motion/timing.js.map +7 -0
- package/dist/core/motion/tween.d.ts +2 -0
- package/dist/core/motion/tween.d.ts.map +1 -1
- package/dist/core/motion/tween.js +1 -0
- package/dist/core/motion/tween.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +2 -2
- package/dist/react/chart.d.ts +7 -2
- package/dist/react/chart.d.ts.map +1 -1
- package/dist/react/chart.js +6 -5
- package/dist/react/chart.js.map +2 -2
- package/dist/react/use-animated-scene.d.ts +6 -5
- package/dist/react/use-animated-scene.d.ts.map +1 -1
- package/dist/react/use-animated-scene.js +24 -24
- package/dist/react/use-animated-scene.js.map +2 -2
- package/package.json +2 -1
- package/src/core/motion/motion-store.ts +226 -0
- package/src/core/motion/timing.ts +63 -0
- package/src/core/motion/tween.ts +1 -1
- package/src/index.ts +9 -0
- package/src/react/chart.tsx +13 -7
- package/src/react/use-animated-scene.ts +33 -33
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ChartTiming } from "./timing.js";
|
|
2
|
+
import type { ChartScene } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every moving number of a chart, as springs keyed by node and channel. A
|
|
5
|
+
* new scene retargets them: each channel starts from where it is painted, at
|
|
6
|
+
* the speed it has, so an update landing mid-flight bends the motion instead
|
|
7
|
+
* of kinking it. Pure and clocked from outside (milliseconds), so the SVG and
|
|
8
|
+
* Canvas renderers move identically and tests step it by hand. Hit-testing
|
|
9
|
+
* and focus never read it: they read the scene it moves towards.
|
|
10
|
+
*/
|
|
11
|
+
export interface MotionStore {
|
|
12
|
+
retarget(scene: ChartScene, now: number): void;
|
|
13
|
+
/** Jumps to `scene`: a resize, or reduced motion. */
|
|
14
|
+
snap(scene: ChartScene): void;
|
|
15
|
+
frame(now: number): ChartScene;
|
|
16
|
+
settled(now: number): boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function createMotionStore(initial: ChartScene, timing: ChartTiming): MotionStore;
|
|
19
|
+
//# sourceMappingURL=motion-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"motion-store.d.ts","sourceRoot":"","sources":["../../../src/core/motion/motion-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAgB,MAAM,yBAAyB,CAAC;AAEzE,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,iBAAiB,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,qDAAqD;IACrD,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;IAC/B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAiDD,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,WAAW,GACjB,WAAW,CA0Jb"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { spring, tween } from "@voila.dev/motion";
|
|
2
|
+
import { staggerDelay } from "./timing.js";
|
|
3
|
+
import { tweenPath } from "./tween.js";
|
|
4
|
+
const CHANNELS = {
|
|
5
|
+
rect: ["x", "y", "width", "height"],
|
|
6
|
+
circle: ["cx", "cy", "r"],
|
|
7
|
+
line: ["x1", "y1", "x2", "y2"],
|
|
8
|
+
text: ["x", "y"],
|
|
9
|
+
path: [],
|
|
10
|
+
group: []
|
|
11
|
+
};
|
|
12
|
+
const OPACITY = "opacity";
|
|
13
|
+
function flatten(nodes, into = /* @__PURE__ */ new Map()) {
|
|
14
|
+
for (const node of nodes) {
|
|
15
|
+
into.set(node.key, node);
|
|
16
|
+
if (node.kind === "group") flatten(node.children, into);
|
|
17
|
+
}
|
|
18
|
+
return into;
|
|
19
|
+
}
|
|
20
|
+
function channelKey(node, channel) {
|
|
21
|
+
return `${node}\0${channel}`;
|
|
22
|
+
}
|
|
23
|
+
function elapsed(motion, now) {
|
|
24
|
+
return Math.max(0, (now - motion.start) / 1e3);
|
|
25
|
+
}
|
|
26
|
+
function done(motion, now) {
|
|
27
|
+
return elapsed(motion, now) >= motion.generator.duration;
|
|
28
|
+
}
|
|
29
|
+
function createMotionStore(initial, timing) {
|
|
30
|
+
let target = initial;
|
|
31
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
32
|
+
const paths = /* @__PURE__ */ new Map();
|
|
33
|
+
function generator(from, to, velocity) {
|
|
34
|
+
return timing.type === "spring" ? spring({
|
|
35
|
+
from,
|
|
36
|
+
to,
|
|
37
|
+
velocity,
|
|
38
|
+
duration: timing.duration,
|
|
39
|
+
bounce: timing.bounce
|
|
40
|
+
}) : tween({
|
|
41
|
+
keyframes: [from, to],
|
|
42
|
+
duration: timing.duration,
|
|
43
|
+
ease: timing.easing
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function pathNow(key, fallback, now) {
|
|
47
|
+
const motion = paths.get(key);
|
|
48
|
+
if (!motion) return fallback;
|
|
49
|
+
return tweenPath(
|
|
50
|
+
motion.from,
|
|
51
|
+
motion.to,
|
|
52
|
+
motion.generator.at(elapsed(motion, now)).value
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function moveNode(node, before, start, now) {
|
|
56
|
+
for (const channel of CHANNELS[node.kind]) {
|
|
57
|
+
const key = channelKey(node.key, channel);
|
|
58
|
+
const running = numbers.get(key);
|
|
59
|
+
const state = running?.generator.at(elapsed(running, now));
|
|
60
|
+
const from = state?.value ?? before?.[channel];
|
|
61
|
+
const to = node[channel];
|
|
62
|
+
numbers.delete(key);
|
|
63
|
+
if (from === void 0 || from === to && !state?.velocity) continue;
|
|
64
|
+
numbers.set(key, {
|
|
65
|
+
generator: generator(from, to, state?.velocity ?? 0),
|
|
66
|
+
start
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (node.kind === "path") {
|
|
70
|
+
const from = pathNow(
|
|
71
|
+
node.key,
|
|
72
|
+
before?.kind === "path" ? before.d : void 0,
|
|
73
|
+
now
|
|
74
|
+
);
|
|
75
|
+
paths.delete(node.key);
|
|
76
|
+
if (from !== void 0 && from !== node.d) {
|
|
77
|
+
paths.set(node.key, {
|
|
78
|
+
generator: generator(0, 1, 0),
|
|
79
|
+
start,
|
|
80
|
+
from,
|
|
81
|
+
to: node.d
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const fading = channelKey(node.key, OPACITY);
|
|
86
|
+
const fade = numbers.get(fading);
|
|
87
|
+
if (before === void 0 || fade) {
|
|
88
|
+
const from = fade ? fade.generator.at(elapsed(fade, now)).value : 0;
|
|
89
|
+
numbers.set(fading, { generator: generator(from, 1, 0), start });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function paint(node, now) {
|
|
93
|
+
if (node.kind === "group") {
|
|
94
|
+
return {
|
|
95
|
+
...node,
|
|
96
|
+
children: node.children.map((child) => paint(child, now))
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const moved = {};
|
|
100
|
+
for (const channel of [...CHANNELS[node.kind], OPACITY]) {
|
|
101
|
+
const motion = numbers.get(channelKey(node.key, channel));
|
|
102
|
+
if (!motion) continue;
|
|
103
|
+
moved[channel] = motion.generator.at(elapsed(motion, now)).value;
|
|
104
|
+
}
|
|
105
|
+
const d = node.kind === "path" ? pathNow(node.key, node.d, now) : void 0;
|
|
106
|
+
const { [OPACITY]: opacity, ...geometry } = moved;
|
|
107
|
+
const next = {
|
|
108
|
+
...node,
|
|
109
|
+
...geometry,
|
|
110
|
+
...d === void 0 ? {} : { d }
|
|
111
|
+
};
|
|
112
|
+
if (opacity === void 0) return next;
|
|
113
|
+
const shown = Math.min(1, Math.max(0, opacity));
|
|
114
|
+
const painted = next;
|
|
115
|
+
return {
|
|
116
|
+
...painted,
|
|
117
|
+
paint: {
|
|
118
|
+
...painted.paint,
|
|
119
|
+
opacity: (painted.paint.opacity ?? 1) * shown
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
retarget(scene, now) {
|
|
125
|
+
const before = flatten(target.nodes);
|
|
126
|
+
const after = flatten(scene.nodes);
|
|
127
|
+
const marks = [...after.values()].filter((node) => node.role === "mark");
|
|
128
|
+
const order = new Map(marks.map((node, index) => [node.key, index]));
|
|
129
|
+
for (const key of before.keys()) {
|
|
130
|
+
if (after.has(key)) continue;
|
|
131
|
+
paths.delete(key);
|
|
132
|
+
for (const channel of [
|
|
133
|
+
...CHANNELS[before.get(key)?.kind ?? "group"] ?? [],
|
|
134
|
+
OPACITY
|
|
135
|
+
]) {
|
|
136
|
+
numbers.delete(channelKey(key, channel));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const node of after.values()) {
|
|
140
|
+
const delay = staggerDelay(
|
|
141
|
+
timing,
|
|
142
|
+
order.get(node.key) ?? 0,
|
|
143
|
+
marks.length
|
|
144
|
+
);
|
|
145
|
+
moveNode(node, before.get(node.key), now + delay * 1e3, now);
|
|
146
|
+
}
|
|
147
|
+
target = scene;
|
|
148
|
+
},
|
|
149
|
+
snap(scene) {
|
|
150
|
+
numbers.clear();
|
|
151
|
+
paths.clear();
|
|
152
|
+
target = scene;
|
|
153
|
+
},
|
|
154
|
+
frame(now) {
|
|
155
|
+
for (const [key, motion] of numbers) {
|
|
156
|
+
if (done(motion, now)) numbers.delete(key);
|
|
157
|
+
}
|
|
158
|
+
for (const [key, motion] of paths) {
|
|
159
|
+
if (done(motion, now)) paths.delete(key);
|
|
160
|
+
}
|
|
161
|
+
if (numbers.size === 0 && paths.size === 0) return target;
|
|
162
|
+
return { ...target, nodes: target.nodes.map((node) => paint(node, now)) };
|
|
163
|
+
},
|
|
164
|
+
settled(now) {
|
|
165
|
+
return [...numbers.values(), ...paths.values()].every(
|
|
166
|
+
(motion) => done(motion, now)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
createMotionStore
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=motion-store.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/core/motion/motion-store.ts"],
|
|
4
|
+
"sourcesContent": ["import { type Generator, spring, tween } from \"@voila.dev/motion\";\nimport { type ChartTiming, staggerDelay } from \"#/core/motion/timing.ts\";\nimport { tweenPath } from \"#/core/motion/tween.ts\";\nimport type { ChartScene, SceneNode } from \"#/core/types.ts\";\n\n/**\n * Every moving number of a chart, as springs keyed by node and channel. A\n * new scene retargets them: each channel starts from where it is painted, at\n * the speed it has, so an update landing mid-flight bends the motion instead\n * of kinking it. Pure and clocked from outside (milliseconds), so the SVG and\n * Canvas renderers move identically and tests step it by hand. Hit-testing\n * and focus never read it: they read the scene it moves towards.\n */\nexport interface MotionStore {\n\tretarget(scene: ChartScene, now: number): void;\n\t/** Jumps to `scene`: a resize, or reduced motion. */\n\tsnap(scene: ChartScene): void;\n\tframe(now: number): ChartScene;\n\tsettled(now: number): boolean;\n}\n\ninterface Motion {\n\treadonly generator: Generator;\n\t/** Milliseconds, the delay included. */\n\treadonly start: number;\n}\n\ninterface PathMotion extends Motion {\n\treadonly from: string;\n\treadonly to: string;\n}\n\ntype Numeric = Record<string, number>;\n\nconst CHANNELS: Record<SceneNode[\"kind\"], readonly string[]> = {\n\trect: [\"x\", \"y\", \"width\", \"height\"],\n\tcircle: [\"cx\", \"cy\", \"r\"],\n\tline: [\"x1\", \"y1\", \"x2\", \"y2\"],\n\ttext: [\"x\", \"y\"],\n\tpath: [],\n\tgroup: [],\n};\n\nconst OPACITY = \"opacity\";\n\nfunction flatten(\n\tnodes: readonly SceneNode[],\n\tinto = new Map<string, SceneNode>(),\n) {\n\tfor (const node of nodes) {\n\t\tinto.set(node.key, node);\n\t\tif (node.kind === \"group\") flatten(node.children, into);\n\t}\n\treturn into;\n}\n\nfunction channelKey(node: string, channel: string): string {\n\treturn `${node}\\u0000${channel}`;\n}\n\nfunction elapsed(motion: Motion, now: number): number {\n\treturn Math.max(0, (now - motion.start) / 1000);\n}\n\nfunction done(motion: Motion, now: number): boolean {\n\treturn elapsed(motion, now) >= motion.generator.duration;\n}\n\nexport function createMotionStore(\n\tinitial: ChartScene,\n\ttiming: ChartTiming,\n): MotionStore {\n\tlet target = initial;\n\tconst numbers = new Map<string, Motion>();\n\tconst paths = new Map<string, PathMotion>();\n\n\tfunction generator(from: number, to: number, velocity: number): Generator {\n\t\treturn timing.type === \"spring\"\n\t\t\t? spring({\n\t\t\t\t\tfrom,\n\t\t\t\t\tto,\n\t\t\t\t\tvelocity,\n\t\t\t\t\tduration: timing.duration,\n\t\t\t\t\tbounce: timing.bounce,\n\t\t\t\t})\n\t\t\t: tween({\n\t\t\t\t\tkeyframes: [from, to],\n\t\t\t\t\tduration: timing.duration,\n\t\t\t\t\tease: timing.easing,\n\t\t\t\t});\n\t}\n\n\tfunction pathNow(key: string, fallback: string | undefined, now: number) {\n\t\tconst motion = paths.get(key);\n\t\tif (!motion) return fallback;\n\t\treturn tweenPath(\n\t\t\tmotion.from,\n\t\t\tmotion.to,\n\t\t\tmotion.generator.at(elapsed(motion, now)).value,\n\t\t);\n\t}\n\n\tfunction moveNode(\n\t\tnode: SceneNode,\n\t\tbefore: SceneNode | undefined,\n\t\tstart: number,\n\t\tnow: number,\n\t) {\n\t\tfor (const channel of CHANNELS[node.kind]) {\n\t\t\tconst key = channelKey(node.key, channel);\n\t\t\tconst running = numbers.get(key);\n\t\t\tconst state = running?.generator.at(elapsed(running, now));\n\t\t\tconst from =\n\t\t\t\tstate?.value ?? (before as unknown as Numeric | undefined)?.[channel];\n\t\t\tconst to = (node as unknown as Numeric)[channel] as number;\n\t\t\tnumbers.delete(key);\n\t\t\tif (from === undefined || (from === to && !state?.velocity)) continue;\n\t\t\tnumbers.set(key, {\n\t\t\t\tgenerator: generator(from, to, state?.velocity ?? 0),\n\t\t\t\tstart,\n\t\t\t});\n\t\t}\n\t\tif (node.kind === \"path\") {\n\t\t\tconst from = pathNow(\n\t\t\t\tnode.key,\n\t\t\t\tbefore?.kind === \"path\" ? before.d : undefined,\n\t\t\t\tnow,\n\t\t\t);\n\t\t\tpaths.delete(node.key);\n\t\t\tif (from !== undefined && from !== node.d) {\n\t\t\t\tpaths.set(node.key, {\n\t\t\t\t\tgenerator: generator(0, 1, 0),\n\t\t\t\t\tstart,\n\t\t\t\t\tfrom,\n\t\t\t\t\tto: node.d,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tconst fading = channelKey(node.key, OPACITY);\n\t\tconst fade = numbers.get(fading);\n\t\tif (before === undefined || fade) {\n\t\t\tconst from = fade ? fade.generator.at(elapsed(fade, now)).value : 0;\n\t\t\tnumbers.set(fading, { generator: generator(from, 1, 0), start });\n\t\t}\n\t}\n\n\tfunction paint(node: SceneNode, now: number): SceneNode {\n\t\tif (node.kind === \"group\") {\n\t\t\treturn {\n\t\t\t\t...node,\n\t\t\t\tchildren: node.children.map((child) => paint(child, now)),\n\t\t\t};\n\t\t}\n\t\tconst moved: Numeric = {};\n\t\tfor (const channel of [...CHANNELS[node.kind], OPACITY]) {\n\t\t\tconst motion = numbers.get(channelKey(node.key, channel));\n\t\t\tif (!motion) continue;\n\t\t\tmoved[channel] = motion.generator.at(elapsed(motion, now)).value;\n\t\t}\n\t\tconst d = node.kind === \"path\" ? pathNow(node.key, node.d, now) : undefined;\n\t\tconst { [OPACITY]: opacity, ...geometry } = moved;\n\t\tconst next = {\n\t\t\t...node,\n\t\t\t...geometry,\n\t\t\t...(d === undefined ? {} : { d }),\n\t\t} as SceneNode;\n\t\tif (opacity === undefined) return next;\n\t\tconst shown = Math.min(1, Math.max(0, opacity));\n\t\tconst painted = next as Exclude<SceneNode, { kind: \"group\" }>;\n\t\treturn {\n\t\t\t...painted,\n\t\t\tpaint: {\n\t\t\t\t...painted.paint,\n\t\t\t\topacity: (painted.paint.opacity ?? 1) * shown,\n\t\t\t},\n\t\t} as SceneNode;\n\t}\n\n\treturn {\n\t\tretarget(scene, now) {\n\t\t\tconst before = flatten(target.nodes);\n\t\t\tconst after = flatten(scene.nodes);\n\t\t\tconst marks = [...after.values()].filter((node) => node.role === \"mark\");\n\t\t\tconst order = new Map(marks.map((node, index) => [node.key, index]));\n\t\t\tfor (const key of before.keys()) {\n\t\t\t\tif (after.has(key)) continue;\n\t\t\t\tpaths.delete(key);\n\t\t\t\tfor (const channel of [\n\t\t\t\t\t...(CHANNELS[before.get(key)?.kind ?? \"group\"] ?? []),\n\t\t\t\t\tOPACITY,\n\t\t\t\t]) {\n\t\t\t\t\tnumbers.delete(channelKey(key, channel));\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const node of after.values()) {\n\t\t\t\tconst delay = staggerDelay(\n\t\t\t\t\ttiming,\n\t\t\t\t\torder.get(node.key) ?? 0,\n\t\t\t\t\tmarks.length,\n\t\t\t\t);\n\t\t\t\tmoveNode(node, before.get(node.key), now + delay * 1000, now);\n\t\t\t}\n\t\t\ttarget = scene;\n\t\t},\n\t\tsnap(scene) {\n\t\t\tnumbers.clear();\n\t\t\tpaths.clear();\n\t\t\ttarget = scene;\n\t\t},\n\t\tframe(now) {\n\t\t\tfor (const [key, motion] of numbers) {\n\t\t\t\tif (done(motion, now)) numbers.delete(key);\n\t\t\t}\n\t\t\tfor (const [key, motion] of paths) {\n\t\t\t\tif (done(motion, now)) paths.delete(key);\n\t\t\t}\n\t\t\tif (numbers.size === 0 && paths.size === 0) return target;\n\t\t\treturn { ...target, nodes: target.nodes.map((node) => paint(node, now)) };\n\t\t},\n\t\tsettled(now) {\n\t\t\treturn [...numbers.values(), ...paths.values()].every((motion) =>\n\t\t\t\tdone(motion, now),\n\t\t\t);\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EasingName } from "@voila.dev/motion";
|
|
2
|
+
/** How a data update moves: what `<Chart animate>` accepts beyond `true`, `false` and a duration. */
|
|
3
|
+
export type ChartAnimation = {
|
|
4
|
+
readonly type?: "spring";
|
|
5
|
+
/** Perceived duration, in milliseconds. */
|
|
6
|
+
readonly duration?: number;
|
|
7
|
+
/** 0 never overshoots (the default: a bar past its value shows a number the data doesn't have); up to 0.5. */
|
|
8
|
+
readonly bounce?: number;
|
|
9
|
+
/** Milliseconds between marks in focus order, capped at 300 ms in all. */
|
|
10
|
+
readonly stagger?: number;
|
|
11
|
+
} | {
|
|
12
|
+
readonly type: "tween";
|
|
13
|
+
readonly duration?: number;
|
|
14
|
+
readonly easing?: EasingName;
|
|
15
|
+
readonly stagger?: number;
|
|
16
|
+
};
|
|
17
|
+
export interface ChartTiming {
|
|
18
|
+
readonly type: "spring" | "tween";
|
|
19
|
+
/** Seconds, as the motion package counts them. */
|
|
20
|
+
readonly duration: number;
|
|
21
|
+
readonly bounce: number;
|
|
22
|
+
readonly easing: EasingName;
|
|
23
|
+
/** Seconds between neighbours. */
|
|
24
|
+
readonly stagger: number;
|
|
25
|
+
}
|
|
26
|
+
/** The timing an `animate` prop means, or `null` to snap. */
|
|
27
|
+
export declare function chartTiming(animate: boolean | number | ChartAnimation): ChartTiming | null;
|
|
28
|
+
/** The delay of the `index`-th of `count` marks: the gap shrinks so the last one never waits more than 300 ms. */
|
|
29
|
+
export declare function staggerDelay(timing: ChartTiming, index: number, count: number): number;
|
|
30
|
+
//# sourceMappingURL=timing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../../../src/core/motion/timing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,qGAAqG;AACrG,MAAM,MAAM,cAAc,GACvB;IACA,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;IACzB,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,8GAA8G;IAC9G,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CACzB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEL,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IAClC,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAKD,6DAA6D;AAC7D,wBAAgB,WAAW,CAC1B,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,cAAc,GACxC,WAAW,GAAG,IAAI,CAgBpB;AAED,kHAAkH;AAClH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACX,MAAM,CAGR"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const DEFAULT_DURATION = 300;
|
|
2
|
+
const MAX_STAGGER_TOTAL = 0.3;
|
|
3
|
+
function chartTiming(animate) {
|
|
4
|
+
if (animate === false || animate === 0) return null;
|
|
5
|
+
const options = typeof animate === "object" ? animate : { duration: animate === true ? DEFAULT_DURATION : animate };
|
|
6
|
+
return {
|
|
7
|
+
type: options.type ?? "spring",
|
|
8
|
+
duration: (options.duration ?? DEFAULT_DURATION) / 1e3,
|
|
9
|
+
bounce: Math.min(
|
|
10
|
+
Math.max("bounce" in options ? options.bounce ?? 0 : 0, 0),
|
|
11
|
+
0.5
|
|
12
|
+
),
|
|
13
|
+
easing: ("easing" in options ? options.easing : void 0) ?? "easeOut",
|
|
14
|
+
stagger: (options.stagger ?? 0) / 1e3
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function staggerDelay(timing, index, count) {
|
|
18
|
+
if (timing.stagger === 0 || count < 2) return 0;
|
|
19
|
+
return index * Math.min(timing.stagger, MAX_STAGGER_TOTAL / (count - 1));
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
chartTiming,
|
|
23
|
+
staggerDelay
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/core/motion/timing.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EasingName } from \"@voila.dev/motion\";\n\n/** How a data update moves: what `<Chart animate>` accepts beyond `true`, `false` and a duration. */\nexport type ChartAnimation =\n\t| {\n\t\t\treadonly type?: \"spring\";\n\t\t\t/** Perceived duration, in milliseconds. */\n\t\t\treadonly duration?: number;\n\t\t\t/** 0 never overshoots (the default: a bar past its value shows a number the data doesn't have); up to 0.5. */\n\t\t\treadonly bounce?: number;\n\t\t\t/** Milliseconds between marks in focus order, capped at 300 ms in all. */\n\t\t\treadonly stagger?: number;\n\t }\n\t| {\n\t\t\treadonly type: \"tween\";\n\t\t\treadonly duration?: number;\n\t\t\treadonly easing?: EasingName;\n\t\t\treadonly stagger?: number;\n\t };\n\nexport interface ChartTiming {\n\treadonly type: \"spring\" | \"tween\";\n\t/** Seconds, as the motion package counts them. */\n\treadonly duration: number;\n\treadonly bounce: number;\n\treadonly easing: EasingName;\n\t/** Seconds between neighbours. */\n\treadonly stagger: number;\n}\n\nconst DEFAULT_DURATION = 300;\nconst MAX_STAGGER_TOTAL = 0.3;\n\n/** The timing an `animate` prop means, or `null` to snap. */\nexport function chartTiming(\n\tanimate: boolean | number | ChartAnimation,\n): ChartTiming | null {\n\tif (animate === false || animate === 0) return null;\n\tconst options: ChartAnimation =\n\t\ttypeof animate === \"object\"\n\t\t\t? animate\n\t\t\t: { duration: animate === true ? DEFAULT_DURATION : animate };\n\treturn {\n\t\ttype: options.type ?? \"spring\",\n\t\tduration: (options.duration ?? DEFAULT_DURATION) / 1000,\n\t\tbounce: Math.min(\n\t\t\tMath.max(\"bounce\" in options ? (options.bounce ?? 0) : 0, 0),\n\t\t\t0.5,\n\t\t),\n\t\teasing: (\"easing\" in options ? options.easing : undefined) ?? \"easeOut\",\n\t\tstagger: (options.stagger ?? 0) / 1000,\n\t};\n}\n\n/** The delay of the `index`-th of `count` marks: the gap shrinks so the last one never waits more than 300 ms. */\nexport function staggerDelay(\n\ttiming: ChartTiming,\n\tindex: number,\n\tcount: number,\n): number {\n\tif (timing.stagger === 0 || count < 2) return 0;\n\treturn index * Math.min(timing.stagger, MAX_STAGGER_TOTAL / (count - 1));\n}\n"],
|
|
5
|
+
"mappings": "AA8BA,MAAM,mBAAmB;AACzB,MAAM,oBAAoB;AAGnB,SAAS,YACf,SACqB;AACrB,MAAI,YAAY,SAAS,YAAY,EAAG,QAAO;AAC/C,QAAM,UACL,OAAO,YAAY,WAChB,UACA,EAAE,UAAU,YAAY,OAAO,mBAAmB,QAAQ;AAC9D,SAAO;AAAA,IACN,MAAM,QAAQ,QAAQ;AAAA,IACtB,WAAW,QAAQ,YAAY,oBAAoB;AAAA,IACnD,QAAQ,KAAK;AAAA,MACZ,KAAK,IAAI,YAAY,UAAW,QAAQ,UAAU,IAAK,GAAG,CAAC;AAAA,MAC3D;AAAA,IACD;AAAA,IACA,SAAS,YAAY,UAAU,QAAQ,SAAS,WAAc;AAAA,IAC9D,UAAU,QAAQ,WAAW,KAAK;AAAA,EACnC;AACD;AAGO,SAAS,aACf,QACA,OACA,OACS;AACT,MAAI,OAAO,YAAY,KAAK,QAAQ,EAAG,QAAO;AAC9C,SAAO,QAAQ,KAAK,IAAI,OAAO,SAAS,qBAAqB,QAAQ,EAAE;AACxE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ChartScene } from "../types.js";
|
|
2
|
+
/** Two paths can tween when only their numbers differ. */
|
|
3
|
+
export declare function tweenPath(from: string, to: string, t: number): string;
|
|
2
4
|
/** `to`, `t` of the way from `from` (0 is `from`, 1 is `to`). */
|
|
3
5
|
export declare function tweenScene(from: ChartScene, to: ChartScene, t: number): ChartScene;
|
|
4
6
|
/** Fast out, slow in: the motion settles rather than stops. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tween.d.ts","sourceRoot":"","sources":["../../../src/core/motion/tween.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,UAAU,EAAa,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"tween.d.ts","sourceRoot":"","sources":["../../../src/core/motion/tween.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,UAAU,EAAa,MAAM,iBAAiB,CAAC;AAoBzE,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAiBrE;AAyED,iEAAiE;AACjE,wBAAgB,UAAU,CACzB,IAAI,EAAE,UAAU,EAChB,EAAE,EAAE,UAAU,EACd,CAAC,EAAE,MAAM,GACP,UAAU,CASZ;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9C"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/core/motion/tween.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ChartPaint, ChartScene, SceneNode } from \"#/core/types.ts\";\n\n/**\n * A scene part of the way from one scene to the next, keyed by node: what an\n * animated update draws on each frame. It lives in the core so the SVG and\n * Canvas renderers animate identically, and never feeds hit-testing or\n * focus: those always read the scene being animated towards.\n *\n * A node present on both sides moves; a new one fades in; one that left is\n * gone at once (fading out a bar the data no longer has would show a value\n * that is not true any more). A path tweens point by point when both shapes\n * share their commands, and swaps at the end when they do not.\n */\n\nconst NUMBER = /-?\\d*\\.?\\d+(?:e[-+]?\\d+)?/gi;\n\nfunction mix(from: number, to: number, t: number): number {\n\treturn from + (to - from) * t;\n}\n\n/** Two paths can tween when only their numbers differ. */\
|
|
5
|
-
"mappings": "AAcA,MAAM,SAAS;AAEf,SAAS,IAAI,MAAc,IAAY,GAAmB;AACzD,SAAO,QAAQ,KAAK,QAAQ;AAC7B;
|
|
4
|
+
"sourcesContent": ["import type { ChartPaint, ChartScene, SceneNode } from \"#/core/types.ts\";\n\n/**\n * A scene part of the way from one scene to the next, keyed by node: what an\n * animated update draws on each frame. It lives in the core so the SVG and\n * Canvas renderers animate identically, and never feeds hit-testing or\n * focus: those always read the scene being animated towards.\n *\n * A node present on both sides moves; a new one fades in; one that left is\n * gone at once (fading out a bar the data no longer has would show a value\n * that is not true any more). A path tweens point by point when both shapes\n * share their commands, and swaps at the end when they do not.\n */\n\nconst NUMBER = /-?\\d*\\.?\\d+(?:e[-+]?\\d+)?/gi;\n\nfunction mix(from: number, to: number, t: number): number {\n\treturn from + (to - from) * t;\n}\n\n/** Two paths can tween when only their numbers differ. */\nexport function tweenPath(from: string, to: string, t: number): string {\n\tconst fromNumbers = from.match(NUMBER);\n\tconst toNumbers = to.match(NUMBER);\n\tif (\n\t\tfromNumbers === null ||\n\t\ttoNumbers === null ||\n\t\tfromNumbers.length !== toNumbers.length ||\n\t\tfrom.replace(NUMBER, \"#\") !== to.replace(NUMBER, \"#\")\n\t) {\n\t\treturn t < 1 ? from : to;\n\t}\n\tlet index = 0;\n\treturn to.replace(NUMBER, () => {\n\t\tconst value = mix(Number(fromNumbers[index]), Number(toNumbers[index]), t);\n\t\tindex += 1;\n\t\treturn String(Math.round(value * 100) / 100);\n\t});\n}\n\nfunction fadeIn<TPaint extends ChartPaint>(paint: TPaint, t: number): TPaint {\n\treturn { ...paint, opacity: (paint.opacity ?? 1) * t };\n}\n\nfunction tweenNode(\n\tfrom: SceneNode | undefined,\n\tto: SceneNode,\n\tt: number,\n): SceneNode {\n\tif (from === undefined || from.kind !== to.kind) {\n\t\treturn to.kind === \"group\"\n\t\t\t? {\n\t\t\t\t\t...to,\n\t\t\t\t\tchildren: to.children.map((child) => tweenNode(undefined, child, t)),\n\t\t\t\t}\n\t\t\t: { ...to, paint: fadeIn(to.paint, t) };\n\t}\n\tswitch (to.kind) {\n\t\tcase \"group\": {\n\t\t\tconst before = new Map(\n\t\t\t\t(from as typeof to).children.map((child) => [child.key, child]),\n\t\t\t);\n\t\t\treturn {\n\t\t\t\t...to,\n\t\t\t\tchildren: to.children.map((child) =>\n\t\t\t\t\ttweenNode(before.get(child.key), child, t),\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\t\tcase \"rect\": {\n\t\t\tconst previous = from as typeof to;\n\t\t\treturn {\n\t\t\t\t...to,\n\t\t\t\tx: mix(previous.x, to.x, t),\n\t\t\t\ty: mix(previous.y, to.y, t),\n\t\t\t\twidth: mix(previous.width, to.width, t),\n\t\t\t\theight: mix(previous.height, to.height, t),\n\t\t\t};\n\t\t}\n\t\tcase \"circle\": {\n\t\t\tconst previous = from as typeof to;\n\t\t\treturn {\n\t\t\t\t...to,\n\t\t\t\tcx: mix(previous.cx, to.cx, t),\n\t\t\t\tcy: mix(previous.cy, to.cy, t),\n\t\t\t\tr: mix(previous.r, to.r, t),\n\t\t\t};\n\t\t}\n\t\tcase \"line\": {\n\t\t\tconst previous = from as typeof to;\n\t\t\treturn {\n\t\t\t\t...to,\n\t\t\t\tx1: mix(previous.x1, to.x1, t),\n\t\t\t\ty1: mix(previous.y1, to.y1, t),\n\t\t\t\tx2: mix(previous.x2, to.x2, t),\n\t\t\t\ty2: mix(previous.y2, to.y2, t),\n\t\t\t};\n\t\t}\n\t\tcase \"text\": {\n\t\t\tconst previous = from as typeof to;\n\t\t\treturn {\n\t\t\t\t...to,\n\t\t\t\tx: mix(previous.x, to.x, t),\n\t\t\t\ty: mix(previous.y, to.y, t),\n\t\t\t};\n\t\t}\n\t\tcase \"path\":\n\t\t\treturn { ...to, d: tweenPath((from as typeof to).d, to.d, t) };\n\t}\n}\n\n/** `to`, `t` of the way from `from` (0 is `from`, 1 is `to`). */\nexport function tweenScene(\n\tfrom: ChartScene,\n\tto: ChartScene,\n\tt: number,\n): ChartScene {\n\tif (t >= 1) {\n\t\treturn to;\n\t}\n\tconst before = new Map(from.nodes.map((node) => [node.key, node]));\n\treturn {\n\t\t...to,\n\t\tnodes: to.nodes.map((node) => tweenNode(before.get(node.key), node, t)),\n\t};\n}\n\n/** Fast out, slow in: the motion settles rather than stops. */\nexport function easeOutCubic(t: number): number {\n\treturn 1 - (1 - t) ** 3;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export { boxSummary, kernelDensity, linearFit, quantile, } from "./core/marks/st
|
|
|
22
22
|
export { type WaffleOptions, waffleY } from "./core/marks/stats/waffle.js";
|
|
23
23
|
export { type TextOptions, text } from "./core/marks/text.js";
|
|
24
24
|
export { type TickOptions, tickX, tickY } from "./core/marks/tick.js";
|
|
25
|
+
export { createMotionStore, type MotionStore, } from "./core/motion/motion-store.js";
|
|
26
|
+
export { type ChartAnimation, type ChartTiming, chartTiming, } from "./core/motion/timing.js";
|
|
25
27
|
export { easeOutCubic, tweenScene } from "./core/motion/tween.js";
|
|
26
28
|
export { type ChartFocusStop, findNearest, focusStops, } from "./core/nearest.js";
|
|
27
29
|
export { estimateTextWidth } from "./core/text.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,SAAS,EACd,WAAW,GACX,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,KAAK,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAAE,KAAK,WAAW,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,KAAK,UAAU,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EACN,KAAK,UAAU,EACf,GAAG,EACH,KAAK,EACL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,EACL,SAAS,GACT,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EACN,KAAK,iBAAiB,EACtB,WAAW,GACX,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACN,KAAK,UAAU,EACf,IAAI,EACJ,KAAK,gBAAgB,EACrB,SAAS,EACT,OAAO,GACP,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EACN,KAAK,gBAAgB,EACrB,SAAS,GACT,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACN,KAAK,iBAAiB,EACtB,WAAW,GACX,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACN,UAAU,EACV,aAAa,EACb,SAAS,EACT,QAAQ,GACR,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,KAAK,aAAa,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,EAAE,KAAK,WAAW,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EACN,KAAK,cAAc,EACnB,WAAW,EACX,UAAU,GACV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,mBAAmB,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,12 @@ import {
|
|
|
43
43
|
import { waffleY } from "./core/marks/stats/waffle.js";
|
|
44
44
|
import { text } from "./core/marks/text.js";
|
|
45
45
|
import { tickX, tickY } from "./core/marks/tick.js";
|
|
46
|
+
import {
|
|
47
|
+
createMotionStore
|
|
48
|
+
} from "./core/motion/motion-store.js";
|
|
49
|
+
import {
|
|
50
|
+
chartTiming
|
|
51
|
+
} from "./core/motion/timing.js";
|
|
46
52
|
import { easeOutCubic, tweenScene } from "./core/motion/tween.js";
|
|
47
53
|
import {
|
|
48
54
|
findNearest,
|
|
@@ -60,7 +66,9 @@ export {
|
|
|
60
66
|
boxSummary,
|
|
61
67
|
boxY,
|
|
62
68
|
cell,
|
|
69
|
+
chartTiming,
|
|
63
70
|
compileChart,
|
|
71
|
+
createMotionStore,
|
|
64
72
|
defineChart,
|
|
65
73
|
differenceY,
|
|
66
74
|
dodgeY,
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export { type CompileOptions, compileChart } from \"#/core/compile.ts\";\nexport {\n\ttype ChartAxisOptions,\n\ttype ChartBuildContext,\n\ttype ChartColorOptions,\n\ttype ChartDefinition,\n\ttype ChartFacet,\n\ttype ChartFacetOptions,\n\ttype ChartPositionScaleOptions,\n\ttype ChartSpec,\n\tdefineChart,\n} from \"#/core/define-chart.ts\";\nexport { facet } from \"#/core/facet.ts\";\nexport { formatDate, formatNumber, formatValue } from \"#/core/format.ts\";\nexport { type AreaOptions, areaX, areaY } from \"#/core/marks/area.ts\";\nexport { type BarOptions, barX, barY } from \"#/core/marks/bar.ts\";\nexport { type CellOptions, cell } from \"#/core/marks/cell.ts\";\nexport { type DotOptions, dot } from \"#/core/marks/dot.ts\";\nexport { type FunnelOptions, funnel } from \"#/core/marks/funnel.ts\";\nexport { type LineOptions, lineX, lineY } from \"#/core/marks/line.ts\";\nexport {\n\ttype ArcOptions,\n\tarc,\n\tdonut,\n\ttype RadarOptions,\n\ttype RadialBarOptions,\n\tradar,\n\tradialBar,\n} from \"#/core/marks/polar.ts\";\nexport { type RectOptions, rectY } from \"#/core/marks/rect.ts\";\nexport { type RuleOptions, ruleX, ruleY } from \"#/core/marks/rule.ts\";\nexport type { ChartMarkOptions } from \"#/core/marks/shared.ts\";\nexport {\n\ttype DifferenceOptions,\n\tdifferenceY,\n} from \"#/core/marks/stats/difference.ts\";\nexport {\n\ttype BoxOptions,\n\tboxY,\n\ttype RidgelineOptions,\n\tridgeline,\n\tviolinY,\n} from \"#/core/marks/stats/distribution.ts\";\nexport { type DodgeOptions, dodgeY } from \"#/core/marks/stats/dodge.ts\";\nexport { type HexbinOptions, hexbin } from \"#/core/marks/stats/hexbin.ts\";\nexport {\n\ttype HistogramOptions,\n\thistogram,\n} from \"#/core/marks/stats/histogram.ts\";\nexport {\n\ttype RegressionOptions,\n\tregressionY,\n} from \"#/core/marks/stats/regression.ts\";\nexport {\n\tboxSummary,\n\tkernelDensity,\n\tlinearFit,\n\tquantile,\n} from \"#/core/marks/stats/statistics.ts\";\nexport { type WaffleOptions, waffleY } from \"#/core/marks/stats/waffle.ts\";\nexport { type TextOptions, text } from \"#/core/marks/text.ts\";\nexport { type TickOptions, tickX, tickY } from \"#/core/marks/tick.ts\";\nexport { easeOutCubic, tweenScene } from \"#/core/motion/tween.ts\";\nexport {\n\ttype ChartFocusStop,\n\tfindNearest,\n\tfocusStops,\n} from \"#/core/nearest.ts\";\nexport { estimateTextWidth } from \"#/core/text.ts\";\nexport { DEFAULT_THEME } from \"#/core/theme.ts\";\nexport type * from \"#/core/types.ts\";\n"],
|
|
5
|
-
"mappings": "AAAA,SAA8B,oBAAoB;AAClD;AAAA,EASC;AAAA,OACM;AACP,SAAS,aAAa;AACtB,SAAS,YAAY,cAAc,mBAAmB;AACtD,SAA2B,OAAO,aAAa;AAC/C,SAA0B,MAAM,YAAY;AAC5C,SAA2B,YAAY;AACvC,SAA0B,WAAW;AACrC,SAA6B,cAAc;AAC3C,SAA2B,OAAO,aAAa;AAC/C;AAAA,EAEC;AAAA,EACA;AAAA,EAGA;AAAA,EACA;AAAA,OACM;AACP,SAA2B,aAAa;AACxC,SAA2B,OAAO,aAAa;AAE/C;AAAA,EAEC;AAAA,OACM;AACP;AAAA,EAEC;AAAA,EAEA;AAAA,EACA;AAAA,OACM;AACP,SAA4B,cAAc;AAC1C,SAA6B,cAAc;AAC3C;AAAA,EAEC;AAAA,OACM;AACP;AAAA,EAEC;AAAA,OACM;AACP;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAA6B,eAAe;AAC5C,SAA2B,YAAY;AACvC,SAA2B,OAAO,aAAa;AAC/C,SAAS,cAAc,kBAAkB;AACzC;AAAA,EAEC;AAAA,EACA;AAAA,OACM;AACP,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;",
|
|
4
|
+
"sourcesContent": ["export { type CompileOptions, compileChart } from \"#/core/compile.ts\";\nexport {\n\ttype ChartAxisOptions,\n\ttype ChartBuildContext,\n\ttype ChartColorOptions,\n\ttype ChartDefinition,\n\ttype ChartFacet,\n\ttype ChartFacetOptions,\n\ttype ChartPositionScaleOptions,\n\ttype ChartSpec,\n\tdefineChart,\n} from \"#/core/define-chart.ts\";\nexport { facet } from \"#/core/facet.ts\";\nexport { formatDate, formatNumber, formatValue } from \"#/core/format.ts\";\nexport { type AreaOptions, areaX, areaY } from \"#/core/marks/area.ts\";\nexport { type BarOptions, barX, barY } from \"#/core/marks/bar.ts\";\nexport { type CellOptions, cell } from \"#/core/marks/cell.ts\";\nexport { type DotOptions, dot } from \"#/core/marks/dot.ts\";\nexport { type FunnelOptions, funnel } from \"#/core/marks/funnel.ts\";\nexport { type LineOptions, lineX, lineY } from \"#/core/marks/line.ts\";\nexport {\n\ttype ArcOptions,\n\tarc,\n\tdonut,\n\ttype RadarOptions,\n\ttype RadialBarOptions,\n\tradar,\n\tradialBar,\n} from \"#/core/marks/polar.ts\";\nexport { type RectOptions, rectY } from \"#/core/marks/rect.ts\";\nexport { type RuleOptions, ruleX, ruleY } from \"#/core/marks/rule.ts\";\nexport type { ChartMarkOptions } from \"#/core/marks/shared.ts\";\nexport {\n\ttype DifferenceOptions,\n\tdifferenceY,\n} from \"#/core/marks/stats/difference.ts\";\nexport {\n\ttype BoxOptions,\n\tboxY,\n\ttype RidgelineOptions,\n\tridgeline,\n\tviolinY,\n} from \"#/core/marks/stats/distribution.ts\";\nexport { type DodgeOptions, dodgeY } from \"#/core/marks/stats/dodge.ts\";\nexport { type HexbinOptions, hexbin } from \"#/core/marks/stats/hexbin.ts\";\nexport {\n\ttype HistogramOptions,\n\thistogram,\n} from \"#/core/marks/stats/histogram.ts\";\nexport {\n\ttype RegressionOptions,\n\tregressionY,\n} from \"#/core/marks/stats/regression.ts\";\nexport {\n\tboxSummary,\n\tkernelDensity,\n\tlinearFit,\n\tquantile,\n} from \"#/core/marks/stats/statistics.ts\";\nexport { type WaffleOptions, waffleY } from \"#/core/marks/stats/waffle.ts\";\nexport { type TextOptions, text } from \"#/core/marks/text.ts\";\nexport { type TickOptions, tickX, tickY } from \"#/core/marks/tick.ts\";\nexport {\n\tcreateMotionStore,\n\ttype MotionStore,\n} from \"#/core/motion/motion-store.ts\";\nexport {\n\ttype ChartAnimation,\n\ttype ChartTiming,\n\tchartTiming,\n} from \"#/core/motion/timing.ts\";\nexport { easeOutCubic, tweenScene } from \"#/core/motion/tween.ts\";\nexport {\n\ttype ChartFocusStop,\n\tfindNearest,\n\tfocusStops,\n} from \"#/core/nearest.ts\";\nexport { estimateTextWidth } from \"#/core/text.ts\";\nexport { DEFAULT_THEME } from \"#/core/theme.ts\";\nexport type * from \"#/core/types.ts\";\n"],
|
|
5
|
+
"mappings": "AAAA,SAA8B,oBAAoB;AAClD;AAAA,EASC;AAAA,OACM;AACP,SAAS,aAAa;AACtB,SAAS,YAAY,cAAc,mBAAmB;AACtD,SAA2B,OAAO,aAAa;AAC/C,SAA0B,MAAM,YAAY;AAC5C,SAA2B,YAAY;AACvC,SAA0B,WAAW;AACrC,SAA6B,cAAc;AAC3C,SAA2B,OAAO,aAAa;AAC/C;AAAA,EAEC;AAAA,EACA;AAAA,EAGA;AAAA,EACA;AAAA,OACM;AACP,SAA2B,aAAa;AACxC,SAA2B,OAAO,aAAa;AAE/C;AAAA,EAEC;AAAA,OACM;AACP;AAAA,EAEC;AAAA,EAEA;AAAA,EACA;AAAA,OACM;AACP,SAA4B,cAAc;AAC1C,SAA6B,cAAc;AAC3C;AAAA,EAEC;AAAA,OACM;AACP;AAAA,EAEC;AAAA,OACM;AACP;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAA6B,eAAe;AAC5C,SAA2B,YAAY;AACvC,SAA2B,OAAO,aAAa;AAC/C;AAAA,EACC;AAAA,OAEM;AACP;AAAA,EAGC;AAAA,OACM;AACP,SAAS,cAAc,kBAAkB;AACzC;AAAA,EAEC;AAAA,EACA;AAAA,OACM;AACP,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/react/chart.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { ChartDefinition } from "../core/define-chart.js";
|
|
3
|
+
import { type ChartAnimation } from "../core/motion/timing.js";
|
|
3
4
|
import type { ChartPoint } from "../core/types.js";
|
|
4
5
|
import { type ChartBrush } from "./brush-contract.js";
|
|
5
6
|
import { type ChartTooltipRenderProps } from "./chart-tooltip.js";
|
|
@@ -23,8 +24,12 @@ interface Props extends Omit<React.ComponentProps<"div">, "children" | "onSelect
|
|
|
23
24
|
readonly legend?: boolean;
|
|
24
25
|
/** The visually hidden table of the values. On by default; turn it off only when the page shows the same table. */
|
|
25
26
|
readonly dataTable?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* How a data update moves: `true` (a spring with no bounce), a perceived
|
|
29
|
+
* duration in milliseconds, or `{ duration, bounce, stagger }` and
|
|
30
|
+
* `{ type: "tween", easing }`. `false` snaps; reduced motion always does.
|
|
31
|
+
*/
|
|
32
|
+
readonly animate?: boolean | number | ChartAnimation;
|
|
28
33
|
/**
|
|
29
34
|
* Zoom a continuous x: the wheel while the chart has focus, + and −,
|
|
30
35
|
* Shift and the arrows to pan, a drag to pan when zoomed, 0 to reset.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart.d.ts","sourceRoot":"","sources":["../../src/react/chart.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"chart.d.ts","sourceRoot":"","sources":["../../src/react/chart.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,KAAK,cAAc,EAAe,MAAM,yBAAyB,CAAC;AAG3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EACN,KAAK,UAAU,EAGf,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGN,KAAK,uBAAuB,EAC5B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA4BzD,UAAU,KACT,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;IAC5E,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,sFAAsF;IACtF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,kGAAkG;IAClG,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EACd,OAAO,GACP,CAAC,CAAC,KAAK,EAAE,uBAAuB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IACzD,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,mHAAmH;IACnH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,cAAc,CAAC;IACrD;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,iHAAiH;IACjH,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAC;CACvD;AAOD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,EACrB,UAAU,EACV,SAAS,EACT,eAAe,EACf,MAAuB,EACvB,WAAW,EACX,YAAoC,EACpC,QAAQ,EAAE,QAAsB,EAChC,OAAc,EACd,MAAM,EACN,SAAgB,EAChB,OAAc,EACd,IAAY,EACZ,KAAK,EAAE,aAAa,EACpB,aAAa,EACb,QAAQ,EACR,KAAK,EACL,GAAG,KAAK,EACR,EAAE,KAAK,qBA0XP"}
|
package/dist/react/chart.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { compileChart } from "../core/compile.js";
|
|
4
|
+
import { chartTiming } from "../core/motion/timing.js";
|
|
4
5
|
import { findNearest, focusStops } from "../core/nearest.js";
|
|
5
6
|
import { toNumber } from "../core/scales/continuous.js";
|
|
6
7
|
import {
|
|
@@ -23,7 +24,6 @@ import { useChartZoom } from "./use-chart-zoom.js";
|
|
|
23
24
|
import { useTextMeasurer } from "./use-text-measurer.js";
|
|
24
25
|
const DEFAULT_HEIGHT = 300;
|
|
25
26
|
const DEFAULT_INITIAL_WIDTH = 640;
|
|
26
|
-
const DEFAULT_DURATION = 300;
|
|
27
27
|
const CHART_CSS = `
|
|
28
28
|
[data-slot="chart-surface"]:focus-visible{outline:2px solid var(--ring,Highlight);outline-offset:2px;border-radius:4px}
|
|
29
29
|
@media (forced-colors:active){
|
|
@@ -106,11 +106,12 @@ function Chart({
|
|
|
106
106
|
() => [definition, hidden],
|
|
107
107
|
[definition, hidden]
|
|
108
108
|
);
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
const animateKey = JSON.stringify(animate);
|
|
110
|
+
const timing = React.useMemo(
|
|
111
|
+
() => chartTiming(JSON.parse(animateKey)),
|
|
112
|
+
[animateKey]
|
|
113
113
|
);
|
|
114
|
+
const shown = useAnimatedScene(scene, animationTrigger, timing);
|
|
114
115
|
const messages = messagesFor(scene.locale);
|
|
115
116
|
const { active, handlers } = useChartFocus({
|
|
116
117
|
stops,
|
package/dist/react/chart.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/react/chart.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from \"react\";\nimport { compileChart } from \"#/core/compile.ts\";\nimport type { ChartDefinition } from \"#/core/define-chart.ts\";\nimport { findNearest, focusStops } from \"#/core/nearest.ts\";\nimport { toNumber } from \"#/core/scales/continuous.ts\";\nimport type { ChartPoint } from \"#/core/types.ts\";\nimport {\n\ttype ChartBrush,\n\ttype ChartBrushRange,\n\tordered,\n} from \"#/react/brush-contract.ts\";\nimport { ChartDataTable } from \"#/react/chart-data-table.tsx\";\nimport { ChartColorRamp, ChartLegend } from \"#/react/chart-legend.tsx\";\nimport {\n\tChartTooltip,\n\tChartTooltipContent,\n\ttype ChartTooltipRenderProps,\n} from \"#/react/chart-tooltip.tsx\";\nimport { FocusOverlay } from \"#/react/focus-overlay.tsx\";\nimport { messagesFor } from \"#/react/messages.ts\";\nimport type { ChartRenderer } from \"#/react/renderer.ts\";\nimport { SR_ONLY } from \"#/react/sr-only.ts\";\nimport { SvgRenderer } from \"#/react/svg-renderer.tsx\";\nimport { useAnimatedScene } from \"#/react/use-animated-scene.ts\";\nimport { useChartFocus } from \"#/react/use-chart-focus.ts\";\nimport { useChartWidth } from \"#/react/use-chart-width.ts\";\nimport { useChartZoom } from \"#/react/use-chart-zoom.ts\";\nimport { useTextMeasurer } from \"#/react/use-text-measurer.ts\";\n\nconst DEFAULT_HEIGHT = 300;\nconst DEFAULT_INITIAL_WIDTH = 640;\nconst DEFAULT_DURATION = 300;\n\n/** Focus ring, forced colours and reduced motion: what inline styles cannot say. */\nconst CHART_CSS = `\n[data-slot=\"chart-surface\"]:focus-visible{outline:2px solid var(--ring,Highlight);outline-offset:2px;border-radius:4px}\n@media (forced-colors:active){\n[data-slot=\"chart-svg\"] [data-role=\"axis\"],[data-slot=\"chart-svg\"] [data-role=\"axis-title\"]{fill:CanvasText}\n[data-slot=\"chart-svg\"] [data-role=\"grid\"]{stroke:GrayText}\n[data-slot=\"chart-focus-ring\"]{stroke:Highlight}\n[data-slot=\"chart-tooltip\"]{border-color:CanvasText}\n}\n@media (prefers-reduced-motion:no-preference){\n[data-slot=\"chart-tooltip\"]{transition:transform 80ms ease-out}\n}\n`;\n\n// `title` would put a native tooltip on the whole chart; the props carry the\n// accessible name as `ariaLabel` instead.\ninterface Props\n\textends Omit<React.ComponentProps<\"div\">, \"children\" | \"onSelect\" | \"title\"> {\n\treadonly definition: ChartDefinition;\n\t/** What the chart shows, in a sentence: the screen reader's name for it. Required. */\n\treadonly ariaLabel: string;\n\t/** The takeaway, when the label alone does not carry it. */\n\treadonly ariaDescription?: string;\n\t/** Pixels. Ignored when `aspectRatio` is set. */\n\treadonly height?: number;\n\t/** Height as a share of the width: `0.5` is twice as wide as tall. */\n\treadonly aspectRatio?: number;\n\t/** Width used on the server and for the first client render, before the container is measured. */\n\treadonly initialWidth?: number;\n\treadonly renderer?: ChartRenderer;\n\t/** `false` turns the tooltip off; a function replaces its content. */\n\treadonly tooltip?:\n\t\t| boolean\n\t\t| ((props: ChartTooltipRenderProps) => React.ReactNode);\n\t/** Shown by default when there is more than one series. */\n\treadonly legend?: boolean;\n\t/** The visually hidden table of the values. On by default; turn it off only when the page shows the same table. */\n\treadonly dataTable?: boolean;\n\t/** Tween a data update, in milliseconds; `false` snaps. Reduced motion always snaps. */\n\treadonly animate?: boolean | number;\n\t/**\n\t * Zoom a continuous x: the wheel while the chart has focus, + and \u2212,\n\t * Shift and the arrows to pan, a drag to pan when zoomed, 0 to reset.\n\t */\n\treadonly zoom?: boolean;\n\t/** Select a range of x by dragging or from the keyboard: `brushX({ onBrush })` from `@voila.dev/chart/brush`. */\n\treadonly brush?: ChartBrush;\n\treadonly onFocusChange?: (point: ChartPoint | null) => void;\n\treadonly onSelect?: (point: ChartPoint | null) => void;\n}\n\n/** A pointer that moved less than this between down and up clicked rather than dragged. */\nconst DRAG_THRESHOLD = 3;\nconst ZOOM_STEP = 0.8;\nconst PAN_SHARE = 0.1;\n\n/**\n * Draws a definition. Everything a reader needs beyond the picture comes with\n * it: keyboard focus on every value, a live region that reads the focused\n * value, a legend that toggles series, and the values as a hidden table.\n */\nexport function Chart({\n\tdefinition,\n\tariaLabel,\n\tariaDescription,\n\theight = DEFAULT_HEIGHT,\n\taspectRatio,\n\tinitialWidth = DEFAULT_INITIAL_WIDTH,\n\trenderer: Renderer = SvgRenderer,\n\ttooltip = true,\n\tlegend,\n\tdataTable = true,\n\tanimate = true,\n\tzoom = false,\n\tbrush: brushBehavior,\n\tonFocusChange,\n\tonSelect,\n\tstyle,\n\t...props\n}: Props) {\n\tconst chartId = React.useId().replaceAll(\":\", \"\");\n\tconst { ref, width, node } = useChartWidth(initialWidth);\n\tconst measureText = useTextMeasurer(node);\n\tconst [hidden, setHidden] = React.useState<ReadonlySet<string>>(\n\t\t() => new Set(),\n\t);\n\tconst chartHeight =\n\t\taspectRatio === undefined ? height : Math.round(width * aspectRatio);\n\n\tconst baseScene = React.useMemo(\n\t\t() =>\n\t\t\tcompileChart(definition, {\n\t\t\t\twidth,\n\t\t\t\theight: chartHeight,\n\t\t\t\tmeasureText,\n\t\t\t\thiddenSeries: hidden,\n\t\t\t}),\n\t\t[definition, width, chartHeight, measureText, hidden],\n\t);\n\tconst baseX = baseScene.scales.x;\n\tconst fullX = React.useMemo<readonly [number, number] | undefined>(\n\t\t() =>\n\t\t\tbaseX && baseX.kind !== \"band\" && baseX.kind !== \"point\"\n\t\t\t\t? [toNumber(baseX.domain[0]), toNumber(baseX.domain[1])]\n\t\t\t\t: undefined,\n\t\t[baseX],\n\t);\n\tconst viewport = useChartZoom(zoom ? fullX : undefined, definition);\n\tconst scene = React.useMemo(\n\t\t() =>\n\t\t\tviewport.domain === null\n\t\t\t\t? baseScene\n\t\t\t\t: compileChart(definition, {\n\t\t\t\t\t\twidth,\n\t\t\t\t\t\theight: chartHeight,\n\t\t\t\t\t\tmeasureText,\n\t\t\t\t\t\thiddenSeries: hidden,\n\t\t\t\t\t\txDomain: viewport.domain,\n\t\t\t\t\t}),\n\t\t[\n\t\t\tbaseScene,\n\t\t\tviewport.domain,\n\t\t\tdefinition,\n\t\t\twidth,\n\t\t\tchartHeight,\n\t\t\tmeasureText,\n\t\t\thidden,\n\t\t],\n\t);\n\tconst [brush, setBrush] = React.useState<ChartBrushRange | null>(null);\n\tconst [zoomAnnouncement, setZoomAnnouncement] = React.useState(\"\");\n\tconst drag = React.useRef<{\n\t\treadonly startX: number;\n\t\treadonly domain: readonly [number, number] | null;\n\t\tmoved: boolean;\n\t} | null>(null);\n\tconst stops = React.useMemo(() => focusStops(scene), [scene]);\n\tconst animationTrigger = React.useMemo(\n\t\t() => [definition, hidden],\n\t\t[definition, hidden],\n\t);\n\tconst shown = useAnimatedScene(\n\t\tscene,\n\t\tanimationTrigger,\n\t\tanimate === false ? 0 : animate === true ? DEFAULT_DURATION : animate,\n\t);\n\tconst messages = messagesFor(scene.locale);\n\tconst { active, handlers } = useChartFocus({\n\t\tstops,\n\t\torder: scene.focusOrder,\n\t\thitTest: (x, y) => findNearest(scene, stops, x, y),\n\t\tonFocusChange,\n\t\tonSelect,\n\t});\n\n\tconst xScale = scene.scales.x;\n\tconst xContinuous =\n\t\txScale !== undefined && xScale.kind !== \"band\" && xScale.kind !== \"point\";\n\n\tfunction updateBrush(range: ChartBrushRange | null) {\n\t\tsetBrush(range);\n\t\tbrushBehavior?.onBrush(range);\n\t}\n\n\tfunction localX(event: React.PointerEvent<HTMLElement>): number {\n\t\treturn event.clientX - event.currentTarget.getBoundingClientRect().left;\n\t}\n\n\tfunction onPointerDown(event: React.PointerEvent<HTMLDivElement>) {\n\t\tif (!brushBehavior && !(zoom && viewport.domain !== null)) {\n\t\t\treturn;\n\t\t}\n\t\tevent.currentTarget.setPointerCapture(event.pointerId);\n\t\t// A drag selects a range, not a value: the hover tooltip gives way.\n\t\thandlers.onPointerLeave();\n\t\tdrag.current = {\n\t\t\tstartX: localX(event),\n\t\t\tdomain: viewport.domain,\n\t\t\tmoved: false,\n\t\t};\n\t}\n\n\tfunction onPointerMove(event: React.PointerEvent<HTMLDivElement>) {\n\t\tconst current = drag.current;\n\t\tif (current === null || xScale === undefined) {\n\t\t\thandlers.onPointerMove(event);\n\t\t\treturn;\n\t\t}\n\t\tconst x = localX(event);\n\t\tif (Math.abs(x - current.startX) > DRAG_THRESHOLD) {\n\t\t\tcurrent.moved = true;\n\t\t}\n\t\tif (!current.moved) return;\n\t\tif (brushBehavior) {\n\t\t\tupdateBrush(\n\t\t\t\tordered(xScale, [xScale.invert(current.startX), xScale.invert(x)]),\n\t\t\t);\n\t\t} else if (current.domain !== null) {\n\t\t\tconst [from, to] = current.domain;\n\t\t\tconst perPixel = (to - from) / Math.max(1, scene.plot.width);\n\t\t\tviewport.pan(\n\t\t\t\t(current.startX - x) * perPixel -\n\t\t\t\t\t((viewport.domain?.[0] ?? from) - from),\n\t\t\t);\n\t\t}\n\t}\n\n\tfunction onPointerUp(event: React.PointerEvent<HTMLDivElement>) {\n\t\tif (\n\t\t\tdrag.current !== null &&\n\t\t\tevent.currentTarget.hasPointerCapture(event.pointerId)\n\t\t) {\n\t\t\tevent.currentTarget.releasePointerCapture(event.pointerId);\n\t\t}\n\t}\n\n\tfunction onClick(event: React.MouseEvent<HTMLDivElement>) {\n\t\tconst dragged = drag.current?.moved === true;\n\t\tdrag.current = null;\n\t\tif (!dragged) {\n\t\t\thandlers.onClick(event);\n\t\t}\n\t}\n\n\tfunction announceViewport(next: readonly [number, number] | null) {\n\t\tif (xScale === undefined) return;\n\t\tconst [from, to] = next ?? fullX ?? [0, 0];\n\t\tconst asValue = (value: number) =>\n\t\t\txScale.kind === \"time\" ? new Date(value) : value;\n\t\tsetZoomAnnouncement(\n\t\t\tmessages.showing(\n\t\t\t\txScale.format(asValue(from)),\n\t\t\t\txScale.format(asValue(to)),\n\t\t\t),\n\t\t);\n\t}\n\n\tfunction onKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n\t\tif (zoom && xContinuous && fullX !== undefined) {\n\t\t\tconst focused = active?.point.xValue;\n\t\t\tconst [from, to] = viewport.domain ?? fullX;\n\t\t\tconst center =\n\t\t\t\tfocused === undefined ? (from + to) / 2 : toNumber(focused);\n\t\t\tif (event.key === \"+\" || event.key === \"=\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.zoomAround(center, ZOOM_STEP);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.key === \"-\" || event.key === \"_\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.zoomAround(center, 1 / ZOOM_STEP);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.key === \"0\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.reset();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tevent.shiftKey &&\n\t\t\t\t(event.key === \"ArrowLeft\" || event.key === \"ArrowRight\")\n\t\t\t) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.pan(\n\t\t\t\t\t(event.key === \"ArrowRight\" ? 1 : -1) * viewport.span() * PAN_SHARE,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\thandlers.onKeyDown(event);\n\t}\n\n\t// Moving the focus says the new value; the last zoom's message gives way.\n\tconst activeKey = active?.point.key;\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: clears on a change of focused point only\n\tReact.useEffect(() => {\n\t\tsetZoomAnnouncement(\"\");\n\t}, [activeKey]);\n\n\tconst viewportKey = viewport.domain?.join(\":\") ?? \"full\";\n\tconst previousViewport = React.useRef(viewportKey);\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: announce a change of viewport, once\n\tReact.useEffect(() => {\n\t\tif (previousViewport.current !== viewportKey) {\n\t\t\tpreviousViewport.current = viewportKey;\n\t\t\tannounceViewport(viewport.domain);\n\t\t}\n\t}, [viewportKey]);\n\n\t// The wheel zooms only while the chart has focus: scrolling past a chart\n\t// must keep scrolling the page.\n\tconst zoomRef = React.useRef({\n\t\tviewport,\n\t\txScale,\n\t\tenabled: zoom && xContinuous,\n\t});\n\tzoomRef.current = { viewport, xScale, enabled: zoom && xContinuous };\n\tReact.useEffect(() => {\n\t\tif (node === null) return;\n\t\tfunction onWheel(event: WheelEvent) {\n\t\t\tconst { viewport: current, xScale: scale, enabled } = zoomRef.current;\n\t\t\tif (\n\t\t\t\t!enabled ||\n\t\t\t\tscale === undefined ||\n\t\t\t\tdocument.activeElement !== node ||\n\t\t\t\tnode === null\n\t\t\t)\n\t\t\t\treturn;\n\t\t\tevent.preventDefault();\n\t\t\tconst x = event.clientX - node.getBoundingClientRect().left;\n\t\t\tcurrent.zoomAround(\n\t\t\t\ttoNumber(scale.invert(x)),\n\t\t\t\tevent.deltaY > 0 ? 1 / ZOOM_STEP : ZOOM_STEP,\n\t\t\t);\n\t\t}\n\t\tnode.addEventListener(\"wheel\", onWheel, { passive: false });\n\t\treturn () => node.removeEventListener(\"wheel\", onWheel);\n\t}, [node]);\n\n\tconst announcement =\n\t\tactive?.source === \"keyboard\"\n\t\t\t? `${active.point.title} : ${(active.stop.points.length > 1\n\t\t\t\t\t? active.stop.points\n\t\t\t\t\t: [active.point]\n\t\t\t\t)\n\t\t\t\t\t.map((point) => `${point.seriesLabel ?? scene.yLabel} ${point.value}`)\n\t\t\t\t\t.join(\", \")}`\n\t\t\t: \"\";\n\tconst showLegend = legend ?? scene.legend.length > 1;\n\tconst descriptionId = `${chartId}-description`;\n\tconst renderTooltip =\n\t\ttypeof tooltip === \"function\" ? tooltip : ChartTooltipContent;\n\n\tfunction toggle(key: string) {\n\t\tsetHidden((current) => {\n\t\t\tconst next = new Set(current);\n\t\t\tif (!next.delete(key)) {\n\t\t\t\tnext.add(key);\n\t\t\t}\n\t\t\treturn next;\n\t\t});\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"chart\"\n\t\t\tstyle={{ display: \"grid\", gap: 8, ...style }}\n\t\t\t{...props}\n\t\t>\n\t\t\t<style href=\"voila-chart\" precedence=\"default\">\n\t\t\t\t{CHART_CSS}\n\t\t\t</style>\n\t\t\t<div\n\t\t\t\tdata-slot=\"chart-frame\"\n\t\t\t\tstyle={{ position: \"relative\", height: chartHeight }}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tref={ref}\n\t\t\t\t\trole=\"img\"\n\t\t\t\t\taria-roledescription={messages.roleDescription}\n\t\t\t\t\taria-label={ariaLabel}\n\t\t\t\t\taria-describedby={descriptionId}\n\t\t\t\t\t// The chart is one focus stop; the arrow keys move inside it, the way\n\t\t\t\t\t// a reader walks the values. role=\"img\" keeps it a single named\n\t\t\t\t\t// graphic, the live region speaks the focused value.\n\t\t\t\t\t// biome-ignore lint/a11y/noNoninteractiveTabindex: keyboard exploration of the values\n\t\t\t\t\ttabIndex={0}\n\t\t\t\t\tdata-slot=\"chart-surface\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tinset: 0,\n\t\t\t\t\t\ttouchAction: brushBehavior || zoom ? \"none\" : \"pan-y\",\n\t\t\t\t\t\t// Dragging across the plot brushes or pans; without this it\n\t\t\t\t\t\t// also selected the tick labels and the tooltip as text.\n\t\t\t\t\t\t...(brushBehavior || zoom\n\t\t\t\t\t\t\t? { userSelect: \"none\", WebkitUserSelect: \"none\" }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t}}\n\t\t\t\t\t{...handlers}\n\t\t\t\t\tonKeyDown={onKeyDown}\n\t\t\t\t\tonPointerDown={onPointerDown}\n\t\t\t\t\tonPointerMove={onPointerMove}\n\t\t\t\t\tonPointerUp={onPointerUp}\n\t\t\t\t\tonClick={onClick}\n\t\t\t\t>\n\t\t\t\t\t<Renderer scene={shown} chartId={chartId} />\n\t\t\t\t\t<FocusOverlay scene={scene} active={active} theme={scene.theme} />\n\t\t\t\t\t{tooltip !== false && active !== null ? (\n\t\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\t\tactive={active}\n\t\t\t\t\t\t\tscene={scene}\n\t\t\t\t\t\t\trender={renderTooltip}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t\t{brushBehavior && xScale ? (\n\t\t\t\t\t<brushBehavior.Layer\n\t\t\t\t\t\tscale={xScale}\n\t\t\t\t\t\tplot={scene.plot}\n\t\t\t\t\t\trange={brush}\n\t\t\t\t\t\tmessages={messages}\n\t\t\t\t\t\tonChange={updateBrush}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t\t<div id={descriptionId} style={SR_ONLY}>\n\t\t\t\t{ariaDescription ? `${ariaDescription} ` : \"\"}\n\t\t\t\t{messages.keyboardHint}\n\t\t\t\t{zoom && xContinuous ? messages.zoomHint : \"\"}\n\t\t\t</div>\n\t\t\t<div aria-live=\"polite\" aria-atomic=\"true\" style={SR_ONLY}>\n\t\t\t\t{zoomAnnouncement || announcement}\n\t\t\t</div>\n\t\t\t{viewport.domain !== null ? (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-slot=\"chart-reset-zoom\"\n\t\t\t\t\tonClick={() => viewport.reset()}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tjustifySelf: \"end\",\n\t\t\t\t\t\tborder:\n\t\t\t\t\t\t\t\"1px solid var(--border, color-mix(in oklab, currentColor 20%, transparent))\",\n\t\t\t\t\t\tborderRadius: 6,\n\t\t\t\t\t\tbackground: \"transparent\",\n\t\t\t\t\t\tcolor: \"inherit\",\n\t\t\t\t\t\tfont: \"inherit\",\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tpadding: \"2px 8px\",\n\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{messages.resetZoom}\n\t\t\t\t</button>\n\t\t\t) : null}\n\t\t\t{showLegend && scene.legend.length > 0 ? (\n\t\t\t\t<ChartLegend\n\t\t\t\t\titems={scene.legend}\n\t\t\t\t\thidden={hidden}\n\t\t\t\t\tlabel={messages.legend}\n\t\t\t\t\tonToggle={toggle}\n\t\t\t\t/>\n\t\t\t) : null}\n\t\t\t{scene.colorRamp && legend !== false ? (\n\t\t\t\t<ChartColorRamp ramp={scene.colorRamp} label={messages.legend} />\n\t\t\t) : null}\n\t\t\t{dataTable ? (\n\t\t\t\t<ChartDataTable\n\t\t\t\t\tscene={scene}\n\t\t\t\t\tstops={stops}\n\t\t\t\t\tcaption={messages.dataTable(ariaLabel)}\n\t\t\t\t\tseriesHeading={messages.series}\n\t\t\t\t/>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import * as React from \"react\";\nimport { compileChart } from \"#/core/compile.ts\";\nimport type { ChartDefinition } from \"#/core/define-chart.ts\";\nimport { type ChartAnimation, chartTiming } from \"#/core/motion/timing.ts\";\nimport { findNearest, focusStops } from \"#/core/nearest.ts\";\nimport { toNumber } from \"#/core/scales/continuous.ts\";\nimport type { ChartPoint } from \"#/core/types.ts\";\nimport {\n\ttype ChartBrush,\n\ttype ChartBrushRange,\n\tordered,\n} from \"#/react/brush-contract.ts\";\nimport { ChartDataTable } from \"#/react/chart-data-table.tsx\";\nimport { ChartColorRamp, ChartLegend } from \"#/react/chart-legend.tsx\";\nimport {\n\tChartTooltip,\n\tChartTooltipContent,\n\ttype ChartTooltipRenderProps,\n} from \"#/react/chart-tooltip.tsx\";\nimport { FocusOverlay } from \"#/react/focus-overlay.tsx\";\nimport { messagesFor } from \"#/react/messages.ts\";\nimport type { ChartRenderer } from \"#/react/renderer.ts\";\nimport { SR_ONLY } from \"#/react/sr-only.ts\";\nimport { SvgRenderer } from \"#/react/svg-renderer.tsx\";\nimport { useAnimatedScene } from \"#/react/use-animated-scene.ts\";\nimport { useChartFocus } from \"#/react/use-chart-focus.ts\";\nimport { useChartWidth } from \"#/react/use-chart-width.ts\";\nimport { useChartZoom } from \"#/react/use-chart-zoom.ts\";\nimport { useTextMeasurer } from \"#/react/use-text-measurer.ts\";\n\nconst DEFAULT_HEIGHT = 300;\nconst DEFAULT_INITIAL_WIDTH = 640;\n\n/** Focus ring, forced colours and reduced motion: what inline styles cannot say. */\nconst CHART_CSS = `\n[data-slot=\"chart-surface\"]:focus-visible{outline:2px solid var(--ring,Highlight);outline-offset:2px;border-radius:4px}\n@media (forced-colors:active){\n[data-slot=\"chart-svg\"] [data-role=\"axis\"],[data-slot=\"chart-svg\"] [data-role=\"axis-title\"]{fill:CanvasText}\n[data-slot=\"chart-svg\"] [data-role=\"grid\"]{stroke:GrayText}\n[data-slot=\"chart-focus-ring\"]{stroke:Highlight}\n[data-slot=\"chart-tooltip\"]{border-color:CanvasText}\n}\n@media (prefers-reduced-motion:no-preference){\n[data-slot=\"chart-tooltip\"]{transition:transform 80ms ease-out}\n}\n`;\n\n// `title` would put a native tooltip on the whole chart; the props carry the\n// accessible name as `ariaLabel` instead.\ninterface Props\n\textends Omit<React.ComponentProps<\"div\">, \"children\" | \"onSelect\" | \"title\"> {\n\treadonly definition: ChartDefinition;\n\t/** What the chart shows, in a sentence: the screen reader's name for it. Required. */\n\treadonly ariaLabel: string;\n\t/** The takeaway, when the label alone does not carry it. */\n\treadonly ariaDescription?: string;\n\t/** Pixels. Ignored when `aspectRatio` is set. */\n\treadonly height?: number;\n\t/** Height as a share of the width: `0.5` is twice as wide as tall. */\n\treadonly aspectRatio?: number;\n\t/** Width used on the server and for the first client render, before the container is measured. */\n\treadonly initialWidth?: number;\n\treadonly renderer?: ChartRenderer;\n\t/** `false` turns the tooltip off; a function replaces its content. */\n\treadonly tooltip?:\n\t\t| boolean\n\t\t| ((props: ChartTooltipRenderProps) => React.ReactNode);\n\t/** Shown by default when there is more than one series. */\n\treadonly legend?: boolean;\n\t/** The visually hidden table of the values. On by default; turn it off only when the page shows the same table. */\n\treadonly dataTable?: boolean;\n\t/**\n\t * How a data update moves: `true` (a spring with no bounce), a perceived\n\t * duration in milliseconds, or `{ duration, bounce, stagger }` and\n\t * `{ type: \"tween\", easing }`. `false` snaps; reduced motion always does.\n\t */\n\treadonly animate?: boolean | number | ChartAnimation;\n\t/**\n\t * Zoom a continuous x: the wheel while the chart has focus, + and \u2212,\n\t * Shift and the arrows to pan, a drag to pan when zoomed, 0 to reset.\n\t */\n\treadonly zoom?: boolean;\n\t/** Select a range of x by dragging or from the keyboard: `brushX({ onBrush })` from `@voila.dev/chart/brush`. */\n\treadonly brush?: ChartBrush;\n\treadonly onFocusChange?: (point: ChartPoint | null) => void;\n\treadonly onSelect?: (point: ChartPoint | null) => void;\n}\n\n/** A pointer that moved less than this between down and up clicked rather than dragged. */\nconst DRAG_THRESHOLD = 3;\nconst ZOOM_STEP = 0.8;\nconst PAN_SHARE = 0.1;\n\n/**\n * Draws a definition. Everything a reader needs beyond the picture comes with\n * it: keyboard focus on every value, a live region that reads the focused\n * value, a legend that toggles series, and the values as a hidden table.\n */\nexport function Chart({\n\tdefinition,\n\tariaLabel,\n\tariaDescription,\n\theight = DEFAULT_HEIGHT,\n\taspectRatio,\n\tinitialWidth = DEFAULT_INITIAL_WIDTH,\n\trenderer: Renderer = SvgRenderer,\n\ttooltip = true,\n\tlegend,\n\tdataTable = true,\n\tanimate = true,\n\tzoom = false,\n\tbrush: brushBehavior,\n\tonFocusChange,\n\tonSelect,\n\tstyle,\n\t...props\n}: Props) {\n\tconst chartId = React.useId().replaceAll(\":\", \"\");\n\tconst { ref, width, node } = useChartWidth(initialWidth);\n\tconst measureText = useTextMeasurer(node);\n\tconst [hidden, setHidden] = React.useState<ReadonlySet<string>>(\n\t\t() => new Set(),\n\t);\n\tconst chartHeight =\n\t\taspectRatio === undefined ? height : Math.round(width * aspectRatio);\n\n\tconst baseScene = React.useMemo(\n\t\t() =>\n\t\t\tcompileChart(definition, {\n\t\t\t\twidth,\n\t\t\t\theight: chartHeight,\n\t\t\t\tmeasureText,\n\t\t\t\thiddenSeries: hidden,\n\t\t\t}),\n\t\t[definition, width, chartHeight, measureText, hidden],\n\t);\n\tconst baseX = baseScene.scales.x;\n\tconst fullX = React.useMemo<readonly [number, number] | undefined>(\n\t\t() =>\n\t\t\tbaseX && baseX.kind !== \"band\" && baseX.kind !== \"point\"\n\t\t\t\t? [toNumber(baseX.domain[0]), toNumber(baseX.domain[1])]\n\t\t\t\t: undefined,\n\t\t[baseX],\n\t);\n\tconst viewport = useChartZoom(zoom ? fullX : undefined, definition);\n\tconst scene = React.useMemo(\n\t\t() =>\n\t\t\tviewport.domain === null\n\t\t\t\t? baseScene\n\t\t\t\t: compileChart(definition, {\n\t\t\t\t\t\twidth,\n\t\t\t\t\t\theight: chartHeight,\n\t\t\t\t\t\tmeasureText,\n\t\t\t\t\t\thiddenSeries: hidden,\n\t\t\t\t\t\txDomain: viewport.domain,\n\t\t\t\t\t}),\n\t\t[\n\t\t\tbaseScene,\n\t\t\tviewport.domain,\n\t\t\tdefinition,\n\t\t\twidth,\n\t\t\tchartHeight,\n\t\t\tmeasureText,\n\t\t\thidden,\n\t\t],\n\t);\n\tconst [brush, setBrush] = React.useState<ChartBrushRange | null>(null);\n\tconst [zoomAnnouncement, setZoomAnnouncement] = React.useState(\"\");\n\tconst drag = React.useRef<{\n\t\treadonly startX: number;\n\t\treadonly domain: readonly [number, number] | null;\n\t\tmoved: boolean;\n\t} | null>(null);\n\tconst stops = React.useMemo(() => focusStops(scene), [scene]);\n\tconst animationTrigger = React.useMemo(\n\t\t() => [definition, hidden],\n\t\t[definition, hidden],\n\t);\n\t// An inline `animate={{ \u2026 }}` object is read by value, not identity.\n\tconst animateKey = JSON.stringify(animate);\n\tconst timing = React.useMemo(\n\t\t() => chartTiming(JSON.parse(animateKey)),\n\t\t[animateKey],\n\t);\n\tconst shown = useAnimatedScene(scene, animationTrigger, timing);\n\tconst messages = messagesFor(scene.locale);\n\tconst { active, handlers } = useChartFocus({\n\t\tstops,\n\t\torder: scene.focusOrder,\n\t\thitTest: (x, y) => findNearest(scene, stops, x, y),\n\t\tonFocusChange,\n\t\tonSelect,\n\t});\n\n\tconst xScale = scene.scales.x;\n\tconst xContinuous =\n\t\txScale !== undefined && xScale.kind !== \"band\" && xScale.kind !== \"point\";\n\n\tfunction updateBrush(range: ChartBrushRange | null) {\n\t\tsetBrush(range);\n\t\tbrushBehavior?.onBrush(range);\n\t}\n\n\tfunction localX(event: React.PointerEvent<HTMLElement>): number {\n\t\treturn event.clientX - event.currentTarget.getBoundingClientRect().left;\n\t}\n\n\tfunction onPointerDown(event: React.PointerEvent<HTMLDivElement>) {\n\t\tif (!brushBehavior && !(zoom && viewport.domain !== null)) {\n\t\t\treturn;\n\t\t}\n\t\tevent.currentTarget.setPointerCapture(event.pointerId);\n\t\t// A drag selects a range, not a value: the hover tooltip gives way.\n\t\thandlers.onPointerLeave();\n\t\tdrag.current = {\n\t\t\tstartX: localX(event),\n\t\t\tdomain: viewport.domain,\n\t\t\tmoved: false,\n\t\t};\n\t}\n\n\tfunction onPointerMove(event: React.PointerEvent<HTMLDivElement>) {\n\t\tconst current = drag.current;\n\t\tif (current === null || xScale === undefined) {\n\t\t\thandlers.onPointerMove(event);\n\t\t\treturn;\n\t\t}\n\t\tconst x = localX(event);\n\t\tif (Math.abs(x - current.startX) > DRAG_THRESHOLD) {\n\t\t\tcurrent.moved = true;\n\t\t}\n\t\tif (!current.moved) return;\n\t\tif (brushBehavior) {\n\t\t\tupdateBrush(\n\t\t\t\tordered(xScale, [xScale.invert(current.startX), xScale.invert(x)]),\n\t\t\t);\n\t\t} else if (current.domain !== null) {\n\t\t\tconst [from, to] = current.domain;\n\t\t\tconst perPixel = (to - from) / Math.max(1, scene.plot.width);\n\t\t\tviewport.pan(\n\t\t\t\t(current.startX - x) * perPixel -\n\t\t\t\t\t((viewport.domain?.[0] ?? from) - from),\n\t\t\t);\n\t\t}\n\t}\n\n\tfunction onPointerUp(event: React.PointerEvent<HTMLDivElement>) {\n\t\tif (\n\t\t\tdrag.current !== null &&\n\t\t\tevent.currentTarget.hasPointerCapture(event.pointerId)\n\t\t) {\n\t\t\tevent.currentTarget.releasePointerCapture(event.pointerId);\n\t\t}\n\t}\n\n\tfunction onClick(event: React.MouseEvent<HTMLDivElement>) {\n\t\tconst dragged = drag.current?.moved === true;\n\t\tdrag.current = null;\n\t\tif (!dragged) {\n\t\t\thandlers.onClick(event);\n\t\t}\n\t}\n\n\tfunction announceViewport(next: readonly [number, number] | null) {\n\t\tif (xScale === undefined) return;\n\t\tconst [from, to] = next ?? fullX ?? [0, 0];\n\t\tconst asValue = (value: number) =>\n\t\t\txScale.kind === \"time\" ? new Date(value) : value;\n\t\tsetZoomAnnouncement(\n\t\t\tmessages.showing(\n\t\t\t\txScale.format(asValue(from)),\n\t\t\t\txScale.format(asValue(to)),\n\t\t\t),\n\t\t);\n\t}\n\n\tfunction onKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n\t\tif (zoom && xContinuous && fullX !== undefined) {\n\t\t\tconst focused = active?.point.xValue;\n\t\t\tconst [from, to] = viewport.domain ?? fullX;\n\t\t\tconst center =\n\t\t\t\tfocused === undefined ? (from + to) / 2 : toNumber(focused);\n\t\t\tif (event.key === \"+\" || event.key === \"=\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.zoomAround(center, ZOOM_STEP);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.key === \"-\" || event.key === \"_\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.zoomAround(center, 1 / ZOOM_STEP);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.key === \"0\") {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.reset();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tevent.shiftKey &&\n\t\t\t\t(event.key === \"ArrowLeft\" || event.key === \"ArrowRight\")\n\t\t\t) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tviewport.pan(\n\t\t\t\t\t(event.key === \"ArrowRight\" ? 1 : -1) * viewport.span() * PAN_SHARE,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\thandlers.onKeyDown(event);\n\t}\n\n\t// Moving the focus says the new value; the last zoom's message gives way.\n\tconst activeKey = active?.point.key;\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: clears on a change of focused point only\n\tReact.useEffect(() => {\n\t\tsetZoomAnnouncement(\"\");\n\t}, [activeKey]);\n\n\tconst viewportKey = viewport.domain?.join(\":\") ?? \"full\";\n\tconst previousViewport = React.useRef(viewportKey);\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: announce a change of viewport, once\n\tReact.useEffect(() => {\n\t\tif (previousViewport.current !== viewportKey) {\n\t\t\tpreviousViewport.current = viewportKey;\n\t\t\tannounceViewport(viewport.domain);\n\t\t}\n\t}, [viewportKey]);\n\n\t// The wheel zooms only while the chart has focus: scrolling past a chart\n\t// must keep scrolling the page.\n\tconst zoomRef = React.useRef({\n\t\tviewport,\n\t\txScale,\n\t\tenabled: zoom && xContinuous,\n\t});\n\tzoomRef.current = { viewport, xScale, enabled: zoom && xContinuous };\n\tReact.useEffect(() => {\n\t\tif (node === null) return;\n\t\tfunction onWheel(event: WheelEvent) {\n\t\t\tconst { viewport: current, xScale: scale, enabled } = zoomRef.current;\n\t\t\tif (\n\t\t\t\t!enabled ||\n\t\t\t\tscale === undefined ||\n\t\t\t\tdocument.activeElement !== node ||\n\t\t\t\tnode === null\n\t\t\t)\n\t\t\t\treturn;\n\t\t\tevent.preventDefault();\n\t\t\tconst x = event.clientX - node.getBoundingClientRect().left;\n\t\t\tcurrent.zoomAround(\n\t\t\t\ttoNumber(scale.invert(x)),\n\t\t\t\tevent.deltaY > 0 ? 1 / ZOOM_STEP : ZOOM_STEP,\n\t\t\t);\n\t\t}\n\t\tnode.addEventListener(\"wheel\", onWheel, { passive: false });\n\t\treturn () => node.removeEventListener(\"wheel\", onWheel);\n\t}, [node]);\n\n\tconst announcement =\n\t\tactive?.source === \"keyboard\"\n\t\t\t? `${active.point.title} : ${(active.stop.points.length > 1\n\t\t\t\t\t? active.stop.points\n\t\t\t\t\t: [active.point]\n\t\t\t\t)\n\t\t\t\t\t.map((point) => `${point.seriesLabel ?? scene.yLabel} ${point.value}`)\n\t\t\t\t\t.join(\", \")}`\n\t\t\t: \"\";\n\tconst showLegend = legend ?? scene.legend.length > 1;\n\tconst descriptionId = `${chartId}-description`;\n\tconst renderTooltip =\n\t\ttypeof tooltip === \"function\" ? tooltip : ChartTooltipContent;\n\n\tfunction toggle(key: string) {\n\t\tsetHidden((current) => {\n\t\t\tconst next = new Set(current);\n\t\t\tif (!next.delete(key)) {\n\t\t\t\tnext.add(key);\n\t\t\t}\n\t\t\treturn next;\n\t\t});\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"chart\"\n\t\t\tstyle={{ display: \"grid\", gap: 8, ...style }}\n\t\t\t{...props}\n\t\t>\n\t\t\t<style href=\"voila-chart\" precedence=\"default\">\n\t\t\t\t{CHART_CSS}\n\t\t\t</style>\n\t\t\t<div\n\t\t\t\tdata-slot=\"chart-frame\"\n\t\t\t\tstyle={{ position: \"relative\", height: chartHeight }}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tref={ref}\n\t\t\t\t\trole=\"img\"\n\t\t\t\t\taria-roledescription={messages.roleDescription}\n\t\t\t\t\taria-label={ariaLabel}\n\t\t\t\t\taria-describedby={descriptionId}\n\t\t\t\t\t// The chart is one focus stop; the arrow keys move inside it, the way\n\t\t\t\t\t// a reader walks the values. role=\"img\" keeps it a single named\n\t\t\t\t\t// graphic, the live region speaks the focused value.\n\t\t\t\t\t// biome-ignore lint/a11y/noNoninteractiveTabindex: keyboard exploration of the values\n\t\t\t\t\ttabIndex={0}\n\t\t\t\t\tdata-slot=\"chart-surface\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tinset: 0,\n\t\t\t\t\t\ttouchAction: brushBehavior || zoom ? \"none\" : \"pan-y\",\n\t\t\t\t\t\t// Dragging across the plot brushes or pans; without this it\n\t\t\t\t\t\t// also selected the tick labels and the tooltip as text.\n\t\t\t\t\t\t...(brushBehavior || zoom\n\t\t\t\t\t\t\t? { userSelect: \"none\", WebkitUserSelect: \"none\" }\n\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t}}\n\t\t\t\t\t{...handlers}\n\t\t\t\t\tonKeyDown={onKeyDown}\n\t\t\t\t\tonPointerDown={onPointerDown}\n\t\t\t\t\tonPointerMove={onPointerMove}\n\t\t\t\t\tonPointerUp={onPointerUp}\n\t\t\t\t\tonClick={onClick}\n\t\t\t\t>\n\t\t\t\t\t<Renderer scene={shown} chartId={chartId} />\n\t\t\t\t\t<FocusOverlay scene={scene} active={active} theme={scene.theme} />\n\t\t\t\t\t{tooltip !== false && active !== null ? (\n\t\t\t\t\t\t<ChartTooltip\n\t\t\t\t\t\t\tactive={active}\n\t\t\t\t\t\t\tscene={scene}\n\t\t\t\t\t\t\trender={renderTooltip}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t\t{brushBehavior && xScale ? (\n\t\t\t\t\t<brushBehavior.Layer\n\t\t\t\t\t\tscale={xScale}\n\t\t\t\t\t\tplot={scene.plot}\n\t\t\t\t\t\trange={brush}\n\t\t\t\t\t\tmessages={messages}\n\t\t\t\t\t\tonChange={updateBrush}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t\t<div id={descriptionId} style={SR_ONLY}>\n\t\t\t\t{ariaDescription ? `${ariaDescription} ` : \"\"}\n\t\t\t\t{messages.keyboardHint}\n\t\t\t\t{zoom && xContinuous ? messages.zoomHint : \"\"}\n\t\t\t</div>\n\t\t\t<div aria-live=\"polite\" aria-atomic=\"true\" style={SR_ONLY}>\n\t\t\t\t{zoomAnnouncement || announcement}\n\t\t\t</div>\n\t\t\t{viewport.domain !== null ? (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-slot=\"chart-reset-zoom\"\n\t\t\t\t\tonClick={() => viewport.reset()}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tjustifySelf: \"end\",\n\t\t\t\t\t\tborder:\n\t\t\t\t\t\t\t\"1px solid var(--border, color-mix(in oklab, currentColor 20%, transparent))\",\n\t\t\t\t\t\tborderRadius: 6,\n\t\t\t\t\t\tbackground: \"transparent\",\n\t\t\t\t\t\tcolor: \"inherit\",\n\t\t\t\t\t\tfont: \"inherit\",\n\t\t\t\t\t\tfontSize: 12,\n\t\t\t\t\t\tpadding: \"2px 8px\",\n\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{messages.resetZoom}\n\t\t\t\t</button>\n\t\t\t) : null}\n\t\t\t{showLegend && scene.legend.length > 0 ? (\n\t\t\t\t<ChartLegend\n\t\t\t\t\titems={scene.legend}\n\t\t\t\t\thidden={hidden}\n\t\t\t\t\tlabel={messages.legend}\n\t\t\t\t\tonToggle={toggle}\n\t\t\t\t/>\n\t\t\t) : null}\n\t\t\t{scene.colorRamp && legend !== false ? (\n\t\t\t\t<ChartColorRamp ramp={scene.colorRamp} label={messages.legend} />\n\t\t\t) : null}\n\t\t\t{dataTable ? (\n\t\t\t\t<ChartDataTable\n\t\t\t\t\tscene={scene}\n\t\t\t\t\tstops={stops}\n\t\t\t\t\tcaption={messages.dataTable(ariaLabel)}\n\t\t\t\t\tseriesHeading={messages.series}\n\t\t\t\t/>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { ChartTiming } from "../core/motion/timing.js";
|
|
1
2
|
import type { ChartScene } from "../core/types.js";
|
|
2
3
|
/**
|
|
3
4
|
* The scene to draw this frame. A data change (`trigger` changed: a new
|
|
4
|
-
* definition, a series toggled)
|
|
5
|
-
* scene
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* definition, a series toggled) moves from what is on screen to the new
|
|
6
|
+
* scene on springs; one landing mid-flight keeps every node's speed. A
|
|
7
|
+
* resize snaps, since watching bars slide while a window is dragged helps no
|
|
8
|
+
* one. Reduced motion always snaps.
|
|
8
9
|
*/
|
|
9
|
-
export declare function useAnimatedScene(scene: ChartScene, trigger: unknown,
|
|
10
|
+
export declare function useAnimatedScene(scene: ChartScene, trigger: unknown, timing: ChartTiming | null): ChartScene;
|
|
10
11
|
//# sourceMappingURL=use-animated-scene.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-animated-scene.d.ts","sourceRoot":"","sources":["../../src/react/use-animated-scene.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-animated-scene.d.ts","sourceRoot":"","sources":["../../src/react/use-animated-scene.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,WAAW,GAAG,IAAI,GACxB,UAAU,CAoCZ"}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
+
import { cancelFrame, frame, shouldReduceMotion } from "@voila.dev/motion";
|
|
1
2
|
import * as React from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function useAnimatedScene(scene, trigger, duration) {
|
|
3
|
+
import {
|
|
4
|
+
createMotionStore
|
|
5
|
+
} from "../core/motion/motion-store.js";
|
|
6
|
+
function useAnimatedScene(scene, trigger, timing) {
|
|
7
7
|
const [shown, setShown] = React.useState(scene);
|
|
8
|
-
const shownRef = React.useRef(scene);
|
|
9
8
|
const triggerRef = React.useRef(trigger);
|
|
9
|
+
const storeRef = React.useRef(null);
|
|
10
10
|
React.useLayoutEffect(() => {
|
|
11
11
|
const changed = triggerRef.current !== trigger;
|
|
12
12
|
triggerRef.current = trigger;
|
|
13
|
-
if (
|
|
14
|
-
|
|
13
|
+
if (timing && storeRef.current?.timing !== timing) {
|
|
14
|
+
storeRef.current = { store: createMotionStore(scene, timing), timing };
|
|
15
|
+
}
|
|
16
|
+
const store = storeRef.current?.store;
|
|
17
|
+
if (!changed || !timing || !store || shouldReduceMotion()) {
|
|
18
|
+
store?.snap(scene);
|
|
15
19
|
setShown(scene);
|
|
16
20
|
return;
|
|
17
21
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const t = Math.min(1, (now - start) / duration);
|
|
26
|
-
const next = tweenScene(from, scene, easeOutCubic(t));
|
|
27
|
-
shownRef.current = next;
|
|
28
|
-
setShown(next);
|
|
29
|
-
if (t < 1) {
|
|
30
|
-
frame = requestAnimationFrame(step);
|
|
22
|
+
store.retarget(scene, performance.now());
|
|
23
|
+
function tick() {
|
|
24
|
+
const now = performance.now();
|
|
25
|
+
if (store?.settled(now)) {
|
|
26
|
+
cancelFrame(tick);
|
|
27
|
+
setShown(scene);
|
|
28
|
+
return;
|
|
31
29
|
}
|
|
30
|
+
setShown(store?.frame(now) ?? scene);
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
tick();
|
|
33
|
+
frame.update(tick, true);
|
|
34
|
+
return () => cancelFrame(tick);
|
|
35
|
+
}, [scene, trigger, timing]);
|
|
36
36
|
return shown;
|
|
37
37
|
}
|
|
38
38
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/react/use-animated-scene.ts"],
|
|
4
|
-
"sourcesContent": ["import * as React from \"react\";\nimport {
|
|
5
|
-
"mappings": "AAAA,
|
|
4
|
+
"sourcesContent": ["import { cancelFrame, frame, shouldReduceMotion } from \"@voila.dev/motion\";\nimport * as React from \"react\";\nimport {\n\tcreateMotionStore,\n\ttype MotionStore,\n} from \"#/core/motion/motion-store.ts\";\nimport type { ChartTiming } from \"#/core/motion/timing.ts\";\nimport type { ChartScene } from \"#/core/types.ts\";\n\n/**\n * The scene to draw this frame. A data change (`trigger` changed: a new\n * definition, a series toggled) moves from what is on screen to the new\n * scene on springs; one landing mid-flight keeps every node's speed. A\n * resize snaps, since watching bars slide while a window is dragged helps no\n * one. Reduced motion always snaps.\n */\nexport function useAnimatedScene(\n\tscene: ChartScene,\n\ttrigger: unknown,\n\ttiming: ChartTiming | null,\n): ChartScene {\n\tconst [shown, setShown] = React.useState(scene);\n\tconst triggerRef = React.useRef(trigger);\n\tconst storeRef = React.useRef<{\n\t\tstore: MotionStore;\n\t\ttiming: ChartTiming;\n\t} | null>(null);\n\n\tReact.useLayoutEffect(() => {\n\t\tconst changed = triggerRef.current !== trigger;\n\t\ttriggerRef.current = trigger;\n\t\tif (timing && storeRef.current?.timing !== timing) {\n\t\t\tstoreRef.current = { store: createMotionStore(scene, timing), timing };\n\t\t}\n\t\tconst store = storeRef.current?.store;\n\t\tif (!changed || !timing || !store || shouldReduceMotion()) {\n\t\t\tstore?.snap(scene);\n\t\t\tsetShown(scene);\n\t\t\treturn;\n\t\t}\n\t\tstore.retarget(scene, performance.now());\n\t\tfunction tick() {\n\t\t\tconst now = performance.now();\n\t\t\tif (store?.settled(now)) {\n\t\t\t\tcancelFrame(tick);\n\t\t\t\tsetShown(scene);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetShown(store?.frame(now) ?? scene);\n\t\t}\n\t\ttick();\n\t\tframe.update(tick, true);\n\t\treturn () => cancelFrame(tick);\n\t}, [scene, trigger, timing]);\n\n\treturn shown;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,aAAa,OAAO,0BAA0B;AACvD,YAAY,WAAW;AACvB;AAAA,EACC;AAAA,OAEM;AAWA,SAAS,iBACf,OACA,SACA,QACa;AACb,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,KAAK;AAC9C,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,QAAM,WAAW,MAAM,OAGb,IAAI;AAEd,QAAM,gBAAgB,MAAM;AAC3B,UAAM,UAAU,WAAW,YAAY;AACvC,eAAW,UAAU;AACrB,QAAI,UAAU,SAAS,SAAS,WAAW,QAAQ;AAClD,eAAS,UAAU,EAAE,OAAO,kBAAkB,OAAO,MAAM,GAAG,OAAO;AAAA,IACtE;AACA,UAAM,QAAQ,SAAS,SAAS;AAChC,QAAI,CAAC,WAAW,CAAC,UAAU,CAAC,SAAS,mBAAmB,GAAG;AAC1D,aAAO,KAAK,KAAK;AACjB,eAAS,KAAK;AACd;AAAA,IACD;AACA,UAAM,SAAS,OAAO,YAAY,IAAI,CAAC;AACvC,aAAS,OAAO;AACf,YAAM,MAAM,YAAY,IAAI;AAC5B,UAAI,OAAO,QAAQ,GAAG,GAAG;AACxB,oBAAY,IAAI;AAChB,iBAAS,KAAK;AACd;AAAA,MACD;AACA,eAAS,OAAO,MAAM,GAAG,KAAK,KAAK;AAAA,IACpC;AACA,SAAK;AACL,UAAM,OAAO,MAAM,IAAI;AACvB,WAAO,MAAM,YAAY,IAAI;AAAA,EAC9B,GAAG,CAAC,OAAO,SAAS,MAAM,CAAC;AAE3B,SAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voila.dev/chart",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.114",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Charts as data: a definition of marks, channels and scales, drawn to SVG or Canvas, keyboard and screen-reader accessible. The core has no dependencies; the heavy layouts (geo, hierarchy, sankey…) sit behind their own subpaths.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"sideEffects": false,
|
|
66
66
|
"dependencies": {
|
|
67
|
+
"@voila.dev/motion": "5.0.114",
|
|
67
68
|
"d3-contour": "4.0.2",
|
|
68
69
|
"d3-delaunay": "6.0.4",
|
|
69
70
|
"d3-force": "3.0.0",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { type Generator, spring, tween } from "@voila.dev/motion";
|
|
2
|
+
import { type ChartTiming, staggerDelay } from "#/core/motion/timing.ts";
|
|
3
|
+
import { tweenPath } from "#/core/motion/tween.ts";
|
|
4
|
+
import type { ChartScene, SceneNode } from "#/core/types.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Every moving number of a chart, as springs keyed by node and channel. A
|
|
8
|
+
* new scene retargets them: each channel starts from where it is painted, at
|
|
9
|
+
* the speed it has, so an update landing mid-flight bends the motion instead
|
|
10
|
+
* of kinking it. Pure and clocked from outside (milliseconds), so the SVG and
|
|
11
|
+
* Canvas renderers move identically and tests step it by hand. Hit-testing
|
|
12
|
+
* and focus never read it: they read the scene it moves towards.
|
|
13
|
+
*/
|
|
14
|
+
export interface MotionStore {
|
|
15
|
+
retarget(scene: ChartScene, now: number): void;
|
|
16
|
+
/** Jumps to `scene`: a resize, or reduced motion. */
|
|
17
|
+
snap(scene: ChartScene): void;
|
|
18
|
+
frame(now: number): ChartScene;
|
|
19
|
+
settled(now: number): boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Motion {
|
|
23
|
+
readonly generator: Generator;
|
|
24
|
+
/** Milliseconds, the delay included. */
|
|
25
|
+
readonly start: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PathMotion extends Motion {
|
|
29
|
+
readonly from: string;
|
|
30
|
+
readonly to: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Numeric = Record<string, number>;
|
|
34
|
+
|
|
35
|
+
const CHANNELS: Record<SceneNode["kind"], readonly string[]> = {
|
|
36
|
+
rect: ["x", "y", "width", "height"],
|
|
37
|
+
circle: ["cx", "cy", "r"],
|
|
38
|
+
line: ["x1", "y1", "x2", "y2"],
|
|
39
|
+
text: ["x", "y"],
|
|
40
|
+
path: [],
|
|
41
|
+
group: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const OPACITY = "opacity";
|
|
45
|
+
|
|
46
|
+
function flatten(
|
|
47
|
+
nodes: readonly SceneNode[],
|
|
48
|
+
into = new Map<string, SceneNode>(),
|
|
49
|
+
) {
|
|
50
|
+
for (const node of nodes) {
|
|
51
|
+
into.set(node.key, node);
|
|
52
|
+
if (node.kind === "group") flatten(node.children, into);
|
|
53
|
+
}
|
|
54
|
+
return into;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function channelKey(node: string, channel: string): string {
|
|
58
|
+
return `${node}\u0000${channel}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function elapsed(motion: Motion, now: number): number {
|
|
62
|
+
return Math.max(0, (now - motion.start) / 1000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function done(motion: Motion, now: number): boolean {
|
|
66
|
+
return elapsed(motion, now) >= motion.generator.duration;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createMotionStore(
|
|
70
|
+
initial: ChartScene,
|
|
71
|
+
timing: ChartTiming,
|
|
72
|
+
): MotionStore {
|
|
73
|
+
let target = initial;
|
|
74
|
+
const numbers = new Map<string, Motion>();
|
|
75
|
+
const paths = new Map<string, PathMotion>();
|
|
76
|
+
|
|
77
|
+
function generator(from: number, to: number, velocity: number): Generator {
|
|
78
|
+
return timing.type === "spring"
|
|
79
|
+
? spring({
|
|
80
|
+
from,
|
|
81
|
+
to,
|
|
82
|
+
velocity,
|
|
83
|
+
duration: timing.duration,
|
|
84
|
+
bounce: timing.bounce,
|
|
85
|
+
})
|
|
86
|
+
: tween({
|
|
87
|
+
keyframes: [from, to],
|
|
88
|
+
duration: timing.duration,
|
|
89
|
+
ease: timing.easing,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pathNow(key: string, fallback: string | undefined, now: number) {
|
|
94
|
+
const motion = paths.get(key);
|
|
95
|
+
if (!motion) return fallback;
|
|
96
|
+
return tweenPath(
|
|
97
|
+
motion.from,
|
|
98
|
+
motion.to,
|
|
99
|
+
motion.generator.at(elapsed(motion, now)).value,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function moveNode(
|
|
104
|
+
node: SceneNode,
|
|
105
|
+
before: SceneNode | undefined,
|
|
106
|
+
start: number,
|
|
107
|
+
now: number,
|
|
108
|
+
) {
|
|
109
|
+
for (const channel of CHANNELS[node.kind]) {
|
|
110
|
+
const key = channelKey(node.key, channel);
|
|
111
|
+
const running = numbers.get(key);
|
|
112
|
+
const state = running?.generator.at(elapsed(running, now));
|
|
113
|
+
const from =
|
|
114
|
+
state?.value ?? (before as unknown as Numeric | undefined)?.[channel];
|
|
115
|
+
const to = (node as unknown as Numeric)[channel] as number;
|
|
116
|
+
numbers.delete(key);
|
|
117
|
+
if (from === undefined || (from === to && !state?.velocity)) continue;
|
|
118
|
+
numbers.set(key, {
|
|
119
|
+
generator: generator(from, to, state?.velocity ?? 0),
|
|
120
|
+
start,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (node.kind === "path") {
|
|
124
|
+
const from = pathNow(
|
|
125
|
+
node.key,
|
|
126
|
+
before?.kind === "path" ? before.d : undefined,
|
|
127
|
+
now,
|
|
128
|
+
);
|
|
129
|
+
paths.delete(node.key);
|
|
130
|
+
if (from !== undefined && from !== node.d) {
|
|
131
|
+
paths.set(node.key, {
|
|
132
|
+
generator: generator(0, 1, 0),
|
|
133
|
+
start,
|
|
134
|
+
from,
|
|
135
|
+
to: node.d,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const fading = channelKey(node.key, OPACITY);
|
|
140
|
+
const fade = numbers.get(fading);
|
|
141
|
+
if (before === undefined || fade) {
|
|
142
|
+
const from = fade ? fade.generator.at(elapsed(fade, now)).value : 0;
|
|
143
|
+
numbers.set(fading, { generator: generator(from, 1, 0), start });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function paint(node: SceneNode, now: number): SceneNode {
|
|
148
|
+
if (node.kind === "group") {
|
|
149
|
+
return {
|
|
150
|
+
...node,
|
|
151
|
+
children: node.children.map((child) => paint(child, now)),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const moved: Numeric = {};
|
|
155
|
+
for (const channel of [...CHANNELS[node.kind], OPACITY]) {
|
|
156
|
+
const motion = numbers.get(channelKey(node.key, channel));
|
|
157
|
+
if (!motion) continue;
|
|
158
|
+
moved[channel] = motion.generator.at(elapsed(motion, now)).value;
|
|
159
|
+
}
|
|
160
|
+
const d = node.kind === "path" ? pathNow(node.key, node.d, now) : undefined;
|
|
161
|
+
const { [OPACITY]: opacity, ...geometry } = moved;
|
|
162
|
+
const next = {
|
|
163
|
+
...node,
|
|
164
|
+
...geometry,
|
|
165
|
+
...(d === undefined ? {} : { d }),
|
|
166
|
+
} as SceneNode;
|
|
167
|
+
if (opacity === undefined) return next;
|
|
168
|
+
const shown = Math.min(1, Math.max(0, opacity));
|
|
169
|
+
const painted = next as Exclude<SceneNode, { kind: "group" }>;
|
|
170
|
+
return {
|
|
171
|
+
...painted,
|
|
172
|
+
paint: {
|
|
173
|
+
...painted.paint,
|
|
174
|
+
opacity: (painted.paint.opacity ?? 1) * shown,
|
|
175
|
+
},
|
|
176
|
+
} as SceneNode;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
retarget(scene, now) {
|
|
181
|
+
const before = flatten(target.nodes);
|
|
182
|
+
const after = flatten(scene.nodes);
|
|
183
|
+
const marks = [...after.values()].filter((node) => node.role === "mark");
|
|
184
|
+
const order = new Map(marks.map((node, index) => [node.key, index]));
|
|
185
|
+
for (const key of before.keys()) {
|
|
186
|
+
if (after.has(key)) continue;
|
|
187
|
+
paths.delete(key);
|
|
188
|
+
for (const channel of [
|
|
189
|
+
...(CHANNELS[before.get(key)?.kind ?? "group"] ?? []),
|
|
190
|
+
OPACITY,
|
|
191
|
+
]) {
|
|
192
|
+
numbers.delete(channelKey(key, channel));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const node of after.values()) {
|
|
196
|
+
const delay = staggerDelay(
|
|
197
|
+
timing,
|
|
198
|
+
order.get(node.key) ?? 0,
|
|
199
|
+
marks.length,
|
|
200
|
+
);
|
|
201
|
+
moveNode(node, before.get(node.key), now + delay * 1000, now);
|
|
202
|
+
}
|
|
203
|
+
target = scene;
|
|
204
|
+
},
|
|
205
|
+
snap(scene) {
|
|
206
|
+
numbers.clear();
|
|
207
|
+
paths.clear();
|
|
208
|
+
target = scene;
|
|
209
|
+
},
|
|
210
|
+
frame(now) {
|
|
211
|
+
for (const [key, motion] of numbers) {
|
|
212
|
+
if (done(motion, now)) numbers.delete(key);
|
|
213
|
+
}
|
|
214
|
+
for (const [key, motion] of paths) {
|
|
215
|
+
if (done(motion, now)) paths.delete(key);
|
|
216
|
+
}
|
|
217
|
+
if (numbers.size === 0 && paths.size === 0) return target;
|
|
218
|
+
return { ...target, nodes: target.nodes.map((node) => paint(node, now)) };
|
|
219
|
+
},
|
|
220
|
+
settled(now) {
|
|
221
|
+
return [...numbers.values(), ...paths.values()].every((motion) =>
|
|
222
|
+
done(motion, now),
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { EasingName } from "@voila.dev/motion";
|
|
2
|
+
|
|
3
|
+
/** How a data update moves: what `<Chart animate>` accepts beyond `true`, `false` and a duration. */
|
|
4
|
+
export type ChartAnimation =
|
|
5
|
+
| {
|
|
6
|
+
readonly type?: "spring";
|
|
7
|
+
/** Perceived duration, in milliseconds. */
|
|
8
|
+
readonly duration?: number;
|
|
9
|
+
/** 0 never overshoots (the default: a bar past its value shows a number the data doesn't have); up to 0.5. */
|
|
10
|
+
readonly bounce?: number;
|
|
11
|
+
/** Milliseconds between marks in focus order, capped at 300 ms in all. */
|
|
12
|
+
readonly stagger?: number;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
readonly type: "tween";
|
|
16
|
+
readonly duration?: number;
|
|
17
|
+
readonly easing?: EasingName;
|
|
18
|
+
readonly stagger?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface ChartTiming {
|
|
22
|
+
readonly type: "spring" | "tween";
|
|
23
|
+
/** Seconds, as the motion package counts them. */
|
|
24
|
+
readonly duration: number;
|
|
25
|
+
readonly bounce: number;
|
|
26
|
+
readonly easing: EasingName;
|
|
27
|
+
/** Seconds between neighbours. */
|
|
28
|
+
readonly stagger: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_DURATION = 300;
|
|
32
|
+
const MAX_STAGGER_TOTAL = 0.3;
|
|
33
|
+
|
|
34
|
+
/** The timing an `animate` prop means, or `null` to snap. */
|
|
35
|
+
export function chartTiming(
|
|
36
|
+
animate: boolean | number | ChartAnimation,
|
|
37
|
+
): ChartTiming | null {
|
|
38
|
+
if (animate === false || animate === 0) return null;
|
|
39
|
+
const options: ChartAnimation =
|
|
40
|
+
typeof animate === "object"
|
|
41
|
+
? animate
|
|
42
|
+
: { duration: animate === true ? DEFAULT_DURATION : animate };
|
|
43
|
+
return {
|
|
44
|
+
type: options.type ?? "spring",
|
|
45
|
+
duration: (options.duration ?? DEFAULT_DURATION) / 1000,
|
|
46
|
+
bounce: Math.min(
|
|
47
|
+
Math.max("bounce" in options ? (options.bounce ?? 0) : 0, 0),
|
|
48
|
+
0.5,
|
|
49
|
+
),
|
|
50
|
+
easing: ("easing" in options ? options.easing : undefined) ?? "easeOut",
|
|
51
|
+
stagger: (options.stagger ?? 0) / 1000,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The delay of the `index`-th of `count` marks: the gap shrinks so the last one never waits more than 300 ms. */
|
|
56
|
+
export function staggerDelay(
|
|
57
|
+
timing: ChartTiming,
|
|
58
|
+
index: number,
|
|
59
|
+
count: number,
|
|
60
|
+
): number {
|
|
61
|
+
if (timing.stagger === 0 || count < 2) return 0;
|
|
62
|
+
return index * Math.min(timing.stagger, MAX_STAGGER_TOTAL / (count - 1));
|
|
63
|
+
}
|
package/src/core/motion/tween.ts
CHANGED
|
@@ -19,7 +19,7 @@ function mix(from: number, to: number, t: number): number {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** Two paths can tween when only their numbers differ. */
|
|
22
|
-
function tweenPath(from: string, to: string, t: number): string {
|
|
22
|
+
export function tweenPath(from: string, to: string, t: number): string {
|
|
23
23
|
const fromNumbers = from.match(NUMBER);
|
|
24
24
|
const toNumbers = to.match(NUMBER);
|
|
25
25
|
if (
|
package/src/index.ts
CHANGED
|
@@ -60,6 +60,15 @@ export {
|
|
|
60
60
|
export { type WaffleOptions, waffleY } from "#/core/marks/stats/waffle.ts";
|
|
61
61
|
export { type TextOptions, text } from "#/core/marks/text.ts";
|
|
62
62
|
export { type TickOptions, tickX, tickY } from "#/core/marks/tick.ts";
|
|
63
|
+
export {
|
|
64
|
+
createMotionStore,
|
|
65
|
+
type MotionStore,
|
|
66
|
+
} from "#/core/motion/motion-store.ts";
|
|
67
|
+
export {
|
|
68
|
+
type ChartAnimation,
|
|
69
|
+
type ChartTiming,
|
|
70
|
+
chartTiming,
|
|
71
|
+
} from "#/core/motion/timing.ts";
|
|
63
72
|
export { easeOutCubic, tweenScene } from "#/core/motion/tween.ts";
|
|
64
73
|
export {
|
|
65
74
|
type ChartFocusStop,
|
package/src/react/chart.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { compileChart } from "#/core/compile.ts";
|
|
3
3
|
import type { ChartDefinition } from "#/core/define-chart.ts";
|
|
4
|
+
import { type ChartAnimation, chartTiming } from "#/core/motion/timing.ts";
|
|
4
5
|
import { findNearest, focusStops } from "#/core/nearest.ts";
|
|
5
6
|
import { toNumber } from "#/core/scales/continuous.ts";
|
|
6
7
|
import type { ChartPoint } from "#/core/types.ts";
|
|
@@ -29,7 +30,6 @@ import { useTextMeasurer } from "#/react/use-text-measurer.ts";
|
|
|
29
30
|
|
|
30
31
|
const DEFAULT_HEIGHT = 300;
|
|
31
32
|
const DEFAULT_INITIAL_WIDTH = 640;
|
|
32
|
-
const DEFAULT_DURATION = 300;
|
|
33
33
|
|
|
34
34
|
/** Focus ring, forced colours and reduced motion: what inline styles cannot say. */
|
|
35
35
|
const CHART_CSS = `
|
|
@@ -69,8 +69,12 @@ interface Props
|
|
|
69
69
|
readonly legend?: boolean;
|
|
70
70
|
/** The visually hidden table of the values. On by default; turn it off only when the page shows the same table. */
|
|
71
71
|
readonly dataTable?: boolean;
|
|
72
|
-
/**
|
|
73
|
-
|
|
72
|
+
/**
|
|
73
|
+
* How a data update moves: `true` (a spring with no bounce), a perceived
|
|
74
|
+
* duration in milliseconds, or `{ duration, bounce, stagger }` and
|
|
75
|
+
* `{ type: "tween", easing }`. `false` snaps; reduced motion always does.
|
|
76
|
+
*/
|
|
77
|
+
readonly animate?: boolean | number | ChartAnimation;
|
|
74
78
|
/**
|
|
75
79
|
* Zoom a continuous x: the wheel while the chart has focus, + and −,
|
|
76
80
|
* Shift and the arrows to pan, a drag to pan when zoomed, 0 to reset.
|
|
@@ -172,11 +176,13 @@ export function Chart({
|
|
|
172
176
|
() => [definition, hidden],
|
|
173
177
|
[definition, hidden],
|
|
174
178
|
);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
// An inline `animate={{ … }}` object is read by value, not identity.
|
|
180
|
+
const animateKey = JSON.stringify(animate);
|
|
181
|
+
const timing = React.useMemo(
|
|
182
|
+
() => chartTiming(JSON.parse(animateKey)),
|
|
183
|
+
[animateKey],
|
|
179
184
|
);
|
|
185
|
+
const shown = useAnimatedScene(scene, animationTrigger, timing);
|
|
180
186
|
const messages = messagesFor(scene.locale);
|
|
181
187
|
const { active, handlers } = useChartFocus({
|
|
182
188
|
stops,
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
+
import { cancelFrame, frame, shouldReduceMotion } from "@voila.dev/motion";
|
|
1
2
|
import * as React from "react";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createMotionStore,
|
|
5
|
+
type MotionStore,
|
|
6
|
+
} from "#/core/motion/motion-store.ts";
|
|
7
|
+
import type { ChartTiming } from "#/core/motion/timing.ts";
|
|
3
8
|
import type { ChartScene } from "#/core/types.ts";
|
|
4
9
|
|
|
5
|
-
function prefersReducedMotion(): boolean {
|
|
6
|
-
return (
|
|
7
|
-
typeof window !== "undefined" &&
|
|
8
|
-
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
10
|
/**
|
|
13
11
|
* The scene to draw this frame. A data change (`trigger` changed: a new
|
|
14
|
-
* definition, a series toggled)
|
|
15
|
-
* scene
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* definition, a series toggled) moves from what is on screen to the new
|
|
13
|
+
* scene on springs; one landing mid-flight keeps every node's speed. A
|
|
14
|
+
* resize snaps, since watching bars slide while a window is dragged helps no
|
|
15
|
+
* one. Reduced motion always snaps.
|
|
18
16
|
*/
|
|
19
17
|
export function useAnimatedScene(
|
|
20
18
|
scene: ChartScene,
|
|
21
19
|
trigger: unknown,
|
|
22
|
-
|
|
20
|
+
timing: ChartTiming | null,
|
|
23
21
|
): ChartScene {
|
|
24
22
|
const [shown, setShown] = React.useState(scene);
|
|
25
|
-
const shownRef = React.useRef(scene);
|
|
26
23
|
const triggerRef = React.useRef(trigger);
|
|
24
|
+
const storeRef = React.useRef<{
|
|
25
|
+
store: MotionStore;
|
|
26
|
+
timing: ChartTiming;
|
|
27
|
+
} | null>(null);
|
|
27
28
|
|
|
28
29
|
React.useLayoutEffect(() => {
|
|
29
30
|
const changed = triggerRef.current !== trigger;
|
|
30
31
|
triggerRef.current = trigger;
|
|
31
|
-
if (
|
|
32
|
-
|
|
32
|
+
if (timing && storeRef.current?.timing !== timing) {
|
|
33
|
+
storeRef.current = { store: createMotionStore(scene, timing), timing };
|
|
34
|
+
}
|
|
35
|
+
const store = storeRef.current?.store;
|
|
36
|
+
if (!changed || !timing || !store || shouldReduceMotion()) {
|
|
37
|
+
store?.snap(scene);
|
|
33
38
|
setShown(scene);
|
|
34
39
|
return;
|
|
35
40
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
function step(now: number) {
|
|
44
|
-
const t = Math.min(1, (now - start) / duration);
|
|
45
|
-
const next = tweenScene(from, scene, easeOutCubic(t));
|
|
46
|
-
shownRef.current = next;
|
|
47
|
-
setShown(next);
|
|
48
|
-
if (t < 1) {
|
|
49
|
-
frame = requestAnimationFrame(step);
|
|
41
|
+
store.retarget(scene, performance.now());
|
|
42
|
+
function tick() {
|
|
43
|
+
const now = performance.now();
|
|
44
|
+
if (store?.settled(now)) {
|
|
45
|
+
cancelFrame(tick);
|
|
46
|
+
setShown(scene);
|
|
47
|
+
return;
|
|
50
48
|
}
|
|
49
|
+
setShown(store?.frame(now) ?? scene);
|
|
51
50
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
tick();
|
|
52
|
+
frame.update(tick, true);
|
|
53
|
+
return () => cancelFrame(tick);
|
|
54
|
+
}, [scene, trigger, timing]);
|
|
55
55
|
|
|
56
56
|
return shown;
|
|
57
57
|
}
|