@motionscript/charts 0.1.0-alpha.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +201 -201
  3. package/NOTICE +49 -49
  4. package/README.md +99 -99
  5. package/dist/bar-chart/bar-chart.js.map +1 -1
  6. package/dist/bar-chart/category-axis.js.map +1 -1
  7. package/dist/bar-chart/chart-bars.js.map +1 -1
  8. package/dist/bar-chart/index.js.map +1 -1
  9. package/dist/bar-chart/shared.js.map +1 -1
  10. package/dist/browser/chunks/{chunk-XROBIRQK.js → chunk-TJNHLQGV.js} +1 -1
  11. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +7 -0
  12. package/dist/browser/index.js +1 -1
  13. package/dist/browser/index.js.map +1 -1
  14. package/dist/browser/kit.js +1 -1
  15. package/dist/browser/manifest.json +1 -1
  16. package/dist/engine.d.ts +1 -1
  17. package/dist/engine.js +1 -1
  18. package/dist/engine.js.map +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/kit/cartesian.js.map +1 -1
  21. package/dist/kit/chart-border.js.map +1 -1
  22. package/dist/kit/chart-canvas.js.map +1 -1
  23. package/dist/kit/chart-legend.js.map +1 -1
  24. package/dist/kit/entries.js.map +1 -1
  25. package/dist/kit/motion.js.map +1 -1
  26. package/dist/kit/paints.js.map +1 -1
  27. package/dist/kit/shared.js.map +1 -1
  28. package/dist/kit/vertical-axis.js.map +1 -1
  29. package/dist/line-chart/chart-line.js.map +1 -1
  30. package/dist/line-chart/chart-region.js.map +1 -1
  31. package/dist/line-chart/horizontal-axis.js.map +1 -1
  32. package/dist/line-chart/index.d.ts +1 -1
  33. package/dist/line-chart/index.js +1 -1
  34. package/dist/line-chart/index.js.map +1 -1
  35. package/dist/line-chart/line-chart.js.map +1 -1
  36. package/dist/line-chart/shared.js.map +1 -1
  37. package/dist/line-chart/x-scale.js.map +1 -1
  38. package/dist/nodes.js.map +1 -1
  39. package/dist/pie-chart/index.js.map +1 -1
  40. package/dist/pie-chart/pie-chart.js.map +1 -1
  41. package/dist/pie-chart/pie-legend.js.map +1 -1
  42. package/dist/pie-chart/pie-ring.js.map +1 -1
  43. package/dist/pie-chart/shared.js.map +1 -1
  44. package/package.json +4 -4
  45. package/registry.json +23 -23
  46. package/src/bar-chart/bar-chart.ts +542 -542
  47. package/src/bar-chart/category-axis.ts +162 -162
  48. package/src/bar-chart/chart-bars.ts +287 -287
  49. package/src/bar-chart/index.ts +31 -31
  50. package/src/bar-chart/shared.ts +354 -354
  51. package/src/engine.ts +28 -28
  52. package/src/index.ts +5 -5
  53. package/src/kit/cartesian.ts +81 -81
  54. package/src/kit/chart-border.ts +50 -50
  55. package/src/kit/chart-canvas.ts +122 -122
  56. package/src/kit/chart-legend.ts +229 -229
  57. package/src/kit/entries.ts +20 -20
  58. package/src/kit/motion.ts +90 -90
  59. package/src/kit/paints.ts +15 -15
  60. package/src/kit/shared.ts +782 -782
  61. package/src/kit/vertical-axis.ts +194 -194
  62. package/src/line-chart/chart-line.ts +156 -156
  63. package/src/line-chart/chart-region.ts +90 -90
  64. package/src/line-chart/horizontal-axis.ts +151 -151
  65. package/src/line-chart/index.ts +39 -39
  66. package/src/line-chart/line-chart.ts +598 -598
  67. package/src/line-chart/shared.ts +473 -473
  68. package/src/line-chart/x-scale.ts +382 -382
  69. package/src/nodes.ts +18 -18
  70. package/src/pie-chart/index.ts +37 -37
  71. package/src/pie-chart/pie-chart.ts +296 -296
  72. package/src/pie-chart/pie-legend.ts +169 -169
  73. package/src/pie-chart/pie-ring.ts +848 -848
  74. package/src/pie-chart/shared.ts +496 -496
  75. package/dist/browser/chunks/chunk-XROBIRQK.js.map +0 -7
  76. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -1,296 +1,296 @@
1
- import {
2
- Rect,
3
- Types,
4
- component2d,
5
- easeOut,
6
- type ChildView,
7
- type DataRecord,
8
- type NodeOf,
9
- // Named so this component's emitted type reaches `Node2D` through the sdk, not core.
10
- type Node2D,
11
- type Node2DProps,
12
- type Timeline,
13
- } from "@motionscript/sdk"
14
-
15
- import { asParts, fresh } from "../kit/entries"
16
- import { beats, type Beat } from "../kit/motion"
17
- import { clampUnit } from "../kit/shared"
18
- import { PieLegend } from "./pie-legend"
19
- import { PieRing } from "./pie-ring"
20
- import {
21
- DEFAULT_PIE_CHART_THEME,
22
- lerpPieChartTheme,
23
- resolvePieChartTheme,
24
- resolvePieSlices,
25
- type PieChartConfig,
26
- type PieChartTheme,
27
- type PieLabelMode,
28
- type PieLabelPlacement,
29
- type PieSlice,
30
- } from "./shared"
31
-
32
- export interface PieChartProps extends Node2DProps {
33
- /** One row per slice — a plain record of field name to value. */
34
- data: DataRecord[]
35
- /** Key into each row for its slice caption, used on the labels and the legend. */
36
- categoryField: string
37
- /** Key into each row for its slice magnitude — the wedge's share of the ring. */
38
- valueField: string
39
- /**
40
- * The chart's visual theme — slice palette, ring geometry, label and centre
41
- * type, legend. Every branch is optional and deep-merged onto
42
- * {@link DEFAULT_PIE_CHART_THEME}.
43
- */
44
- theme?: PieChartTheme
45
- /** What the ring's labels say. */
46
- labelMode?: PieLabelMode
47
- /** Where they sit: inside their wedge, outside on a leader, or nowhere. */
48
- labelPlacement?: PieLabelPlacement
49
- /** Whether the legend column beside the ring is drawn at all. */
50
- showLegend?: boolean
51
- /** Gap between the ring and the legend, in px. */
52
- gap?: number
53
- /** Text set in the ring's hole — see `revealCenter`. */
54
- centerLabel?: string
55
- /** Smaller second line under {@link centerLabel}. */
56
- centerCaption?: string
57
- /**
58
- * Whether the hole's summary is drawn at rest. `true` (the default) is the
59
- * plain answer; a scene that wants the conclusion to land on a later beat sets
60
- * this `false` and runs `revealCenter` when it gets there.
61
- */
62
- showCenter?: boolean
63
- }
64
-
65
- /** What the selection reads: the rows, how they are cut, and the pops the commands move. */
66
- export interface PieChartValues {
67
- readonly data: DataRecord[]
68
- readonly categoryField: string
69
- readonly valueField: string
70
- readonly theme: PieChartConfig
71
- readonly pops: number[]
72
- }
73
-
74
- /**
75
- * How `enter` divides its duration: the wedges sweeping in, then the labels and
76
- * the legend arriving together. Shares of the whole, so they sum to 1.
77
- */
78
- const SWEEP_SHARE = 0.65
79
- const LABEL_SHARE = 0.35
80
-
81
- const SLICE_ARG = Types.number(0, { min: 0, step: 1 })
82
-
83
- /** The slices the ring and the legend draw, resolved from the rows and the palette. */
84
- function slicesOf(s: PieChartValues): PieSlice[] {
85
- return resolvePieSlices(s.data, s.categoryField, s.valueField, s.theme.sliceFills)
86
- }
87
-
88
- /** How far slice `index` is pushed out, in `[0, 1]`. */
89
- function popAt(s: PieChartValues, index: number): number {
90
- return clampUnit(s.pops[index] ?? 0)
91
- }
92
-
93
- /** Whether slice `index` is grown out past the ring's rim: past half way, so a slice mid-swell is not yet counted. */
94
- export function isSliceSelected(s: PieChartValues, index: number): boolean {
95
- return popAt(s, index) > 0.5
96
- }
97
-
98
- /**
99
- * How far each slice is dimmed by the selection, one entry per slice: 1 while
100
- * nothing is out, and for the selected slice itself; falling toward the theme's
101
- * `dimOpacity` for the rest in proportion to how far the selection has grown.
102
- */
103
- function sliceDims(s: PieChartValues): number[] {
104
- let focus = 0
105
- for (let i = 0; i < s.data.length; i++) focus = Math.max(focus, popAt(s, i))
106
- const dim = s.theme.spotlight.dimOpacity
107
- if (focus <= 0) return s.data.map(() => 1)
108
- return s.data.map((_, i) => 1 - (1 - clampUnit(dim)) * focus * (1 - popAt(s, i)))
109
- }
110
-
111
- type AnyTimeline = Timeline<any, any>
112
-
113
- /**
114
- * Every slice's pop eased from where it is to what `pop` asks for. A move that
115
- * changes nothing still takes its slot, writing nothing.
116
- */
117
- function popping(timeline: AnyTimeline, s: PieChartValues, pop: (index: number) => number) {
118
- const from = s.data.map((_, i) => popAt(s, i))
119
- const targets = s.data.map((_, i) => clampUnit(pop(i)))
120
- if (from.every((value, i) => value === targets[i])) return timeline.wait(timeline.duration)
121
- return timeline.animate((t) => ({ pops: from.map((value, i) => value + (targets[i] - value) * t) }))
122
- }
123
-
124
- /** The ring and the legend, found through the chart's children: the last row of them is the one it built. */
125
- function partsOf(children: readonly ChildView[]) {
126
- for (let i = children.length - 1; i >= 0; i--) {
127
- const row = children[i]
128
- const ring = Rect.is(row) ? row.children.find(PieRing.is) : undefined
129
- if (ring) return { ring, legend: row.children.find(PieLegend.is) ?? null }
130
- }
131
- return null
132
- }
133
-
134
- /**
135
- * A pie or donut chart, composed from two parts laid out by a horizontal-flow
136
- * {@link Rect} — the ring filling whatever the hugging legend leaves, both
137
- * centred:
138
- *
139
- * - {@link PieRing} — the wedges, each sized by its share of the total, with its
140
- * label inside its own band or pinned outside on a leader line, and the
141
- * summary in the hole.
142
- * - {@link PieLegend} — a column of swatch-and-caption rows naming each slice.
143
- *
144
- * **One mark per row, not per column**: a pie's palette is indexed by row and
145
- * there is nothing to hide, since dropping a slice would re-cut every other
146
- * wedge. `selectSlice` and its siblings are what a pie has instead — a slice
147
- * swells out of the ring and everything else dims behind it.
148
- *
149
- * **Resting state is the finished chart**; `enter` arms every part on its first
150
- * frame. The one exception is the hole's summary, which a scene can hold back
151
- * with `showCenter: false`, because it is usually the chart's *conclusion*.
152
- */
153
- export const PieChart = component2d({
154
- key: "pieChart",
155
- state: {
156
- data: fresh<DataRecord[]>(() => []),
157
- categoryField: "",
158
- valueField: "",
159
- theme: Types.custom<PieChartTheme, PieChartConfig>(DEFAULT_PIE_CHART_THEME, { resolve: resolvePieChartTheme, lerp: lerpPieChartTheme }),
160
- labelMode: Types.custom<PieLabelMode>("percent"),
161
- labelPlacement: Types.custom<PieLabelPlacement>("leader"),
162
- gap: 40,
163
- centerLabel: "",
164
- centerCaption: "",
165
- /**
166
- * How far each slice is pushed out of the ring, `0`–`1` per index. Held here
167
- * rather than on the ring because two parts read it: the wedges grow out by
168
- * it, and the legend entries dim by it. Seeded flush, one to a row.
169
- */
170
- pops: fresh<number[]>(() => []),
171
- /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
172
- showLegend: Types.boolean(true, { animatable: false }),
173
- /** Read as the chart is built: whether the hole's summary is drawn at rest. */
174
- showCenter: Types.boolean(true, { animatable: false }),
175
- },
176
- defaults: {
177
- // Its parts fill it; a node made of them would hug them.
178
- width: "fill",
179
- height: "fill",
180
- pops: (state) => slicesOf(state).map(() => 0),
181
- },
182
- allowedChildren: [],
183
- // The slice *structure* (one label and one legend row per row) is read once;
184
- // the slices themselves, their paint and the selection are bound, so a theme
185
- // tween re-paints and a `set` of `data` re-cuts the wedges.
186
- children: (state) => {
187
- const theme = () => state.theme
188
- const slices = () => slicesOf(state)
189
- const parts: Node2D[] = [
190
- new PieRing({
191
- width: "fill",
192
- height: "fill",
193
- slices,
194
- ring: () => theme().ring,
195
- label: () => theme().label,
196
- centerStyle: () => theme().center,
197
- labelMode: state.labelMode,
198
- labelPlacement: state.labelPlacement,
199
- pops: () => state.pops,
200
- dimOpacity: () => theme().spotlight.dimOpacity,
201
- centerLabel: state.centerLabel,
202
- centerCaption: state.centerCaption,
203
- centerReveal: state.showCenter === false ? 0 : 1,
204
- }) as unknown as Node2D,
205
- ]
206
- if (state.showLegend !== false) {
207
- parts.push(
208
- new PieLegend({
209
- slices,
210
- legend: () => theme().legend,
211
- dims: () => sliceDims(state),
212
- // The column's spacing is the theme's as the chart was built, like the
213
- // entries' type and chips.
214
- gap: state.theme.legend.itemGap / 3,
215
- padding: state.theme.legend.padding,
216
- }) as unknown as Node2D
217
- )
218
- }
219
- const row = new Rect({ flow: "horizontal", width: "fill", height: "fill", gap: state.gap, align: "center", children: parts })
220
- return asParts([row as unknown as Node2D])
221
- },
222
- commands: {
223
- /**
224
- * Animate the whole chart in: the wedges sweep in clockwise from the theme's
225
- * `startAngle`, then the labels fade up and the legend staggers in together.
226
- * Its first frame arms the wedges, the labels and the legend; the hole's
227
- * summary keeps its own signal. `duration` is the **whole** entrance.
228
- */
229
- enter: {
230
- args: {},
231
- // Held from the chart's arrival, so it never shows finished ahead of its draw.
232
- entrance: true,
233
- easing: easeOut(),
234
- run: (timeline, _state, children) => {
235
- const parts = partsOf(children)
236
- if (!parts) return timeline.wait(timeline.duration)
237
- const { ring, legend } = parts
238
- const sweep = timeline.duration * SWEEP_SHARE
239
- const labels = timeline.duration * LABEL_SHARE
240
- const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
241
- return beats(timeline, [
242
- at0(ring.to({ growth: 0, labelReveal: 0 })),
243
- ...(legend ? [at0(legend.run("arm"))] : []),
244
- { at: 0, duration: sweep, piece: ring.run("sweep") },
245
- { at: sweep, duration: labels, piece: ring.run("revealLabels") },
246
- ...(legend ? [{ at: sweep, duration: labels, piece: legend.run("enter") }] : []),
247
- ])
248
- },
249
- },
250
- /**
251
- * Fade the hole's summary up. Held out of `enter` on purpose: what sits in a
252
- * donut's hole is usually the chart's conclusion, so the scene lands it on
253
- * its own beat. Pair it with `showCenter: false`.
254
- */
255
- revealCenter: {
256
- args: {},
257
- easing: easeOut(),
258
- run: (timeline, _state, children) => partsOf(children)?.ring.run("revealCenter") ?? timeline.wait(timeline.duration),
259
- },
260
- /** Grow slice `index` out past the ring's rim and dim the rest, leaving any other selected slice where it is. */
261
- selectSlice: {
262
- args: { sliceIndex: SLICE_ARG },
263
- easing: easeOut(),
264
- run: (timeline, state) => {
265
- const index = timeline.args.sliceIndex
266
- return popping(timeline, state, (i) => (i === index ? 1 : popAt(state, i)))
267
- },
268
- },
269
- /** Settle slice `index` back flush with the ring. */
270
- deselectSlice: {
271
- args: { sliceIndex: SLICE_ARG },
272
- easing: easeOut(),
273
- run: (timeline, state) => {
274
- const index = timeline.args.sliceIndex
275
- return popping(timeline, state, (i) => (i === index ? 0 : popAt(state, i)))
276
- },
277
- },
278
- /** Move the focus to slice `index` alone, settling whatever else was out. */
279
- selectOnlySlice: {
280
- args: { sliceIndex: SLICE_ARG },
281
- easing: easeOut(),
282
- run: (timeline, state) => {
283
- const index = timeline.args.sliceIndex
284
- return popping(timeline, state, (i) => (i === index ? 1 : 0))
285
- },
286
- },
287
- /** Settle every slice flush and bring the whole chart back to full. */
288
- clearSelection: {
289
- args: {},
290
- easing: easeOut(),
291
- run: (timeline, state) => popping(timeline, state, () => 0),
292
- },
293
- },
294
- })
295
-
296
- export type PieChart = NodeOf<typeof PieChart>
1
+ import {
2
+ Rect,
3
+ Types,
4
+ component2d,
5
+ easeOut,
6
+ type ChildView,
7
+ type DataRecord,
8
+ type NodeOf,
9
+ // Named so this component's emitted type reaches `Node2D` through the sdk, not core.
10
+ type Node2D,
11
+ type Node2DProps,
12
+ type Timeline,
13
+ } from "@motionscript/sdk"
14
+
15
+ import { asParts, fresh } from "../kit/entries"
16
+ import { beats, type Beat } from "../kit/motion"
17
+ import { clampUnit } from "../kit/shared"
18
+ import { PieLegend } from "./pie-legend"
19
+ import { PieRing } from "./pie-ring"
20
+ import {
21
+ DEFAULT_PIE_CHART_THEME,
22
+ lerpPieChartTheme,
23
+ resolvePieChartTheme,
24
+ resolvePieSlices,
25
+ type PieChartConfig,
26
+ type PieChartTheme,
27
+ type PieLabelMode,
28
+ type PieLabelPlacement,
29
+ type PieSlice,
30
+ } from "./shared"
31
+
32
+ export interface PieChartProps extends Node2DProps {
33
+ /** One row per slice — a plain record of field name to value. */
34
+ data: DataRecord[]
35
+ /** Key into each row for its slice caption, used on the labels and the legend. */
36
+ categoryField: string
37
+ /** Key into each row for its slice magnitude — the wedge's share of the ring. */
38
+ valueField: string
39
+ /**
40
+ * The chart's visual theme — slice palette, ring geometry, label and centre
41
+ * type, legend. Every branch is optional and deep-merged onto
42
+ * {@link DEFAULT_PIE_CHART_THEME}.
43
+ */
44
+ theme?: PieChartTheme
45
+ /** What the ring's labels say. */
46
+ labelMode?: PieLabelMode
47
+ /** Where they sit: inside their wedge, outside on a leader, or nowhere. */
48
+ labelPlacement?: PieLabelPlacement
49
+ /** Whether the legend column beside the ring is drawn at all. */
50
+ showLegend?: boolean
51
+ /** Gap between the ring and the legend, in px. */
52
+ gap?: number
53
+ /** Text set in the ring's hole — see `revealCenter`. */
54
+ centerLabel?: string
55
+ /** Smaller second line under {@link centerLabel}. */
56
+ centerCaption?: string
57
+ /**
58
+ * Whether the hole's summary is drawn at rest. `true` (the default) is the
59
+ * plain answer; a scene that wants the conclusion to land on a later beat sets
60
+ * this `false` and runs `revealCenter` when it gets there.
61
+ */
62
+ showCenter?: boolean
63
+ }
64
+
65
+ /** What the selection reads: the rows, how they are cut, and the pops the commands move. */
66
+ export interface PieChartValues {
67
+ readonly data: DataRecord[]
68
+ readonly categoryField: string
69
+ readonly valueField: string
70
+ readonly theme: PieChartConfig
71
+ readonly pops: number[]
72
+ }
73
+
74
+ /**
75
+ * How `enter` divides its duration: the wedges sweeping in, then the labels and
76
+ * the legend arriving together. Shares of the whole, so they sum to 1.
77
+ */
78
+ const SWEEP_SHARE = 0.65
79
+ const LABEL_SHARE = 0.35
80
+
81
+ const SLICE_ARG = Types.number(0, { min: 0, step: 1 })
82
+
83
+ /** The slices the ring and the legend draw, resolved from the rows and the palette. */
84
+ function slicesOf(s: PieChartValues): PieSlice[] {
85
+ return resolvePieSlices(s.data, s.categoryField, s.valueField, s.theme.sliceFills)
86
+ }
87
+
88
+ /** How far slice `index` is pushed out, in `[0, 1]`. */
89
+ function popAt(s: PieChartValues, index: number): number {
90
+ return clampUnit(s.pops[index] ?? 0)
91
+ }
92
+
93
+ /** Whether slice `index` is grown out past the ring's rim: past half way, so a slice mid-swell is not yet counted. */
94
+ export function isSliceSelected(s: PieChartValues, index: number): boolean {
95
+ return popAt(s, index) > 0.5
96
+ }
97
+
98
+ /**
99
+ * How far each slice is dimmed by the selection, one entry per slice: 1 while
100
+ * nothing is out, and for the selected slice itself; falling toward the theme's
101
+ * `dimOpacity` for the rest in proportion to how far the selection has grown.
102
+ */
103
+ function sliceDims(s: PieChartValues): number[] {
104
+ let focus = 0
105
+ for (let i = 0; i < s.data.length; i++) focus = Math.max(focus, popAt(s, i))
106
+ const dim = s.theme.spotlight.dimOpacity
107
+ if (focus <= 0) return s.data.map(() => 1)
108
+ return s.data.map((_, i) => 1 - (1 - clampUnit(dim)) * focus * (1 - popAt(s, i)))
109
+ }
110
+
111
+ type AnyTimeline = Timeline<any, any>
112
+
113
+ /**
114
+ * Every slice's pop eased from where it is to what `pop` asks for. A move that
115
+ * changes nothing still takes its slot, writing nothing.
116
+ */
117
+ function popping(timeline: AnyTimeline, s: PieChartValues, pop: (index: number) => number) {
118
+ const from = s.data.map((_, i) => popAt(s, i))
119
+ const targets = s.data.map((_, i) => clampUnit(pop(i)))
120
+ if (from.every((value, i) => value === targets[i])) return timeline.wait(timeline.duration)
121
+ return timeline.animate((t) => ({ pops: from.map((value, i) => value + (targets[i] - value) * t) }))
122
+ }
123
+
124
+ /** The ring and the legend, found through the chart's children: the last row of them is the one it built. */
125
+ function partsOf(children: readonly ChildView[]) {
126
+ for (let i = children.length - 1; i >= 0; i--) {
127
+ const row = children[i]
128
+ const ring = Rect.is(row) ? row.children.find(PieRing.is) : undefined
129
+ if (ring) return { ring, legend: row.children.find(PieLegend.is) ?? null }
130
+ }
131
+ return null
132
+ }
133
+
134
+ /**
135
+ * A pie or donut chart, composed from two parts laid out by a horizontal-flow
136
+ * {@link Rect} — the ring filling whatever the hugging legend leaves, both
137
+ * centred:
138
+ *
139
+ * - {@link PieRing} — the wedges, each sized by its share of the total, with its
140
+ * label inside its own band or pinned outside on a leader line, and the
141
+ * summary in the hole.
142
+ * - {@link PieLegend} — a column of swatch-and-caption rows naming each slice.
143
+ *
144
+ * **One mark per row, not per column**: a pie's palette is indexed by row and
145
+ * there is nothing to hide, since dropping a slice would re-cut every other
146
+ * wedge. `selectSlice` and its siblings are what a pie has instead — a slice
147
+ * swells out of the ring and everything else dims behind it.
148
+ *
149
+ * **Resting state is the finished chart**; `enter` arms every part on its first
150
+ * frame. The one exception is the hole's summary, which a scene can hold back
151
+ * with `showCenter: false`, because it is usually the chart's *conclusion*.
152
+ */
153
+ export const PieChart = component2d({
154
+ key: "pieChart",
155
+ state: {
156
+ data: fresh<DataRecord[]>(() => []),
157
+ categoryField: "",
158
+ valueField: "",
159
+ theme: Types.custom<PieChartTheme, PieChartConfig>(DEFAULT_PIE_CHART_THEME, { resolve: resolvePieChartTheme, lerp: lerpPieChartTheme }),
160
+ labelMode: Types.custom<PieLabelMode>("percent"),
161
+ labelPlacement: Types.custom<PieLabelPlacement>("leader"),
162
+ gap: 40,
163
+ centerLabel: "",
164
+ centerCaption: "",
165
+ /**
166
+ * How far each slice is pushed out of the ring, `0`–`1` per index. Held here
167
+ * rather than on the ring because two parts read it: the wedges grow out by
168
+ * it, and the legend entries dim by it. Seeded flush, one to a row.
169
+ */
170
+ pops: fresh<number[]>(() => []),
171
+ /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
172
+ showLegend: Types.boolean(true, { animatable: false }),
173
+ /** Read as the chart is built: whether the hole's summary is drawn at rest. */
174
+ showCenter: Types.boolean(true, { animatable: false }),
175
+ },
176
+ defaults: {
177
+ // Its parts fill it; a node made of them would hug them.
178
+ width: "fill",
179
+ height: "fill",
180
+ pops: (state) => slicesOf(state).map(() => 0),
181
+ },
182
+ allowedChildren: [],
183
+ // The slice *structure* (one label and one legend row per row) is read once;
184
+ // the slices themselves, their paint and the selection are bound, so a theme
185
+ // tween re-paints and a `set` of `data` re-cuts the wedges.
186
+ children: (state) => {
187
+ const theme = () => state.theme
188
+ const slices = () => slicesOf(state)
189
+ const parts: Node2D[] = [
190
+ new PieRing({
191
+ width: "fill",
192
+ height: "fill",
193
+ slices,
194
+ ring: () => theme().ring,
195
+ label: () => theme().label,
196
+ centerStyle: () => theme().center,
197
+ labelMode: state.labelMode,
198
+ labelPlacement: state.labelPlacement,
199
+ pops: () => state.pops,
200
+ dimOpacity: () => theme().spotlight.dimOpacity,
201
+ centerLabel: state.centerLabel,
202
+ centerCaption: state.centerCaption,
203
+ centerReveal: state.showCenter === false ? 0 : 1,
204
+ }) as unknown as Node2D,
205
+ ]
206
+ if (state.showLegend !== false) {
207
+ parts.push(
208
+ new PieLegend({
209
+ slices,
210
+ legend: () => theme().legend,
211
+ dims: () => sliceDims(state),
212
+ // The column's spacing is the theme's as the chart was built, like the
213
+ // entries' type and chips.
214
+ gap: state.theme.legend.itemGap / 3,
215
+ padding: state.theme.legend.padding,
216
+ }) as unknown as Node2D
217
+ )
218
+ }
219
+ const row = new Rect({ flow: "horizontal", width: "fill", height: "fill", gap: state.gap, align: "center", children: parts })
220
+ return asParts([row as unknown as Node2D])
221
+ },
222
+ commands: {
223
+ /**
224
+ * Animate the whole chart in: the wedges sweep in clockwise from the theme's
225
+ * `startAngle`, then the labels fade up and the legend staggers in together.
226
+ * Its first frame arms the wedges, the labels and the legend; the hole's
227
+ * summary keeps its own signal. `duration` is the **whole** entrance.
228
+ */
229
+ enter: {
230
+ args: {},
231
+ // Held from the chart's arrival, so it never shows finished ahead of its draw.
232
+ entrance: true,
233
+ easing: easeOut(),
234
+ run: (timeline, _state, children) => {
235
+ const parts = partsOf(children)
236
+ if (!parts) return timeline.wait(timeline.duration)
237
+ const { ring, legend } = parts
238
+ const sweep = timeline.duration * SWEEP_SHARE
239
+ const labels = timeline.duration * LABEL_SHARE
240
+ const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
241
+ return beats(timeline, [
242
+ at0(ring.to({ growth: 0, labelReveal: 0 })),
243
+ ...(legend ? [at0(legend.run("arm"))] : []),
244
+ { at: 0, duration: sweep, piece: ring.run("sweep") },
245
+ { at: sweep, duration: labels, piece: ring.run("revealLabels") },
246
+ ...(legend ? [{ at: sweep, duration: labels, piece: legend.run("enter") }] : []),
247
+ ])
248
+ },
249
+ },
250
+ /**
251
+ * Fade the hole's summary up. Held out of `enter` on purpose: what sits in a
252
+ * donut's hole is usually the chart's conclusion, so the scene lands it on
253
+ * its own beat. Pair it with `showCenter: false`.
254
+ */
255
+ revealCenter: {
256
+ args: {},
257
+ easing: easeOut(),
258
+ run: (timeline, _state, children) => partsOf(children)?.ring.run("revealCenter") ?? timeline.wait(timeline.duration),
259
+ },
260
+ /** Grow slice `index` out past the ring's rim and dim the rest, leaving any other selected slice where it is. */
261
+ selectSlice: {
262
+ args: { sliceIndex: SLICE_ARG },
263
+ easing: easeOut(),
264
+ run: (timeline, state) => {
265
+ const index = timeline.args.sliceIndex
266
+ return popping(timeline, state, (i) => (i === index ? 1 : popAt(state, i)))
267
+ },
268
+ },
269
+ /** Settle slice `index` back flush with the ring. */
270
+ deselectSlice: {
271
+ args: { sliceIndex: SLICE_ARG },
272
+ easing: easeOut(),
273
+ run: (timeline, state) => {
274
+ const index = timeline.args.sliceIndex
275
+ return popping(timeline, state, (i) => (i === index ? 0 : popAt(state, i)))
276
+ },
277
+ },
278
+ /** Move the focus to slice `index` alone, settling whatever else was out. */
279
+ selectOnlySlice: {
280
+ args: { sliceIndex: SLICE_ARG },
281
+ easing: easeOut(),
282
+ run: (timeline, state) => {
283
+ const index = timeline.args.sliceIndex
284
+ return popping(timeline, state, (i) => (i === index ? 1 : 0))
285
+ },
286
+ },
287
+ /** Settle every slice flush and bring the whole chart back to full. */
288
+ clearSelection: {
289
+ args: {},
290
+ easing: easeOut(),
291
+ run: (timeline, state) => popping(timeline, state, () => 0),
292
+ },
293
+ },
294
+ })
295
+
296
+ export type PieChart = NodeOf<typeof PieChart>