@voila.dev/chart 5.0.113 → 5.0.117
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/canvas/paint-scene.d.ts.map +1 -1
- package/dist/canvas/paint-scene.js +3 -1
- package/dist/canvas/paint-scene.js.map +2 -2
- package/dist/contour/density.d.ts.map +1 -1
- package/dist/contour/density.js +2 -0
- package/dist/contour/density.js.map +2 -2
- package/dist/core/marks/area.d.ts.map +1 -1
- package/dist/core/marks/area.js +27 -11
- package/dist/core/marks/area.js.map +2 -2
- package/dist/core/marks/bar.d.ts.map +1 -1
- package/dist/core/marks/bar.js +2 -0
- package/dist/core/marks/bar.js.map +2 -2
- package/dist/core/marks/line.d.ts.map +1 -1
- package/dist/core/marks/line.js +13 -4
- package/dist/core/marks/line.js.map +2 -2
- package/dist/core/marks/polar.d.ts.map +1 -1
- package/dist/core/marks/polar.js +22 -5
- package/dist/core/marks/polar.js.map +2 -2
- package/dist/core/marks/shared.d.ts +8 -1
- package/dist/core/marks/shared.d.ts.map +1 -1
- package/dist/core/marks/shared.js.map +2 -2
- package/dist/core/marks/stats/difference.d.ts.map +1 -1
- package/dist/core/marks/stats/difference.js +20 -7
- package/dist/core/marks/stats/difference.js.map +2 -2
- package/dist/core/motion/arc-motion.d.ts +9 -0
- package/dist/core/motion/arc-motion.d.ts.map +1 -0
- package/dist/core/motion/arc-motion.js +67 -0
- package/dist/core/motion/arc-motion.js.map +7 -0
- package/dist/core/motion/enter-exit.d.ts +6 -0
- package/dist/core/motion/enter-exit.d.ts.map +1 -0
- package/dist/core/motion/enter-exit.js +20 -0
- package/dist/core/motion/enter-exit.js.map +7 -0
- package/dist/core/motion/leaving.d.ts +12 -0
- package/dist/core/motion/leaving.d.ts.map +1 -0
- package/dist/core/motion/leaving.js +33 -0
- package/dist/core/motion/leaving.js.map +7 -0
- package/dist/core/motion/motion-store.d.ts +22 -0
- package/dist/core/motion/motion-store.d.ts.map +1 -0
- package/dist/core/motion/motion-store.js +325 -0
- package/dist/core/motion/motion-store.js.map +7 -0
- package/dist/core/motion/points-motion.d.ts +6 -0
- package/dist/core/motion/points-motion.d.ts.map +1 -0
- package/dist/core/motion/points-motion.js +140 -0
- package/dist/core/motion/points-motion.js.map +7 -0
- package/dist/core/motion/strings.d.ts +18 -0
- package/dist/core/motion/strings.d.ts.map +1 -0
- package/dist/core/motion/strings.js +34 -0
- package/dist/core/motion/strings.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/core/types.d.ts +85 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/geo/geo.d.ts.map +1 -1
- package/dist/geo/geo.js +2 -0
- package/dist/geo/geo.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 +12 -7
- package/dist/react/chart.js.map +2 -2
- package/dist/react/svg-scene.d.ts.map +1 -1
- package/dist/react/svg-scene.js +3 -1
- package/dist/react/svg-scene.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 +36 -24
- package/dist/react/use-animated-scene.js.map +2 -2
- package/dist/voronoi/voronoi.d.ts.map +1 -1
- package/dist/voronoi/voronoi.js +2 -0
- package/dist/voronoi/voronoi.js.map +2 -2
- package/package.json +2 -1
- package/src/canvas/paint-scene.ts +5 -1
- package/src/contour/density.ts +2 -0
- package/src/core/marks/area.ts +28 -13
- package/src/core/marks/bar.ts +2 -0
- package/src/core/marks/line.ts +15 -8
- package/src/core/marks/polar.ts +22 -5
- package/src/core/marks/shared.ts +8 -0
- package/src/core/marks/stats/difference.ts +28 -8
- package/src/core/motion/arc-motion.ts +113 -0
- package/src/core/motion/enter-exit.ts +44 -0
- package/src/core/motion/leaving.ts +55 -0
- package/src/core/motion/motion-store.ts +449 -0
- package/src/core/motion/points-motion.ts +190 -0
- package/src/core/motion/strings.ts +68 -0
- package/src/core/motion/timing.ts +63 -0
- package/src/core/motion/tween.ts +1 -1
- package/src/core/types.ts +98 -0
- package/src/geo/geo.ts +2 -0
- package/src/index.ts +9 -0
- package/src/react/chart.tsx +21 -9
- package/src/react/svg-scene.tsx +5 -1
- package/src/react/use-animated-scene.ts +49 -33
- package/src/voronoi/voronoi.ts +2 -0
|
@@ -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/core/types.ts
CHANGED
|
@@ -92,6 +92,12 @@ export interface ChartPaint {
|
|
|
92
92
|
readonly strokeOpacity?: number;
|
|
93
93
|
/** Fill with diagonal hatching in the fill colour: a value pencilled in, not yet true. */
|
|
94
94
|
readonly hatch?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Only the first `fraction` of the stroke is drawn: a line tracing itself
|
|
97
|
+
* in. `length` is the path's length in pixels, which Canvas needs for its
|
|
98
|
+
* dash and SVG doesn't (it measures with `pathLength`).
|
|
99
|
+
*/
|
|
100
|
+
readonly drawn?: { readonly fraction: number; readonly length: number };
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
export interface ChartTextPaint extends ChartPaint {
|
|
@@ -108,8 +114,17 @@ interface SceneNodeBase {
|
|
|
108
114
|
readonly series?: string;
|
|
109
115
|
/** A role for styling and tests: "mark", "axis", "grid", "label"… */
|
|
110
116
|
readonly role?: string;
|
|
117
|
+
/** How the node appears when an update adds it: see `ChartEnter`. Fades by default. */
|
|
118
|
+
readonly enter?: ChartEnter;
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
/**
|
|
122
|
+
* How a data mark appears. "grow" rises from its baseline (bars, areas) or
|
|
123
|
+
* opens from its neighbour (slices); "draw" traces a line in on the first
|
|
124
|
+
* render; "fade" fades in; "none" is there at once.
|
|
125
|
+
*/
|
|
126
|
+
export type ChartEnter = "grow" | "draw" | "fade" | "none";
|
|
127
|
+
|
|
113
128
|
export interface SceneGroup extends SceneNodeBase {
|
|
114
129
|
readonly kind: "group";
|
|
115
130
|
/** Shifts the children: a facet's cell drawn at its place in the grid. Clips are in the shifted space. */
|
|
@@ -125,6 +140,8 @@ export interface SceneRect extends SceneNodeBase {
|
|
|
125
140
|
readonly width: number;
|
|
126
141
|
readonly height: number;
|
|
127
142
|
readonly corners?: ChartCorners;
|
|
143
|
+
/** The edge a bar grows from and shrinks back to: `y` for a column, `x` for a bar, in pixels. */
|
|
144
|
+
readonly baseline?: { readonly axis: "x" | "y"; readonly at: number };
|
|
128
145
|
readonly paint: ChartPaint;
|
|
129
146
|
}
|
|
130
147
|
|
|
@@ -132,9 +149,90 @@ export interface SceneRect extends SceneNodeBase {
|
|
|
132
149
|
export interface ScenePath extends SceneNodeBase {
|
|
133
150
|
readonly kind: "path";
|
|
134
151
|
readonly d: string;
|
|
152
|
+
/** What the shape means, when it has a simple meaning: motion moves that, then redraws `d`. */
|
|
153
|
+
readonly geometry?: SceneGeometry;
|
|
154
|
+
/**
|
|
155
|
+
* How that geometry moves, brought by the mark that drew it (like
|
|
156
|
+
* `morph`), so a chart only loads the motion of the shapes it has.
|
|
157
|
+
*/
|
|
158
|
+
readonly motion?: GeometryMotion;
|
|
135
159
|
readonly paint: ChartPaint;
|
|
160
|
+
/**
|
|
161
|
+
* How an update moves this outline into a new one whose commands differ:
|
|
162
|
+
* a country, a contour, a cell. Set by the marks whose shapes have no
|
|
163
|
+
* simpler meaning, so the morph only loads with them; without it, paths
|
|
164
|
+
* whose commands differ swap at the end.
|
|
165
|
+
*/
|
|
166
|
+
readonly morph?: PathMorpher;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A point of a line, an area or a radar, keyed by its position so an update can slide it. */
|
|
170
|
+
export interface GeometryPoint {
|
|
171
|
+
readonly key: string;
|
|
172
|
+
readonly x: number;
|
|
173
|
+
readonly y: number;
|
|
174
|
+
/** The area's lower edge under this point. */
|
|
175
|
+
readonly x0?: number;
|
|
176
|
+
readonly y0?: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type SceneGeometry =
|
|
180
|
+
| {
|
|
181
|
+
readonly kind: "points";
|
|
182
|
+
/** One run per unbroken stretch: a gap in the data splits the line. */
|
|
183
|
+
readonly runs: ReadonlyArray<ReadonlyArray<GeometryPoint>>;
|
|
184
|
+
readonly shape: "line" | "area" | "polygon";
|
|
185
|
+
readonly curve?: ChartCurve;
|
|
186
|
+
}
|
|
187
|
+
| {
|
|
188
|
+
readonly kind: "arc";
|
|
189
|
+
readonly cx: number;
|
|
190
|
+
readonly cy: number;
|
|
191
|
+
readonly innerRadius: number;
|
|
192
|
+
readonly outerRadius: number;
|
|
193
|
+
readonly startAngle: number;
|
|
194
|
+
readonly endAngle: number;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A geometry in motion: every number it has, by channel name, where each
|
|
199
|
+
* starts and where it goes, and how to rebuild the shape from the numbers of
|
|
200
|
+
* one frame. The store springs the numbers; the plan only knows shapes.
|
|
201
|
+
*/
|
|
202
|
+
export interface GeometryPlan {
|
|
203
|
+
readonly starts: ReadonlyMap<string, number>;
|
|
204
|
+
readonly targets: ReadonlyMap<string, number>;
|
|
205
|
+
build(value: (channel: string) => number): SceneGeometry;
|
|
136
206
|
}
|
|
137
207
|
|
|
208
|
+
/** What the motion store asks of a kind of geometry. */
|
|
209
|
+
export interface GeometryMotion {
|
|
210
|
+
/** How `from` (what is painted) becomes `to`; undefined to tween the path instead. */
|
|
211
|
+
plan(from: SceneGeometry, to: SceneGeometry): GeometryPlan | undefined;
|
|
212
|
+
/** The path data of one frame, through the mark's own builder, so every frame is a valid shape. */
|
|
213
|
+
draw(geometry: SceneGeometry): string;
|
|
214
|
+
/** Its length in pixels: what Canvas dashes against to trace a line in. */
|
|
215
|
+
length?(geometry: SceneGeometry): number;
|
|
216
|
+
/** The shape a node an update adds grows from; `siblings` is its new list, `known` the old scene. */
|
|
217
|
+
enter?(
|
|
218
|
+
node: ScenePath,
|
|
219
|
+
siblings: readonly SceneNode[],
|
|
220
|
+
known: ReadonlyMap<string, SceneNode>,
|
|
221
|
+
): ScenePath | undefined;
|
|
222
|
+
/** The shape a node an update removes collapses to; `siblings` is its old list, `known` the new scene. */
|
|
223
|
+
exit?(
|
|
224
|
+
node: ScenePath,
|
|
225
|
+
siblings: readonly SceneNode[],
|
|
226
|
+
known: ReadonlyMap<string, SceneNode>,
|
|
227
|
+
): ScenePath | undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The outlines between two paths, 0 at `from` and 1 at `to`. */
|
|
231
|
+
export type PathMorpher = (
|
|
232
|
+
from: string,
|
|
233
|
+
to: string,
|
|
234
|
+
) => (progress: number) => string;
|
|
235
|
+
|
|
138
236
|
export interface SceneCircle extends SceneNodeBase {
|
|
139
237
|
readonly kind: "circle";
|
|
140
238
|
readonly cx: number;
|
package/src/geo/geo.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mixPath } from "@voila.dev/motion";
|
|
1
2
|
import {
|
|
2
3
|
type GeoPermissibleObjects,
|
|
3
4
|
type GeoProjection,
|
|
@@ -202,6 +203,7 @@ export function geoShape<TFeature extends Feature>(
|
|
|
202
203
|
key: `${id}:${name}`,
|
|
203
204
|
role: "mark",
|
|
204
205
|
d,
|
|
206
|
+
morph: mixPath,
|
|
205
207
|
paint: paint({
|
|
206
208
|
fill,
|
|
207
209
|
stroke: options.stroke ?? context.theme.background,
|
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.
|
|
@@ -168,15 +172,20 @@ export function Chart({
|
|
|
168
172
|
moved: boolean;
|
|
169
173
|
} | null>(null);
|
|
170
174
|
const stops = React.useMemo(() => focusStops(scene), [scene]);
|
|
175
|
+
// A step zoom from the keyboard moves like a data update; the wheel and a
|
|
176
|
+
// drag are direct manipulation, so they snap.
|
|
177
|
+
const [keyboardZooms, setKeyboardZooms] = React.useState(0);
|
|
171
178
|
const animationTrigger = React.useMemo(
|
|
172
|
-
() => [definition, hidden],
|
|
173
|
-
[definition, hidden],
|
|
179
|
+
() => [definition, hidden, keyboardZooms],
|
|
180
|
+
[definition, hidden, keyboardZooms],
|
|
174
181
|
);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
// An inline `animate={{ … }}` object is read by value, not identity.
|
|
183
|
+
const animateKey = JSON.stringify(animate);
|
|
184
|
+
const timing = React.useMemo(
|
|
185
|
+
() => chartTiming(JSON.parse(animateKey)),
|
|
186
|
+
[animateKey],
|
|
179
187
|
);
|
|
188
|
+
const shown = useAnimatedScene(scene, animationTrigger, timing);
|
|
180
189
|
const messages = messagesFor(scene.locale);
|
|
181
190
|
const { active, handlers } = useChartFocus({
|
|
182
191
|
stops,
|
|
@@ -277,16 +286,19 @@ export function Chart({
|
|
|
277
286
|
if (event.key === "+" || event.key === "=") {
|
|
278
287
|
event.preventDefault();
|
|
279
288
|
viewport.zoomAround(center, ZOOM_STEP);
|
|
289
|
+
setKeyboardZooms((count) => count + 1);
|
|
280
290
|
return;
|
|
281
291
|
}
|
|
282
292
|
if (event.key === "-" || event.key === "_") {
|
|
283
293
|
event.preventDefault();
|
|
284
294
|
viewport.zoomAround(center, 1 / ZOOM_STEP);
|
|
295
|
+
setKeyboardZooms((count) => count + 1);
|
|
285
296
|
return;
|
|
286
297
|
}
|
|
287
298
|
if (event.key === "0") {
|
|
288
299
|
event.preventDefault();
|
|
289
300
|
viewport.reset();
|
|
301
|
+
setKeyboardZooms((count) => count + 1);
|
|
290
302
|
return;
|
|
291
303
|
}
|
|
292
304
|
if (
|
package/src/react/svg-scene.tsx
CHANGED
|
@@ -53,7 +53,11 @@ function paintProps(
|
|
|
53
53
|
: (paint.fill ?? "none"),
|
|
54
54
|
stroke: paint.stroke,
|
|
55
55
|
strokeWidth: paint.strokeWidth,
|
|
56
|
-
strokeDasharray: paint.strokeDasharray,
|
|
56
|
+
strokeDasharray: paint.drawn ? "1 1" : paint.strokeDasharray,
|
|
57
|
+
// Measured in path lengths: the dash shows the first `fraction` of the line.
|
|
58
|
+
...(paint.drawn
|
|
59
|
+
? { pathLength: 1, strokeDashoffset: 1 - paint.drawn.fraction }
|
|
60
|
+
: {}),
|
|
57
61
|
strokeLinecap: paint.strokeLinecap,
|
|
58
62
|
strokeLinejoin: paint.strokeLinejoin,
|
|
59
63
|
opacity: paint.opacity,
|
|
@@ -1,57 +1,73 @@
|
|
|
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
|
+
// The first render may play once: lines that `enter: "draw"` trace themselves in.
|
|
25
|
+
const introduced = React.useRef(false);
|
|
26
|
+
const introStart = React.useRef<number | null>(null);
|
|
27
|
+
const storeRef = React.useRef<{
|
|
28
|
+
store: MotionStore;
|
|
29
|
+
timing: ChartTiming;
|
|
30
|
+
} | null>(null);
|
|
27
31
|
|
|
28
32
|
React.useLayoutEffect(() => {
|
|
29
33
|
const changed = triggerRef.current !== trigger;
|
|
30
34
|
triggerRef.current = trigger;
|
|
31
|
-
if (
|
|
32
|
-
|
|
35
|
+
if (timing && storeRef.current?.timing !== timing) {
|
|
36
|
+
storeRef.current = { store: createMotionStore(scene, timing), timing };
|
|
37
|
+
}
|
|
38
|
+
const store = storeRef.current?.store;
|
|
39
|
+
const still = !timing || !store || shouldReduceMotion();
|
|
40
|
+
let intro = false;
|
|
41
|
+
if (!still && !changed && !introduced.current) {
|
|
42
|
+
const start = introStart.current ?? performance.now();
|
|
43
|
+
// A re-render mid-intro (the width measured after mount, fonts loaded): the
|
|
44
|
+
// new geometry, drawn from where the intro already is, never from scratch.
|
|
45
|
+
if (introStart.current !== null) store.snap(scene);
|
|
46
|
+
intro = store.introduce(start);
|
|
47
|
+
if (intro) introStart.current = start;
|
|
48
|
+
}
|
|
49
|
+
if (still || (!changed && !intro)) {
|
|
50
|
+
introduced.current = true;
|
|
51
|
+
store?.snap(scene);
|
|
33
52
|
setShown(scene);
|
|
34
53
|
return;
|
|
35
54
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const next = tweenScene(from, scene, easeOutCubic(t));
|
|
46
|
-
shownRef.current = next;
|
|
47
|
-
setShown(next);
|
|
48
|
-
if (t < 1) {
|
|
49
|
-
frame = requestAnimationFrame(step);
|
|
55
|
+
if (changed) store.retarget(scene, performance.now());
|
|
56
|
+
function tick() {
|
|
57
|
+
const now = performance.now();
|
|
58
|
+
if (store?.settled(now)) {
|
|
59
|
+
// Only once it has played: StrictMode's rerun of the effect starts it again.
|
|
60
|
+
introduced.current = true;
|
|
61
|
+
cancelFrame(tick);
|
|
62
|
+
setShown(scene);
|
|
63
|
+
return;
|
|
50
64
|
}
|
|
65
|
+
setShown(store?.frame(now) ?? scene);
|
|
51
66
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
tick();
|
|
68
|
+
frame.update(tick, true);
|
|
69
|
+
return () => cancelFrame(tick);
|
|
70
|
+
}, [scene, trigger, timing]);
|
|
55
71
|
|
|
56
72
|
return shown;
|
|
57
73
|
}
|
package/src/voronoi/voronoi.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mixPath } from "@voila.dev/motion";
|
|
1
2
|
import { Delaunay } from "d3-delaunay";
|
|
2
3
|
import { isPlaceable, readChannel } from "#/core/channel.ts";
|
|
3
4
|
import { markId, valueOn } from "#/core/marks/shared.ts";
|
|
@@ -66,6 +67,7 @@ export function voronoi<TDatum>(
|
|
|
66
67
|
key: `${id}:${dataIndex}`,
|
|
67
68
|
role: "mark",
|
|
68
69
|
d: diagram.renderCell(cell) ?? "",
|
|
70
|
+
morph: mixPath,
|
|
69
71
|
paint: options.fill
|
|
70
72
|
? {
|
|
71
73
|
fill: options.fill(data[dataIndex], dataIndex),
|