@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.
Files changed (103) hide show
  1. package/dist/canvas/paint-scene.d.ts.map +1 -1
  2. package/dist/canvas/paint-scene.js +3 -1
  3. package/dist/canvas/paint-scene.js.map +2 -2
  4. package/dist/contour/density.d.ts.map +1 -1
  5. package/dist/contour/density.js +2 -0
  6. package/dist/contour/density.js.map +2 -2
  7. package/dist/core/marks/area.d.ts.map +1 -1
  8. package/dist/core/marks/area.js +27 -11
  9. package/dist/core/marks/area.js.map +2 -2
  10. package/dist/core/marks/bar.d.ts.map +1 -1
  11. package/dist/core/marks/bar.js +2 -0
  12. package/dist/core/marks/bar.js.map +2 -2
  13. package/dist/core/marks/line.d.ts.map +1 -1
  14. package/dist/core/marks/line.js +13 -4
  15. package/dist/core/marks/line.js.map +2 -2
  16. package/dist/core/marks/polar.d.ts.map +1 -1
  17. package/dist/core/marks/polar.js +22 -5
  18. package/dist/core/marks/polar.js.map +2 -2
  19. package/dist/core/marks/shared.d.ts +8 -1
  20. package/dist/core/marks/shared.d.ts.map +1 -1
  21. package/dist/core/marks/shared.js.map +2 -2
  22. package/dist/core/marks/stats/difference.d.ts.map +1 -1
  23. package/dist/core/marks/stats/difference.js +20 -7
  24. package/dist/core/marks/stats/difference.js.map +2 -2
  25. package/dist/core/motion/arc-motion.d.ts +9 -0
  26. package/dist/core/motion/arc-motion.d.ts.map +1 -0
  27. package/dist/core/motion/arc-motion.js +67 -0
  28. package/dist/core/motion/arc-motion.js.map +7 -0
  29. package/dist/core/motion/enter-exit.d.ts +6 -0
  30. package/dist/core/motion/enter-exit.d.ts.map +1 -0
  31. package/dist/core/motion/enter-exit.js +20 -0
  32. package/dist/core/motion/enter-exit.js.map +7 -0
  33. package/dist/core/motion/leaving.d.ts +12 -0
  34. package/dist/core/motion/leaving.d.ts.map +1 -0
  35. package/dist/core/motion/leaving.js +33 -0
  36. package/dist/core/motion/leaving.js.map +7 -0
  37. package/dist/core/motion/motion-store.d.ts +22 -0
  38. package/dist/core/motion/motion-store.d.ts.map +1 -0
  39. package/dist/core/motion/motion-store.js +325 -0
  40. package/dist/core/motion/motion-store.js.map +7 -0
  41. package/dist/core/motion/points-motion.d.ts +6 -0
  42. package/dist/core/motion/points-motion.d.ts.map +1 -0
  43. package/dist/core/motion/points-motion.js +140 -0
  44. package/dist/core/motion/points-motion.js.map +7 -0
  45. package/dist/core/motion/strings.d.ts +18 -0
  46. package/dist/core/motion/strings.d.ts.map +1 -0
  47. package/dist/core/motion/strings.js +34 -0
  48. package/dist/core/motion/strings.js.map +7 -0
  49. package/dist/core/motion/timing.d.ts +30 -0
  50. package/dist/core/motion/timing.d.ts.map +1 -0
  51. package/dist/core/motion/timing.js +25 -0
  52. package/dist/core/motion/timing.js.map +7 -0
  53. package/dist/core/motion/tween.d.ts +2 -0
  54. package/dist/core/motion/tween.d.ts.map +1 -1
  55. package/dist/core/motion/tween.js +1 -0
  56. package/dist/core/motion/tween.js.map +2 -2
  57. package/dist/core/types.d.ts +85 -0
  58. package/dist/core/types.d.ts.map +1 -1
  59. package/dist/geo/geo.d.ts.map +1 -1
  60. package/dist/geo/geo.js +2 -0
  61. package/dist/geo/geo.js.map +2 -2
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +8 -0
  65. package/dist/index.js.map +2 -2
  66. package/dist/react/chart.d.ts +7 -2
  67. package/dist/react/chart.d.ts.map +1 -1
  68. package/dist/react/chart.js +12 -7
  69. package/dist/react/chart.js.map +2 -2
  70. package/dist/react/svg-scene.d.ts.map +1 -1
  71. package/dist/react/svg-scene.js +3 -1
  72. package/dist/react/svg-scene.js.map +2 -2
  73. package/dist/react/use-animated-scene.d.ts +6 -5
  74. package/dist/react/use-animated-scene.d.ts.map +1 -1
  75. package/dist/react/use-animated-scene.js +36 -24
  76. package/dist/react/use-animated-scene.js.map +2 -2
  77. package/dist/voronoi/voronoi.d.ts.map +1 -1
  78. package/dist/voronoi/voronoi.js +2 -0
  79. package/dist/voronoi/voronoi.js.map +2 -2
  80. package/package.json +2 -1
  81. package/src/canvas/paint-scene.ts +5 -1
  82. package/src/contour/density.ts +2 -0
  83. package/src/core/marks/area.ts +28 -13
  84. package/src/core/marks/bar.ts +2 -0
  85. package/src/core/marks/line.ts +15 -8
  86. package/src/core/marks/polar.ts +22 -5
  87. package/src/core/marks/shared.ts +8 -0
  88. package/src/core/marks/stats/difference.ts +28 -8
  89. package/src/core/motion/arc-motion.ts +113 -0
  90. package/src/core/motion/enter-exit.ts +44 -0
  91. package/src/core/motion/leaving.ts +55 -0
  92. package/src/core/motion/motion-store.ts +449 -0
  93. package/src/core/motion/points-motion.ts +190 -0
  94. package/src/core/motion/strings.ts +68 -0
  95. package/src/core/motion/timing.ts +63 -0
  96. package/src/core/motion/tween.ts +1 -1
  97. package/src/core/types.ts +98 -0
  98. package/src/geo/geo.ts +2 -0
  99. package/src/index.ts +9 -0
  100. package/src/react/chart.tsx +21 -9
  101. package/src/react/svg-scene.tsx +5 -1
  102. package/src/react/use-animated-scene.ts +49 -33
  103. package/src/voronoi/voronoi.ts +2 -0
@@ -12,6 +12,8 @@ import {
12
12
  seriesResolver,
13
13
  tips,
14
14
  } from "#/core/marks/shared.ts";
15
+ import { ARC_MOTION } from "#/core/motion/arc-motion.ts";
16
+ import { POINTS_MOTION } from "#/core/motion/points-motion.ts";
15
17
  import {
16
18
  arcPath,
17
19
  type ChartXY,
@@ -181,6 +183,9 @@ export function arc<TDatum>(
181
183
  series: series.key,
182
184
  role: "mark",
183
185
  d: arcPath(shape),
186
+ geometry: { kind: "arc", ...shape },
187
+ motion: ARC_MOTION,
188
+ enter: options.enter ?? "grow",
184
189
  paint: paint({
185
190
  fill: series.color,
186
191
  stroke: options.stroke ?? context.theme.background,
@@ -306,6 +311,9 @@ export function radialBar<TDatum>(
306
311
  series: series.key,
307
312
  role: "mark",
308
313
  d: arcPath(shape),
314
+ geometry: { kind: "arc", ...shape },
315
+ motion: ARC_MOTION,
316
+ enter: options.enter ?? "grow",
309
317
  paint: paint({ fill: series.color, opacity: options.opacity }),
310
318
  });
311
319
  if (!tips(options.tip, data[index], index)) {
@@ -532,16 +540,25 @@ export function radar<TDatum>(
532
540
  }
533
541
  }
534
542
  for (const [key, polygon] of polygons) {
543
+ const geometry = {
544
+ kind: "points" as const,
545
+ shape: "polygon" as const,
546
+ runs: [
547
+ polygon.vertices.map((vertex, spoke) => ({
548
+ key: categoryKey(axes[spoke] as ChartValue),
549
+ ...(vertex ?? { x: frame.cx, y: frame.cy }),
550
+ })),
551
+ ],
552
+ };
535
553
  nodes.push({
536
554
  kind: "path",
537
555
  key: `${id}:shape:${key}`,
538
556
  series: key,
539
557
  role: "mark",
540
- d: polygonPath(
541
- polygon.vertices.map(
542
- (vertex) => vertex ?? { x: frame.cx, y: frame.cy },
543
- ),
544
- ),
558
+ d: polygonPath(geometry.runs[0]),
559
+ geometry,
560
+ motion: POINTS_MOTION,
561
+ enter: options.enter,
545
562
  paint: paint({
546
563
  fill: polygon.color,
547
564
  fillOpacity: options.fillOpacity ?? 0.2,
@@ -8,6 +8,7 @@ import { categoryKey } from "#/core/scales/discrete.ts";
8
8
  import type {
9
9
  ChartAccessor,
10
10
  ChartChannel,
11
+ ChartEnter,
11
12
  ChartMarkContext,
12
13
  ChartPaint,
13
14
  ChartPositionScale,
@@ -29,6 +30,13 @@ export interface ChartMarkOptions<TDatum> {
29
30
  */
30
31
  readonly tip?: boolean | ((datum: TDatum, index: number) => boolean);
31
32
  readonly opacity?: number;
33
+ /**
34
+ * How the mark appears when an update adds it: "grow" from its baseline
35
+ * or neighbour (the default for bars, areas and slices), "fade" (the
36
+ * default otherwise), "none". A line can also "draw": trace itself in on
37
+ * the first render.
38
+ */
39
+ readonly enter?: ChartEnter;
32
40
  }
33
41
 
34
42
  export function markId(
@@ -5,12 +5,16 @@ import {
5
5
  readChannel,
6
6
  } from "#/core/channel.ts";
7
7
  import { markId, paint, valueOn } from "#/core/marks/shared.ts";
8
- import { areaPath, type ChartXY, linePath } from "#/core/paths.ts";
8
+ import { POINTS_MOTION, pointsPath } from "#/core/motion/points-motion.ts";
9
+ import { areaPath, type ChartXY } from "#/core/paths.ts";
10
+ import { categoryKey } from "#/core/scales/discrete.ts";
9
11
  import type {
10
12
  ChartAccessor,
11
13
  ChartMark,
12
14
  ChartPoint,
13
15
  ChartValue,
16
+ GeometryMotion,
17
+ SceneGeometry,
14
18
  SceneNode,
15
19
  } from "#/core/types.ts";
16
20
 
@@ -34,6 +38,25 @@ interface Sample {
34
38
  readonly bottom: number;
35
39
  }
36
40
 
41
+ /** One of the two lines, keyed by x so an update slides it like any line. */
42
+ function keyedLine(
43
+ samples: readonly (Sample & { readonly key: string })[],
44
+ edge: "top" | "bottom",
45
+ ): { d: string; geometry: SceneGeometry; motion: GeometryMotion } {
46
+ const geometry: SceneGeometry = {
47
+ kind: "points",
48
+ shape: "line",
49
+ runs: [
50
+ samples.map((sample) => ({
51
+ key: sample.key,
52
+ x: sample.x,
53
+ y: sample[edge],
54
+ })),
55
+ ],
56
+ };
57
+ return { d: pointsPath(geometry), geometry, motion: POINTS_MOTION };
58
+ }
59
+
37
60
  /** Splits the band between two lines into runs of one sign, cut where they cross. */
38
61
  function signRuns(
39
62
  samples: ReadonlyArray<Sample>,
@@ -99,7 +122,7 @@ export function differenceY<TDatum>(
99
122
  }
100
123
  const positive = options.positiveFill ?? context.paletteColor(1);
101
124
  const negative = options.negativeFill ?? context.paletteColor(0);
102
- const samples: Sample[] = [];
125
+ const samples: Array<Sample & { readonly key: string }> = [];
103
126
  const points: ChartPoint[] = [];
104
127
  for (const [index, raw] of xs.entries()) {
105
128
  const x = valueOn(xScale, raw);
@@ -107,6 +130,7 @@ export function differenceY<TDatum>(
107
130
  const b = second[index];
108
131
  if (x === undefined || a === undefined || b === undefined) continue;
109
132
  const sample = {
133
+ key: categoryKey(x),
110
134
  x: xScale.center(x),
111
135
  top: yScale.map(a),
112
136
  bottom: yScale.map(b),
@@ -157,9 +181,7 @@ export function differenceY<TDatum>(
157
181
  key: `${id}:line:y1`,
158
182
  series: `${id}:y1`,
159
183
  role: "mark",
160
- d: linePath(
161
- samples.map((sample) => ({ x: sample.x, y: sample.top })),
162
- ),
184
+ ...keyedLine(samples, "top"),
163
185
  paint: {
164
186
  fill: "none",
165
187
  stroke: context.theme.foreground,
@@ -171,9 +193,7 @@ export function differenceY<TDatum>(
171
193
  key: `${id}:line:y2`,
172
194
  series: `${id}:y2`,
173
195
  role: "mark",
174
- d: linePath(
175
- samples.map((sample) => ({ x: sample.x, y: sample.bottom })),
176
- ),
196
+ ...keyedLine(samples, "bottom"),
177
197
  paint: {
178
198
  fill: "none",
179
199
  stroke: context.theme.muted,
@@ -0,0 +1,113 @@
1
+ import { arcPath } from "#/core/paths.ts";
2
+ import type {
3
+ GeometryMotion,
4
+ GeometryPlan,
5
+ SceneGeometry,
6
+ SceneNode,
7
+ ScenePath,
8
+ } from "#/core/types.ts";
9
+
10
+ type Arc = Extract<SceneGeometry, { kind: "arc" }>;
11
+
12
+ const ARC_FIELDS = [
13
+ "cx",
14
+ "cy",
15
+ "innerRadius",
16
+ "outerRadius",
17
+ "startAngle",
18
+ "endAngle",
19
+ ] as const;
20
+
21
+ function arcOf(node: SceneNode | undefined): Arc | undefined {
22
+ return node?.kind === "path" && node.geometry?.kind === "arc"
23
+ ? node.geometry
24
+ : undefined;
25
+ }
26
+
27
+ /** Slices of one ring tile it: those are the neighbours a slice opens from and closes onto. */
28
+ function sameRing(a: Arc, b: Arc): boolean {
29
+ return (
30
+ a.cx === b.cx &&
31
+ a.cy === b.cy &&
32
+ a.innerRadius === b.innerRadius &&
33
+ a.outerRadius === b.outerRadius
34
+ );
35
+ }
36
+
37
+ function withAngle(node: ScenePath, arc: Arc, angle: number): ScenePath {
38
+ return { ...node, geometry: { ...arc, startAngle: angle, endAngle: angle } };
39
+ }
40
+
41
+ /**
42
+ * The boundary angle next to `node` among the slices of its ring in `order`,
43
+ * read from `known`: the end of the nearest one before it, or the start of the
44
+ * nearest one after it, whichever side `afterFirst` looks at first.
45
+ */
46
+ function boundary(
47
+ node: SceneNode,
48
+ order: readonly SceneNode[],
49
+ known: ReadonlyMap<string, SceneNode>,
50
+ arc: Arc,
51
+ afterFirst: boolean,
52
+ ): number {
53
+ const index = order.findIndex((other) => other.key === node.key);
54
+ function scan(step: -1 | 1): number | undefined {
55
+ for (let at = index + step; at >= 0 && at < order.length; at += step) {
56
+ const other = arcOf(known.get((order[at] as SceneNode).key));
57
+ if (other && sameRing(other, arc)) {
58
+ return step === 1 ? other.startAngle : other.endAngle;
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+ const [first, second] = afterFirst ? ([1, -1] as const) : ([-1, 1] as const);
64
+ return scan(first) ?? scan(second) ?? arc.startAngle;
65
+ }
66
+
67
+ function arcPlan(from: Arc, to: Arc): GeometryPlan {
68
+ function record(arc: Arc) {
69
+ return new Map<string, number>(
70
+ ARC_FIELDS.map((field) => [field, arc[field]]),
71
+ );
72
+ }
73
+ return {
74
+ starts: record(from),
75
+ targets: record(to),
76
+ build(value) {
77
+ const arc = { kind: "arc" } as Record<string, unknown>;
78
+ for (const field of ARC_FIELDS) arc[field] = value(field);
79
+ return arc as Arc;
80
+ },
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Slices and radial bars: their angles and radii move, and the arc is
86
+ * rebuilt each frame, so its large-arc flag is never wrong. A new slice opens
87
+ * from the boundary where it will sit; a removed one closes onto the slice
88
+ * after it.
89
+ */
90
+ export const ARC_MOTION: GeometryMotion = {
91
+ plan: (from, to) =>
92
+ from.kind === "arc" && to.kind === "arc" ? arcPlan(from, to) : undefined,
93
+ draw: (geometry) =>
94
+ geometry.kind === "arc"
95
+ ? arcPath({
96
+ ...geometry,
97
+ endAngle: Math.max(geometry.startAngle, geometry.endAngle),
98
+ })
99
+ : "",
100
+ enter(node, siblings, before) {
101
+ const arc = arcOf(node);
102
+ return node.enter === "grow" && arc
103
+ ? withAngle(node, arc, boundary(node, siblings, before, arc, false))
104
+ : undefined;
105
+ },
106
+ exit(node, siblings, after) {
107
+ const arc = arcOf(node);
108
+ // A slice closes onto the one after it, so it takes that one's new start.
109
+ return node.enter === "grow" && arc
110
+ ? withAngle(node, arc, boundary(node, siblings, after, arc, true))
111
+ : undefined;
112
+ },
113
+ };
@@ -0,0 +1,44 @@
1
+ import type { SceneNode, SceneRect } from "#/core/types.ts";
2
+
3
+ /**
4
+ * The shape a data mark grows from when an update adds it, and collapses to
5
+ * when an update removes it. Always a node of the same kind, so the store
6
+ * springs it like any other change. Undefined when the mark has no such
7
+ * shape: it fades in, and is gone at once when it leaves. A bar has its
8
+ * baseline; a path asks the motion its mark brought (a slice opens from its
9
+ * neighbour, an area grows from its lower edge).
10
+ */
11
+
12
+ function collapsedBar(node: SceneRect): SceneRect | undefined {
13
+ const { baseline } = node;
14
+ if (!baseline) return undefined;
15
+ return baseline.axis === "y"
16
+ ? { ...node, y: baseline.at, height: 0 }
17
+ : { ...node, x: baseline.at, width: 0 };
18
+ }
19
+
20
+ /** Where a node an update adds starts from. `siblings` is the list it sits in, `before` the old scene. */
21
+ export function enterFrom(
22
+ node: SceneNode,
23
+ siblings: readonly SceneNode[],
24
+ before: ReadonlyMap<string, SceneNode>,
25
+ ): SceneNode | undefined {
26
+ if (node.enter !== "grow") return undefined;
27
+ if (node.kind === "rect") return collapsedBar(node);
28
+ return node.kind === "path"
29
+ ? node.motion?.enter?.(node, siblings, before)
30
+ : undefined;
31
+ }
32
+
33
+ /** Where a node an update removes collapses to. `siblings` is the old list it sat in, `after` the new scene. */
34
+ export function exitTo(
35
+ node: SceneNode,
36
+ siblings: readonly SceneNode[],
37
+ after: ReadonlyMap<string, SceneNode>,
38
+ ): SceneNode | undefined {
39
+ if (node.enter !== "grow") return undefined;
40
+ if (node.kind === "rect") return collapsedBar(node);
41
+ return node.kind === "path"
42
+ ? node.motion?.exit?.(node, siblings, after)
43
+ : undefined;
44
+ }
@@ -0,0 +1,55 @@
1
+ import type { SceneNode } from "#/core/types.ts";
2
+
3
+ /**
4
+ * Guides that left the scene still fade out where they stood: a tick label
5
+ * or a grid line vanishing at once reads as a glitch. Data marks never
6
+ * linger — a bar the data no longer has would show a value that isn't true.
7
+ */
8
+ const GUIDE_ROLES = new Set(["grid", "axis"]);
9
+
10
+ export interface LeavingGuide {
11
+ readonly node: SceneNode;
12
+ /** The group it sat in, or null at the root. */
13
+ readonly parent: string | null;
14
+ readonly index: number;
15
+ }
16
+
17
+ /** The guides of `nodes` whose keys `stays` no longer has. */
18
+ export function leavingGuides(
19
+ nodes: readonly SceneNode[],
20
+ stays: (key: string) => boolean,
21
+ parent: string | null = null,
22
+ into: LeavingGuide[] = [],
23
+ ): LeavingGuide[] {
24
+ for (const [index, node] of nodes.entries()) {
25
+ if (node.kind === "group") {
26
+ leavingGuides(node.children, stays, node.key, into);
27
+ continue;
28
+ }
29
+ if (!stays(node.key) && GUIDE_ROLES.has(node.role ?? "")) {
30
+ into.push({ node, parent, index });
31
+ }
32
+ }
33
+ return into;
34
+ }
35
+
36
+ /** `children` with the leaving guides of `parent` back at their places, faded to `opacity`. */
37
+ export function withLeaving(
38
+ children: readonly SceneNode[],
39
+ parent: string | null,
40
+ leaving: ReadonlyArray<readonly [LeavingGuide, number]>,
41
+ ): SceneNode[] {
42
+ const placed = [...children];
43
+ for (const [guide, opacity] of leaving) {
44
+ if (guide.parent !== parent || guide.node.kind === "group") continue;
45
+ const faded = {
46
+ ...guide.node,
47
+ paint: {
48
+ ...guide.node.paint,
49
+ opacity: (guide.node.paint.opacity ?? 1) * opacity,
50
+ },
51
+ } as SceneNode;
52
+ placed.splice(Math.min(guide.index, placed.length), 0, faded);
53
+ }
54
+ return placed;
55
+ }