@motionscript/charts 0.1.0-alpha.0 → 0.1.0-alpha.10

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 (152) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/NOTICE +49 -0
  3. package/README.md +99 -0
  4. package/dist/bar-chart/bar-chart.d.ts +133 -228
  5. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  6. package/dist/bar-chart/bar-chart.js +386 -565
  7. package/dist/bar-chart/bar-chart.js.map +1 -1
  8. package/dist/bar-chart/category-axis.d.ts +35 -41
  9. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  10. package/dist/bar-chart/category-axis.js +93 -115
  11. package/dist/bar-chart/category-axis.js.map +1 -1
  12. package/dist/bar-chart/chart-bars.d.ts +50 -70
  13. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  14. package/dist/bar-chart/chart-bars.js +155 -210
  15. package/dist/bar-chart/chart-bars.js.map +1 -1
  16. package/dist/bar-chart/index.d.ts +2 -2
  17. package/dist/bar-chart/index.d.ts.map +1 -1
  18. package/dist/bar-chart/index.js +1 -1
  19. package/dist/bar-chart/index.js.map +1 -1
  20. package/dist/bar-chart/shared.d.ts +7 -7
  21. package/dist/bar-chart/shared.d.ts.map +1 -1
  22. package/dist/bar-chart/shared.js +7 -7
  23. package/dist/bar-chart/shared.js.map +1 -1
  24. package/dist/browser/chunks/chunk-TJNHLQGV.js +2 -0
  25. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +7 -0
  26. package/dist/browser/index.js +1 -1
  27. package/dist/browser/index.js.map +3 -3
  28. package/dist/browser/kit.js +1 -1
  29. package/dist/browser/manifest.json +4 -3
  30. package/dist/engine.d.ts +10 -8
  31. package/dist/engine.d.ts.map +1 -1
  32. package/dist/engine.js +10 -8
  33. package/dist/engine.js.map +1 -1
  34. package/dist/kit/cartesian.d.ts +44 -0
  35. package/dist/kit/cartesian.d.ts.map +1 -0
  36. package/dist/kit/cartesian.js +59 -0
  37. package/dist/kit/cartesian.js.map +1 -0
  38. package/dist/kit/chart-border.d.ts +10 -11
  39. package/dist/kit/chart-border.d.ts.map +1 -1
  40. package/dist/kit/chart-border.js +15 -32
  41. package/dist/kit/chart-border.js.map +1 -1
  42. package/dist/kit/chart-canvas.d.ts +32 -21
  43. package/dist/kit/chart-canvas.d.ts.map +1 -1
  44. package/dist/kit/chart-canvas.js +33 -90
  45. package/dist/kit/chart-canvas.js.map +1 -1
  46. package/dist/kit/chart-legend.d.ts +123 -85
  47. package/dist/kit/chart-legend.d.ts.map +1 -1
  48. package/dist/kit/chart-legend.js +137 -186
  49. package/dist/kit/chart-legend.js.map +1 -1
  50. package/dist/kit/entries.d.ts +17 -0
  51. package/dist/kit/entries.d.ts.map +1 -0
  52. package/dist/kit/entries.js +19 -0
  53. package/dist/kit/entries.js.map +1 -0
  54. package/dist/kit/motion.d.ts +48 -0
  55. package/dist/kit/motion.d.ts.map +1 -0
  56. package/dist/kit/motion.js +63 -0
  57. package/dist/kit/motion.js.map +1 -0
  58. package/dist/kit/paints.d.ts +10 -0
  59. package/dist/kit/paints.d.ts.map +1 -0
  60. package/dist/kit/paints.js +12 -0
  61. package/dist/kit/paints.js.map +1 -0
  62. package/dist/kit/shared.d.ts +61 -10
  63. package/dist/kit/shared.d.ts.map +1 -1
  64. package/dist/kit/shared.js +108 -13
  65. package/dist/kit/shared.js.map +1 -1
  66. package/dist/kit/vertical-axis.d.ts +48 -61
  67. package/dist/kit/vertical-axis.d.ts.map +1 -1
  68. package/dist/kit/vertical-axis.js +97 -132
  69. package/dist/kit/vertical-axis.js.map +1 -1
  70. package/dist/line-chart/chart-line.d.ts +60 -77
  71. package/dist/line-chart/chart-line.d.ts.map +1 -1
  72. package/dist/line-chart/chart-line.js +100 -121
  73. package/dist/line-chart/chart-line.js.map +1 -1
  74. package/dist/line-chart/chart-region.d.ts +32 -33
  75. package/dist/line-chart/chart-region.d.ts.map +1 -1
  76. package/dist/line-chart/chart-region.js +31 -65
  77. package/dist/line-chart/chart-region.js.map +1 -1
  78. package/dist/line-chart/horizontal-axis.d.ts +58 -64
  79. package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
  80. package/dist/line-chart/horizontal-axis.js +94 -123
  81. package/dist/line-chart/horizontal-axis.js.map +1 -1
  82. package/dist/line-chart/index.d.ts +3 -3
  83. package/dist/line-chart/index.d.ts.map +1 -1
  84. package/dist/line-chart/index.js +2 -2
  85. package/dist/line-chart/index.js.map +1 -1
  86. package/dist/line-chart/line-chart.d.ts +156 -303
  87. package/dist/line-chart/line-chart.d.ts.map +1 -1
  88. package/dist/line-chart/line-chart.js +410 -638
  89. package/dist/line-chart/line-chart.js.map +1 -1
  90. package/dist/line-chart/shared.d.ts +7 -7
  91. package/dist/line-chart/shared.d.ts.map +1 -1
  92. package/dist/line-chart/shared.js +23 -8
  93. package/dist/line-chart/shared.js.map +1 -1
  94. package/dist/line-chart/x-scale.d.ts +4 -11
  95. package/dist/line-chart/x-scale.d.ts.map +1 -1
  96. package/dist/line-chart/x-scale.js +16 -5
  97. package/dist/line-chart/x-scale.js.map +1 -1
  98. package/dist/nodes.d.ts +6 -9
  99. package/dist/nodes.d.ts.map +1 -1
  100. package/dist/nodes.js +7 -9
  101. package/dist/nodes.js.map +1 -1
  102. package/dist/pie-chart/index.d.ts +2 -2
  103. package/dist/pie-chart/index.d.ts.map +1 -1
  104. package/dist/pie-chart/index.js +1 -1
  105. package/dist/pie-chart/index.js.map +1 -1
  106. package/dist/pie-chart/pie-chart.d.ts +108 -126
  107. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  108. package/dist/pie-chart/pie-chart.js +202 -271
  109. package/dist/pie-chart/pie-chart.js.map +1 -1
  110. package/dist/pie-chart/pie-legend.d.ts +84 -31
  111. package/dist/pie-chart/pie-legend.d.ts.map +1 -1
  112. package/dist/pie-chart/pie-legend.js +93 -107
  113. package/dist/pie-chart/pie-legend.js.map +1 -1
  114. package/dist/pie-chart/pie-ring.d.ts +160 -122
  115. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  116. package/dist/pie-chart/pie-ring.js +333 -361
  117. package/dist/pie-chart/pie-ring.js.map +1 -1
  118. package/dist/pie-chart/shared.d.ts +12 -6
  119. package/dist/pie-chart/shared.d.ts.map +1 -1
  120. package/dist/pie-chart/shared.js +14 -8
  121. package/dist/pie-chart/shared.js.map +1 -1
  122. package/package.json +7 -4
  123. package/src/bar-chart/bar-chart.ts +424 -686
  124. package/src/bar-chart/category-axis.ts +96 -111
  125. package/src/bar-chart/chart-bars.ts +178 -201
  126. package/src/bar-chart/index.ts +2 -2
  127. package/src/bar-chart/shared.ts +7 -7
  128. package/src/engine.ts +10 -8
  129. package/src/kit/cartesian.ts +81 -0
  130. package/src/kit/chart-border.ts +23 -34
  131. package/src/kit/chart-canvas.ts +45 -80
  132. package/src/kit/chart-legend.ts +185 -206
  133. package/src/kit/entries.ts +20 -0
  134. package/src/kit/motion.ts +90 -0
  135. package/src/kit/paints.ts +15 -0
  136. package/src/kit/shared.ts +120 -13
  137. package/src/kit/vertical-axis.ts +111 -141
  138. package/src/line-chart/chart-line.ts +103 -155
  139. package/src/line-chart/chart-region.ts +36 -81
  140. package/src/line-chart/horizontal-axis.ts +102 -153
  141. package/src/line-chart/index.ts +3 -3
  142. package/src/line-chart/line-chart.ts +463 -759
  143. package/src/line-chart/shared.ts +22 -8
  144. package/src/line-chart/x-scale.ts +20 -5
  145. package/src/nodes.ts +8 -10
  146. package/src/pie-chart/index.ts +2 -2
  147. package/src/pie-chart/pie-chart.ts +225 -303
  148. package/src/pie-chart/pie-legend.ts +125 -115
  149. package/src/pie-chart/pie-ring.ts +388 -443
  150. package/src/pie-chart/shared.ts +14 -8
  151. package/dist/browser/chunks/chunk-ZMBOBHYL.js +0 -2
  152. package/dist/browser/chunks/chunk-ZMBOBHYL.js.map +0 -7
@@ -1,291 +1,222 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- import { Rect, command, node, Node2D, easeOut, property, } from "@motionscript/core";
1
+ import { Rect, Types, component2d, easeOut, } from "@motionscript/sdk";
2
+ import { asParts, fresh } from "../kit/entries.js";
3
+ import { beats } from "../kit/motion.js";
8
4
  import { clampUnit } from "../kit/shared.js";
9
5
  import { PieLegend } from "./pie-legend.js";
10
6
  import { PieRing } from "./pie-ring.js";
11
7
  import { DEFAULT_PIE_CHART_THEME, lerpPieChartTheme, resolvePieChartTheme, resolvePieSlices, } from "./shared.js";
12
- import { drive, hold, sequence as sequenceOf, together, } from "@motionscript/core/component";
13
8
  /**
14
- * How {@link PieChart.enter} divides its duration: the wedges sweeping in, then
15
- * the labels and the legend arriving together. Shares of the whole, so they sum
16
- * to 1 — the entrance takes exactly the time it was given, for the reason spelt
17
- * out on `LineChart.enter`.
9
+ * How `enter` divides its duration: the wedges sweeping in, then the labels and
10
+ * the legend arriving together. Shares of the whole, so they sum to 1.
18
11
  */
19
12
  const SWEEP_SHARE = 0.65;
20
13
  const LABEL_SHARE = 0.35;
14
+ const SLICE_ARG = Types.number(0, { min: 0, step: 1 });
15
+ /** The slices the ring and the legend draw, resolved from the rows and the palette. */
16
+ function slicesOf(s) {
17
+ return resolvePieSlices(s.data, s.categoryField, s.valueField, s.theme.sliceFills);
18
+ }
19
+ /** How far slice `index` is pushed out, in `[0, 1]`. */
20
+ function popAt(s, index) {
21
+ return clampUnit(s.pops[index] ?? 0);
22
+ }
23
+ /** Whether slice `index` is grown out past the ring's rim: past half way, so a slice mid-swell is not yet counted. */
24
+ export function isSliceSelected(s, index) {
25
+ return popAt(s, index) > 0.5;
26
+ }
21
27
  /**
22
- * A pie or donut chart, composed from two independent parts rather than one
23
- * monolithic draw — the third of the chart family, built the same way as the
24
- * line and bar charts beside it:
28
+ * How far each slice is dimmed by the selection, one entry per slice: 1 while
29
+ * nothing is out, and for the selected slice itself; falling toward the theme's
30
+ * `dimOpacity` for the rest in proportion to how far the selection has grown.
31
+ */
32
+ function sliceDims(s) {
33
+ let focus = 0;
34
+ for (let i = 0; i < s.data.length; i++)
35
+ focus = Math.max(focus, popAt(s, i));
36
+ const dim = s.theme.spotlight.dimOpacity;
37
+ if (focus <= 0)
38
+ return s.data.map(() => 1);
39
+ return s.data.map((_, i) => 1 - (1 - clampUnit(dim)) * focus * (1 - popAt(s, i)));
40
+ }
41
+ /**
42
+ * Every slice's pop eased from where it is to what `pop` asks for. A move that
43
+ * changes nothing still takes its slot, writing nothing.
44
+ */
45
+ function popping(timeline, s, pop) {
46
+ const from = s.data.map((_, i) => popAt(s, i));
47
+ const targets = s.data.map((_, i) => clampUnit(pop(i)));
48
+ if (from.every((value, i) => value === targets[i]))
49
+ return timeline.wait(timeline.duration);
50
+ return timeline.animate((t) => ({ pops: from.map((value, i) => value + (targets[i] - value) * t) }));
51
+ }
52
+ /** The ring and the legend, found through the chart's children: the last row of them is the one it built. */
53
+ function partsOf(children) {
54
+ for (let i = children.length - 1; i >= 0; i--) {
55
+ const row = children[i];
56
+ const ring = Rect.is(row) ? row.children.find(PieRing.is) : undefined;
57
+ if (ring)
58
+ return { ring, legend: row.children.find(PieLegend.is) ?? null };
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * A pie or donut chart, composed from two parts laid out by a horizontal-flow
64
+ * {@link Rect} — the ring filling whatever the hugging legend leaves, both
65
+ * centred:
25
66
  *
26
67
  * - {@link PieRing} — the wedges, each sized by its share of the total, with its
27
- * label set inside its own band or pinned outside on a leader line, and the
28
- * summary in the hole. The only part of the family that draws a path.
68
+ * label inside its own band or pinned outside on a leader line, and the
69
+ * summary in the hole.
29
70
  * - {@link PieLegend} — a column of swatch-and-caption rows naming each slice.
30
71
  *
31
- * The two are laid out by a plain horizontal-flow {@link Rect}: the ring fills whatever the
32
- * hugging legend leaves, both centred, so the chart re-flows if its box changes.
33
- *
34
- * **One mark per row, not per column** — which is what makes this the odd one of
35
- * the three. A line or a bar chart plots *series*, so its colours belong to
36
- * columns and hiding one is a sensible thing to ask for. A pie plots rows, so its
37
- * palette is indexed by row and there is nothing to hide: dropping a slice would
38
- * silently re-cut every other wedge's share, which is a different chart rather
39
- * than the same chart with a series switched off. {@link selectSlice} and its
40
- * siblings are what a pie has instead — a slice swells out of the ring, its hole
41
- * edge staying put, and everything else dims behind it.
72
+ * **One mark per row, not per column**: a pie's palette is indexed by row and
73
+ * there is nothing to hide, since dropping a slice would re-cut every other
74
+ * wedge. `selectSlice` and its siblings are what a pie has instead — a slice
75
+ * swells out of the ring and everything else dims behind it.
42
76
  *
43
- * **Resting state is the finished chart** — the same bargain the other two make:
44
- * the studio paints a node's Initial state on a paused canvas, so a chart that
45
- * only appeared once a command had run would be edited blind. {@link enter} arms
46
- * every part back to its pre-entrance state before animating. The one exception
47
- * is the hole's summary, which a scene can hold back with `showCenter: false`
48
- * because it is usually the chart's *conclusion* rather than part of its setup.
77
+ * **Resting state is the finished chart**; `enter` arms every part on its first
78
+ * frame. The one exception is the hole's summary, which a scene can hold back
79
+ * with `showCenter: false`, because it is usually the chart's *conclusion*.
49
80
  */
50
- let PieChart = class PieChart extends Node2D {
51
- // ---- Part handles (built in the constructor) --------------------------
52
- ring;
53
- legend = null;
54
- /** `this.theme` read back through its *resolved* shape — see `LineChart.theme`. */
55
- get resolvedTheme() {
56
- return this.theme;
57
- }
58
- /** The slices the ring and the legend draw, resolved from the rows and the palette. */
59
- get slices() {
60
- return resolvePieSlices(this.data, this.categoryField, this.valueField, this.resolvedTheme.sliceFills);
61
- }
62
- /**
63
- * How far each slice is dimmed by the current selection, one entry per slice —
64
- * 1 while nothing is selected, and for the selected slice itself; falling
65
- * toward the theme's `dimOpacity` for the rest in proportion to how far the
66
- * selection has grown.
67
- */
68
- sliceDims() {
69
- let focus = 0;
70
- for (let i = 0; i < this.data.length; i++) {
71
- focus = Math.max(focus, this.popAt(i));
72
- }
73
- const dim = this.resolvedTheme.spotlight.dimOpacity;
74
- if (focus <= 0)
75
- return this.data.map(() => 1);
76
- return this.data.map((_, i) => 1 - (1 - clampUnit(dim)) * focus * (1 - this.popAt(i)));
77
- }
78
- /** How far slice `index` is currently pushed out, in `[0, 1]`. */
79
- popAt(index) {
80
- return clampUnit(this.pops[index] ?? 0);
81
- }
82
- constructor(props) {
83
- super(props);
84
- // The whole chart is composed from plain props/`data` — no context needed,
85
- // so the parts are built here (super() applied the props above). The slice
86
- // *structure* (one wedge and one legend row each) is read once, like every
87
- // other chart's series list; everything the parts draw with is passed as a
88
- // closure, so `chart.to({ theme })` genuinely re-paints the ring.
89
- const slices = this.slices;
90
- const showLegend = props?.showLegend !== false;
91
- this.pops = slices.map(() => 0);
92
- this.ring = new PieRing({
93
- width: "fill",
94
- height: "fill",
95
- slices,
96
- ring: () => this.resolvedTheme.ring,
97
- label: () => this.resolvedTheme.label,
98
- centerStyle: () => this.resolvedTheme.center,
99
- labelMode: this.labelMode,
100
- labelPlacement: this.labelPlacement,
101
- pops: () => this.pops,
102
- dimOpacity: () => this.resolvedTheme.spotlight.dimOpacity,
103
- centerLabel: this.centerLabel,
104
- centerCaption: this.centerCaption,
105
- centerReveal: props?.showCenter === false ? 0 : 1,
106
- });
107
- const children = [this.ring];
108
- if (showLegend) {
109
- this.legend = new PieLegend({
81
+ export const PieChart = component2d({
82
+ key: "pieChart",
83
+ state: {
84
+ data: fresh(() => []),
85
+ categoryField: "",
86
+ valueField: "",
87
+ theme: Types.custom(DEFAULT_PIE_CHART_THEME, { resolve: resolvePieChartTheme, lerp: lerpPieChartTheme }),
88
+ labelMode: Types.custom("percent"),
89
+ labelPlacement: Types.custom("leader"),
90
+ gap: 40,
91
+ centerLabel: "",
92
+ centerCaption: "",
93
+ /**
94
+ * How far each slice is pushed out of the ring, `0`–`1` per index. Held here
95
+ * rather than on the ring because two parts read it: the wedges grow out by
96
+ * it, and the legend entries dim by it. Seeded flush, one to a row.
97
+ */
98
+ pops: fresh(() => []),
99
+ /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
100
+ showLegend: Types.boolean(true, { animatable: false }),
101
+ /** Read as the chart is built: whether the hole's summary is drawn at rest. */
102
+ showCenter: Types.boolean(true, { animatable: false }),
103
+ },
104
+ defaults: {
105
+ // Its parts fill it; a node made of them would hug them.
106
+ width: "fill",
107
+ height: "fill",
108
+ pops: (state) => slicesOf(state).map(() => 0),
109
+ },
110
+ allowedChildren: [],
111
+ // The slice *structure* (one label and one legend row per row) is read once;
112
+ // the slices themselves, their paint and the selection are bound, so a theme
113
+ // tween re-paints and a `set` of `data` re-cuts the wedges.
114
+ children: (state) => {
115
+ const theme = () => state.theme;
116
+ const slices = () => slicesOf(state);
117
+ const parts = [
118
+ new PieRing({
119
+ width: "fill",
120
+ height: "fill",
121
+ slices,
122
+ ring: () => theme().ring,
123
+ label: () => theme().label,
124
+ centerStyle: () => theme().center,
125
+ labelMode: state.labelMode,
126
+ labelPlacement: state.labelPlacement,
127
+ pops: () => state.pops,
128
+ dimOpacity: () => theme().spotlight.dimOpacity,
129
+ centerLabel: state.centerLabel,
130
+ centerCaption: state.centerCaption,
131
+ centerReveal: state.showCenter === false ? 0 : 1,
132
+ }),
133
+ ];
134
+ if (state.showLegend !== false) {
135
+ parts.push(new PieLegend({
110
136
  slices,
111
- legend: () => this.resolvedTheme.legend,
112
- dims: () => this.sliceDims(),
113
- });
114
- children.push(this.legend);
137
+ legend: () => theme().legend,
138
+ dims: () => sliceDims(state),
139
+ // The column's spacing is the theme's as the chart was built, like the
140
+ // entries' type and chips.
141
+ gap: state.theme.legend.itemGap / 3,
142
+ padding: state.theme.legend.padding,
143
+ }));
115
144
  }
116
- this.add(new Rect({ flow: "horizontal",
117
- width: "fill",
118
- height: "fill",
119
- gap: this.gap,
120
- align: "center",
121
- children,
122
- }));
123
- }
124
- // ---- Orchestrated entrance --------------------------------------------
125
- /**
126
- * Prime every part at its pre-entrance state — no wedges swept, no labels, the
127
- * legend shifted and transparent.
128
- *
129
- * Called by {@link enter} before its first frame so nothing flashes ahead of
130
- * its beat. Idempotent, and public so a scene can hide a chart ahead of time
131
- * without starting the entrance. The hole's summary is deliberately untouched:
132
- * it has its own signal, so a scene that set it up to land later keeps that.
133
- */
134
- arm() {
135
- this.ring.set({ growth: 0, labelReveal: 0 });
136
- this.legend?.arm();
137
- }
138
- /**
139
- * Animate the whole chart in: the wedges sweep in clockwise from the theme's
140
- * `startAngle`, then the labels fade up and the legend staggers in together.
141
- *
142
- * `duration` is the **whole** entrance; the phases are shares of it.
143
- */
144
- enter(args) {
145
- const duration = args.duration;
146
- const easing = args.easing ?? easeOut();
147
- this.arm();
148
- return sequenceOf(this.ring.sweep({ duration: duration * SWEEP_SHARE, easing }), together(this.ring.revealLabels({ duration: duration * LABEL_SHARE, easing }), this.legend
149
- ? this.legend.enter({ duration: duration * LABEL_SHARE, easing })
150
- : hold(duration * LABEL_SHARE)));
151
- }
152
- /**
153
- * Fade the hole's summary up.
154
- *
155
- * Held out of {@link enter} on purpose: what sits in a donut's hole is usually
156
- * the chart's conclusion, so the scene lands it on the beat it belongs to
157
- * rather than giving it away with the first frame. Pair it with
158
- * `showCenter: false`, which is what leaves the hole clear to begin with.
159
- */
160
- revealCenter(args = {}) {
161
- return this.ring.revealCenter({ duration: args.duration ?? 0.6, easing: args.easing ?? easeOut() });
162
- }
163
- // ---- Selection ---------------------------------------------------------
164
- /** True when slice `index` is currently grown out past the ring's rim. */
165
- isSelected(index) {
166
- return this.popAt(index) > 0.5;
167
- }
168
- /**
169
- * Grow slice `index` out past the ring's rim and dim the rest of the chart
170
- * behind it, leaving any other selected slice where it is.
171
- */
172
- selectSlice(args) {
173
- const { sliceIndex: index } = args.data;
174
- const duration = args.duration ?? 0.45;
175
- const easing = args.easing ?? easeOut();
176
- return this.tweenPops((i) => (i === index ? 1 : this.popAt(i)), duration, easing);
177
- }
178
- /** Settle slice `index` back flush with the ring. */
179
- deselectSlice(args) {
180
- const { sliceIndex: index } = args.data;
181
- const duration = args.duration ?? 0.45;
182
- const easing = args.easing ?? easeOut();
183
- return this.tweenPops((i) => (i === index ? 0 : this.popAt(i)), duration, easing);
184
- }
185
- /** Move the focus to slice `index` alone, settling whatever else was out. */
186
- selectOnlySlice(args) {
187
- const { sliceIndex: index } = args.data;
188
- const duration = args.duration ?? 0.45;
189
- const easing = args.easing ?? easeOut();
190
- return this.tweenPops((i) => (i === index ? 1 : 0), duration, easing);
191
- }
192
- /** Settle every slice flush and bring the whole chart back to full. */
193
- clearSelection(args = {}) {
194
- const duration = args.duration ?? 0.45;
195
- const easing = args.easing ?? easeOut();
196
- return this.tweenPops(() => 0, duration, easing);
197
- }
198
- /**
199
- * Ease every slice's pop from where it is now to what `pop` asks for.
200
- *
201
- * Consumes its full duration even when nothing moves: in the studio a
202
- * command's duration is what the timeline lays out and what its parallel
203
- * siblings are timed against, so a no-op that took no frames would pull
204
- * everything after it forward.
205
- *
206
- * `from` and the targets are read when the command is *built*, which for a
207
- * driven scene is with the node in the state its own start time implies — so a
208
- * select that follows another starts from where that one left the ring.
209
- */
210
- tweenPops(pop, duration, easing) {
211
- const from = this.data.map((_, i) => this.popAt(i));
212
- const targets = this.data.map((_, i) => clampUnit(pop(i)));
213
- if (from.every((value, i) => value === targets[i]))
214
- return hold(duration);
215
- return drive(duration, (t) => {
216
- const e = easing(t);
217
- this.pops = from.map((value, i) => value + (targets[i] - value) * e);
218
- });
219
- }
220
- };
221
- __decorate([
222
- property({ default: () => [] })
223
- ], PieChart.prototype, "data", void 0);
224
- __decorate([
225
- property({ default: "" })
226
- ], PieChart.prototype, "categoryField", void 0);
227
- __decorate([
228
- property({ default: "" })
229
- ], PieChart.prototype, "valueField", void 0);
230
- __decorate([
231
- property({
232
- default: DEFAULT_PIE_CHART_THEME,
233
- mapper: resolvePieChartTheme,
234
- tween: lerpPieChartTheme,
235
- })
236
- ], PieChart.prototype, "theme", void 0);
237
- __decorate([
238
- property({ default: "percent" })
239
- ], PieChart.prototype, "labelMode", void 0);
240
- __decorate([
241
- property({ default: "leader" })
242
- ], PieChart.prototype, "labelPlacement", void 0);
243
- __decorate([
244
- property({ default: 40 })
245
- ], PieChart.prototype, "gap", void 0);
246
- __decorate([
247
- property({ default: "" })
248
- ], PieChart.prototype, "centerLabel", void 0);
249
- __decorate([
250
- property({ default: "" })
251
- ], PieChart.prototype, "centerCaption", void 0);
252
- __decorate([
253
- property({ default: () => [] })
254
- ], PieChart.prototype, "pops", void 0);
255
- __decorate([
256
- command()
257
- ], PieChart.prototype, "enter", null);
258
- __decorate([
259
- command()
260
- ], PieChart.prototype, "revealCenter", null);
261
- __decorate([
262
- command({ args: [{ key: "sliceIndex", kind: "number", default: 0, min: 0, step: 1 }] })
263
- ], PieChart.prototype, "selectSlice", null);
264
- __decorate([
265
- command({ args: [{ key: "sliceIndex", kind: "number", default: 0, min: 0, step: 1 }] })
266
- ], PieChart.prototype, "deselectSlice", null);
267
- __decorate([
268
- command({ args: [{ key: "sliceIndex", kind: "number", default: 0, min: 0, step: 1 }] })
269
- ], PieChart.prototype, "selectOnlySlice", null);
270
- __decorate([
271
- command()
272
- ], PieChart.prototype, "clearSelection", null);
273
- PieChart = __decorate([
274
- node({
275
- key: "pieChart",
276
- parentKey: "node",
277
- forkable: true,
278
- layout: {
279
- children: "freeform",
280
- defaultWidthMode: "fixed",
281
- defaultHeightMode: "fixed",
282
- acceptsChildren: true,
145
+ const row = new Rect({ flow: "horizontal", width: "fill", height: "fill", gap: state.gap, align: "center", children: parts });
146
+ return asParts([row]);
147
+ },
148
+ commands: {
149
+ /**
150
+ * Animate the whole chart in: the wedges sweep in clockwise from the theme's
151
+ * `startAngle`, then the labels fade up and the legend staggers in together.
152
+ * Its first frame arms the wedges, the labels and the legend; the hole's
153
+ * summary keeps its own signal. `duration` is the **whole** entrance.
154
+ */
155
+ enter: {
156
+ args: {},
157
+ // Held from the chart's arrival, so it never shows finished ahead of its draw.
158
+ entrance: true,
159
+ easing: easeOut(),
160
+ run: (timeline, _state, children) => {
161
+ const parts = partsOf(children);
162
+ if (!parts)
163
+ return timeline.wait(timeline.duration);
164
+ const { ring, legend } = parts;
165
+ const sweep = timeline.duration * SWEEP_SHARE;
166
+ const labels = timeline.duration * LABEL_SHARE;
167
+ const at0 = (piece) => ({ at: 0, duration: 0, piece });
168
+ return beats(timeline, [
169
+ at0(ring.to({ growth: 0, labelReveal: 0 })),
170
+ ...(legend ? [at0(legend.run("arm"))] : []),
171
+ { at: 0, duration: sweep, piece: ring.run("sweep") },
172
+ { at: sweep, duration: labels, piece: ring.run("revealLabels") },
173
+ ...(legend ? [{ at: sweep, duration: labels, piece: legend.run("enter") }] : []),
174
+ ]);
175
+ },
176
+ },
177
+ /**
178
+ * Fade the hole's summary up. Held out of `enter` on purpose: what sits in a
179
+ * donut's hole is usually the chart's conclusion, so the scene lands it on
180
+ * its own beat. Pair it with `showCenter: false`.
181
+ */
182
+ revealCenter: {
183
+ args: {},
184
+ easing: easeOut(),
185
+ run: (timeline, _state, children) => partsOf(children)?.ring.run("revealCenter") ?? timeline.wait(timeline.duration),
186
+ },
187
+ /** Grow slice `index` out past the ring's rim and dim the rest, leaving any other selected slice where it is. */
188
+ selectSlice: {
189
+ args: { sliceIndex: SLICE_ARG },
190
+ easing: easeOut(),
191
+ run: (timeline, state) => {
192
+ const index = timeline.args.sliceIndex;
193
+ return popping(timeline, state, (i) => (i === index ? 1 : popAt(state, i)));
194
+ },
195
+ },
196
+ /** Settle slice `index` back flush with the ring. */
197
+ deselectSlice: {
198
+ args: { sliceIndex: SLICE_ARG },
199
+ easing: easeOut(),
200
+ run: (timeline, state) => {
201
+ const index = timeline.args.sliceIndex;
202
+ return popping(timeline, state, (i) => (i === index ? 0 : popAt(state, i)));
203
+ },
204
+ },
205
+ /** Move the focus to slice `index` alone, settling whatever else was out. */
206
+ selectOnlySlice: {
207
+ args: { sliceIndex: SLICE_ARG },
208
+ easing: easeOut(),
209
+ run: (timeline, state) => {
210
+ const index = timeline.args.sliceIndex;
211
+ return popping(timeline, state, (i) => (i === index ? 1 : 0));
212
+ },
283
213
  },
284
- seed: {
285
- width: 680,
286
- height: 480,
214
+ /** Settle every slice flush and bring the whole chart back to full. */
215
+ clearSelection: {
216
+ args: {},
217
+ easing: easeOut(),
218
+ run: (timeline, state) => popping(timeline, state, () => 0),
287
219
  },
288
- })
289
- ], PieChart);
290
- export { PieChart };
220
+ },
221
+ });
291
222
  //# sourceMappingURL=pie-chart.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pie-chart.js","sourceRoot":"","sources":["../../src/pie-chart/pie-chart.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Rect,\n command,\n node,\n Node2D,\n easeOut,\n property,\n type CommandArgs,\n type DataRecord,\n type EasingFunction,\n type NodeConfig,\n type Node2DProps,\n} from \"@motionscript/core\"\n\nimport { clampUnit } from \"../kit/shared\"\nimport { PieLegend } from \"./pie-legend\"\nimport { PieRing } from \"./pie-ring\"\nimport {\n DEFAULT_PIE_CHART_THEME,\n lerpPieChartTheme,\n resolvePieChartTheme,\n resolvePieSlices,\n type PieChartConfig,\n type PieChartTheme,\n type PieLabelMode,\n type PieLabelPlacement,\n type PieSlice,\n} from \"./shared\"\nimport {\n drive,\n hold,\n sequence as sequenceOf,\n together,\n type Seekable,\n} from \"@motionscript/core/component\"\n\nexport interface PieChartProps extends Node2DProps {\n /** One row per slice — a plain record of field name to value. */\n data: DataRecord[]\n /** Key into each row for its slice caption, used on the labels and the legend. */\n categoryField: string\n /** Key into each row for its slice magnitude — the wedge's share of the ring. */\n valueField: string\n /**\n * The chart's visual theme — slice palette, ring geometry, label and centre\n * type, legend. Every branch is optional and deep-merged onto\n * {@link DEFAULT_PIE_CHART_THEME}.\n */\n theme?: PieChartTheme\n /** What the ring's labels say. */\n labelMode?: PieLabelMode\n /** Where they sit: inside their wedge, outside on a leader, or nowhere. */\n labelPlacement?: PieLabelPlacement\n /** Whether the legend column beside the ring is drawn at all. */\n showLegend?: boolean\n /** Gap between the ring and the legend, in px. */\n gap?: number\n /** Text set in the ring's hole — see {@link revealCenter}. */\n centerLabel?: string\n /** Smaller second line under {@link centerLabel}. */\n centerCaption?: string\n /**\n * Whether the hole's summary is drawn at rest. `true` (the default) is the\n * plain answer; a scene that wants the conclusion to land on a later beat sets\n * this `false` and calls {@link revealCenter} when it gets there.\n */\n showCenter?: boolean\n}\n\n/**\n * How {@link PieChart.enter} divides its duration: the wedges sweeping in, then\n * the labels and the legend arriving together. Shares of the whole, so they sum\n * to 1 — the entrance takes exactly the time it was given, for the reason spelt\n * out on `LineChart.enter`.\n */\nconst SWEEP_SHARE = 0.65\nconst LABEL_SHARE = 0.35\n\n/**\n * A pie or donut chart, composed from two independent parts rather than one\n * monolithic draw — the third of the chart family, built the same way as the\n * line and bar charts beside it:\n *\n * - {@link PieRing} — the wedges, each sized by its share of the total, with its\n * label set inside its own band or pinned outside on a leader line, and the\n * summary in the hole. The only part of the family that draws a path.\n * - {@link PieLegend} — a column of swatch-and-caption rows naming each slice.\n *\n * The two are laid out by a plain horizontal-flow {@link Rect}: the ring fills whatever the\n * hugging legend leaves, both centred, so the chart re-flows if its box changes.\n *\n * **One mark per row, not per column** — which is what makes this the odd one of\n * the three. A line or a bar chart plots *series*, so its colours belong to\n * columns and hiding one is a sensible thing to ask for. A pie plots rows, so its\n * palette is indexed by row and there is nothing to hide: dropping a slice would\n * silently re-cut every other wedge's share, which is a different chart rather\n * than the same chart with a series switched off. {@link selectSlice} and its\n * siblings are what a pie has instead — a slice swells out of the ring, its hole\n * edge staying put, and everything else dims behind it.\n *\n * **Resting state is the finished chart** — the same bargain the other two make:\n * the studio paints a node's Initial state on a paused canvas, so a chart that\n * only appeared once a command had run would be edited blind. {@link enter} arms\n * every part back to its pre-entrance state before animating. The one exception\n * is the hole's summary, which a scene can hold back with `showCenter: false`\n * because it is usually the chart's *conclusion* rather than part of its setup.\n */\n@node({\n key: \"pieChart\",\n parentKey: \"node\",\n forkable: true,\n layout: {\n children: \"freeform\",\n defaultWidthMode: \"fixed\",\n defaultHeightMode: \"fixed\",\n acceptsChildren: true,\n },\n seed: {\n width: 680,\n height: 480,\n },\n})\nexport class PieChart extends Node2D<PieChartProps> {\n @property({ default: () => [] as DataRecord[] }) declare data: DataRecord[]\n @property({ default: \"\" }) declare categoryField: string\n @property({ default: \"\" }) declare valueField: string\n @property({\n default: DEFAULT_PIE_CHART_THEME,\n mapper: resolvePieChartTheme,\n tween: lerpPieChartTheme,\n })\n declare theme: PieChartTheme\n @property({ default: \"percent\" }) declare labelMode: PieLabelMode\n @property({ default: \"leader\" }) declare labelPlacement: PieLabelPlacement\n @property({ default: 40 }) declare gap: number\n @property({ default: \"\" }) declare centerLabel: string\n @property({ default: \"\" }) declare centerCaption: string\n\n /**\n * How far each slice is currently pushed out of the ring, `0`–`1` per index.\n *\n * Held here rather than on the ring because two parts read it: the wedges grow\n * out by it, and the legend entries dim by it. One signal, one tween, and the\n * two can't drift.\n */\n @property({ default: () => [] as number[] }) declare pops: number[]\n\n // ---- Part handles (built in the constructor) --------------------------\n private ring: PieRing\n private legend: PieLegend | null = null\n\n /** `this.theme` read back through its *resolved* shape — see `LineChart.theme`. */\n private get resolvedTheme(): PieChartConfig {\n return this.theme as unknown as PieChartConfig\n }\n\n /** The slices the ring and the legend draw, resolved from the rows and the palette. */\n private get slices(): PieSlice[] {\n return resolvePieSlices(\n this.data,\n this.categoryField,\n this.valueField,\n this.resolvedTheme.sliceFills\n )\n }\n\n /**\n * How far each slice is dimmed by the current selection, one entry per slice —\n * 1 while nothing is selected, and for the selected slice itself; falling\n * toward the theme's `dimOpacity` for the rest in proportion to how far the\n * selection has grown.\n */\n private sliceDims(): number[] {\n let focus = 0\n for (let i = 0; i < this.data.length; i++) {\n focus = Math.max(focus, this.popAt(i))\n }\n const dim = this.resolvedTheme.spotlight.dimOpacity\n if (focus <= 0) return this.data.map(() => 1)\n return this.data.map(\n (_, i) => 1 - (1 - clampUnit(dim)) * focus * (1 - this.popAt(i))\n )\n }\n\n /** How far slice `index` is currently pushed out, in `[0, 1]`. */\n private popAt(index: number): number {\n return clampUnit(this.pops[index] ?? 0)\n }\n\n constructor(props?: NodeConfig<PieChart, PieChartProps>) {\n super(props)\n\n // The whole chart is composed from plain props/`data` — no context needed,\n // so the parts are built here (super() applied the props above). The slice\n // *structure* (one wedge and one legend row each) is read once, like every\n // other chart's series list; everything the parts draw with is passed as a\n // closure, so `chart.to({ theme })` genuinely re-paints the ring.\n const slices = this.slices\n const showLegend = props?.showLegend !== false\n this.pops = slices.map(() => 0)\n\n this.ring = new PieRing({\n width: \"fill\",\n height: \"fill\",\n slices,\n ring: () => this.resolvedTheme.ring,\n label: () => this.resolvedTheme.label,\n centerStyle: () => this.resolvedTheme.center,\n labelMode: this.labelMode,\n labelPlacement: this.labelPlacement,\n pops: () => this.pops,\n dimOpacity: () => this.resolvedTheme.spotlight.dimOpacity,\n centerLabel: this.centerLabel,\n centerCaption: this.centerCaption,\n centerReveal: props?.showCenter === false ? 0 : 1,\n })\n\n const children: Node2D[] = [this.ring]\n if (showLegend) {\n this.legend = new PieLegend({\n slices,\n legend: () => this.resolvedTheme.legend,\n dims: () => this.sliceDims(),\n })\n children.push(this.legend)\n }\n\n this.add(\n new Rect({ flow: \"horizontal\",\n width: \"fill\",\n height: \"fill\",\n gap: this.gap,\n align: \"center\",\n children,\n })\n )\n }\n\n // ---- Orchestrated entrance --------------------------------------------\n\n /**\n * Prime every part at its pre-entrance state — no wedges swept, no labels, the\n * legend shifted and transparent.\n *\n * Called by {@link enter} before its first frame so nothing flashes ahead of\n * its beat. Idempotent, and public so a scene can hide a chart ahead of time\n * without starting the entrance. The hole's summary is deliberately untouched:\n * it has its own signal, so a scene that set it up to land later keeps that.\n */\n arm(): void {\n this.ring.set({ growth: 0, labelReveal: 0 })\n this.legend?.arm()\n }\n\n /**\n * Animate the whole chart in: the wedges sweep in clockwise from the theme's\n * `startAngle`, then the labels fade up and the legend staggers in together.\n *\n * `duration` is the **whole** entrance; the phases are shares of it.\n */\n @command()\n enter(\n args: CommandArgs<Record<string, never>> & { duration: number }\n ): Seekable {\n const duration = args.duration\n const easing = args.easing ?? easeOut()\n this.arm()\n\n return sequenceOf(\n this.ring.sweep({ duration: duration * SWEEP_SHARE, easing }),\n together(\n this.ring.revealLabels({ duration: duration * LABEL_SHARE, easing }),\n this.legend\n ? this.legend.enter({ duration: duration * LABEL_SHARE, easing })\n : hold(duration * LABEL_SHARE)\n )\n )\n }\n\n /**\n * Fade the hole's summary up.\n *\n * Held out of {@link enter} on purpose: what sits in a donut's hole is usually\n * the chart's conclusion, so the scene lands it on the beat it belongs to\n * rather than giving it away with the first frame. Pair it with\n * `showCenter: false`, which is what leaves the hole clear to begin with.\n */\n @command()\n revealCenter(\n args: CommandArgs<Record<string, never>> & { duration?: number } = {}\n ): Seekable {\n return this.ring.revealCenter({ duration: args.duration ?? 0.6, easing: args.easing ?? easeOut() })\n }\n\n // ---- Selection ---------------------------------------------------------\n\n /** True when slice `index` is currently grown out past the ring's rim. */\n isSelected(index: number): boolean {\n return this.popAt(index) > 0.5\n }\n\n /**\n * Grow slice `index` out past the ring's rim and dim the rest of the chart\n * behind it, leaving any other selected slice where it is.\n */\n @command({ args: [{ key: \"sliceIndex\", kind: \"number\", default: 0, min: 0, step: 1 }] })\n selectSlice(\n args: CommandArgs<{ sliceIndex: number }> & { duration?: number }\n ): Seekable {\n const { sliceIndex: index } = args.data as { sliceIndex: number }\n const duration = args.duration ?? 0.45\n const easing = args.easing ?? easeOut()\n return this.tweenPops((i) => (i === index ? 1 : this.popAt(i)), duration, easing)\n }\n\n /** Settle slice `index` back flush with the ring. */\n @command({ args: [{ key: \"sliceIndex\", kind: \"number\", default: 0, min: 0, step: 1 }] })\n deselectSlice(\n args: CommandArgs<{ sliceIndex: number }> & { duration?: number }\n ): Seekable {\n const { sliceIndex: index } = args.data as { sliceIndex: number }\n const duration = args.duration ?? 0.45\n const easing = args.easing ?? easeOut()\n return this.tweenPops((i) => (i === index ? 0 : this.popAt(i)), duration, easing)\n }\n\n /** Move the focus to slice `index` alone, settling whatever else was out. */\n @command({ args: [{ key: \"sliceIndex\", kind: \"number\", default: 0, min: 0, step: 1 }] })\n selectOnlySlice(\n args: CommandArgs<{ sliceIndex: number }> & { duration?: number }\n ): Seekable {\n const { sliceIndex: index } = args.data as { sliceIndex: number }\n const duration = args.duration ?? 0.45\n const easing = args.easing ?? easeOut()\n return this.tweenPops((i) => (i === index ? 1 : 0), duration, easing)\n }\n\n /** Settle every slice flush and bring the whole chart back to full. */\n @command()\n clearSelection(\n args: CommandArgs<Record<string, never>> & { duration?: number } = {}\n ): Seekable {\n const duration = args.duration ?? 0.45\n const easing = args.easing ?? easeOut()\n return this.tweenPops(() => 0, duration, easing)\n }\n\n /**\n * Ease every slice's pop from where it is now to what `pop` asks for.\n *\n * Consumes its full duration even when nothing moves: in the studio a\n * command's duration is what the timeline lays out and what its parallel\n * siblings are timed against, so a no-op that took no frames would pull\n * everything after it forward.\n *\n * `from` and the targets are read when the command is *built*, which for a\n * driven scene is with the node in the state its own start time implies — so a\n * select that follows another starts from where that one left the ring.\n */\n private tweenPops(\n pop: (index: number) => number,\n duration: number,\n easing: EasingFunction\n ): Seekable {\n const from = this.data.map((_, i) => this.popAt(i))\n const targets = this.data.map((_, i) => clampUnit(pop(i)))\n if (from.every((value, i) => value === targets[i])) return hold(duration)\n\n return drive(duration, (t) => {\n const e = easing(t)\n this.pops = from.map((value, i) => value + (targets[i] - value) * e)\n })\n }\n}\n"]}
1
+ {"version":3,"file":"pie-chart.js","sourceRoot":"","sources":["../../src/pie-chart/pie-chart.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Rect,\n Types,\n component2d,\n easeOut,\n type ChildView,\n type DataRecord,\n type NodeOf,\n // Named so this component's emitted type reaches `Node2D` through the sdk, not core.\n type Node2D,\n type Node2DProps,\n type Timeline,\n} from \"@motionscript/sdk\"\n\nimport { asParts, fresh } from \"../kit/entries\"\nimport { beats, type Beat } from \"../kit/motion\"\nimport { clampUnit } from \"../kit/shared\"\nimport { PieLegend } from \"./pie-legend\"\nimport { PieRing } from \"./pie-ring\"\nimport {\n DEFAULT_PIE_CHART_THEME,\n lerpPieChartTheme,\n resolvePieChartTheme,\n resolvePieSlices,\n type PieChartConfig,\n type PieChartTheme,\n type PieLabelMode,\n type PieLabelPlacement,\n type PieSlice,\n} from \"./shared\"\n\nexport interface PieChartProps extends Node2DProps {\n /** One row per slice — a plain record of field name to value. */\n data: DataRecord[]\n /** Key into each row for its slice caption, used on the labels and the legend. */\n categoryField: string\n /** Key into each row for its slice magnitude — the wedge's share of the ring. */\n valueField: string\n /**\n * The chart's visual theme — slice palette, ring geometry, label and centre\n * type, legend. Every branch is optional and deep-merged onto\n * {@link DEFAULT_PIE_CHART_THEME}.\n */\n theme?: PieChartTheme\n /** What the ring's labels say. */\n labelMode?: PieLabelMode\n /** Where they sit: inside their wedge, outside on a leader, or nowhere. */\n labelPlacement?: PieLabelPlacement\n /** Whether the legend column beside the ring is drawn at all. */\n showLegend?: boolean\n /** Gap between the ring and the legend, in px. */\n gap?: number\n /** Text set in the ring's hole — see `revealCenter`. */\n centerLabel?: string\n /** Smaller second line under {@link centerLabel}. */\n centerCaption?: string\n /**\n * Whether the hole's summary is drawn at rest. `true` (the default) is the\n * plain answer; a scene that wants the conclusion to land on a later beat sets\n * this `false` and runs `revealCenter` when it gets there.\n */\n showCenter?: boolean\n}\n\n/** What the selection reads: the rows, how they are cut, and the pops the commands move. */\nexport interface PieChartValues {\n readonly data: DataRecord[]\n readonly categoryField: string\n readonly valueField: string\n readonly theme: PieChartConfig\n readonly pops: number[]\n}\n\n/**\n * How `enter` divides its duration: the wedges sweeping in, then the labels and\n * the legend arriving together. Shares of the whole, so they sum to 1.\n */\nconst SWEEP_SHARE = 0.65\nconst LABEL_SHARE = 0.35\n\nconst SLICE_ARG = Types.number(0, { min: 0, step: 1 })\n\n/** The slices the ring and the legend draw, resolved from the rows and the palette. */\nfunction slicesOf(s: PieChartValues): PieSlice[] {\n return resolvePieSlices(s.data, s.categoryField, s.valueField, s.theme.sliceFills)\n}\n\n/** How far slice `index` is pushed out, in `[0, 1]`. */\nfunction popAt(s: PieChartValues, index: number): number {\n return clampUnit(s.pops[index] ?? 0)\n}\n\n/** Whether slice `index` is grown out past the ring's rim: past half way, so a slice mid-swell is not yet counted. */\nexport function isSliceSelected(s: PieChartValues, index: number): boolean {\n return popAt(s, index) > 0.5\n}\n\n/**\n * How far each slice is dimmed by the selection, one entry per slice: 1 while\n * nothing is out, and for the selected slice itself; falling toward the theme's\n * `dimOpacity` for the rest in proportion to how far the selection has grown.\n */\nfunction sliceDims(s: PieChartValues): number[] {\n let focus = 0\n for (let i = 0; i < s.data.length; i++) focus = Math.max(focus, popAt(s, i))\n const dim = s.theme.spotlight.dimOpacity\n if (focus <= 0) return s.data.map(() => 1)\n return s.data.map((_, i) => 1 - (1 - clampUnit(dim)) * focus * (1 - popAt(s, i)))\n}\n\ntype AnyTimeline = Timeline<any, any>\n\n/**\n * Every slice's pop eased from where it is to what `pop` asks for. A move that\n * changes nothing still takes its slot, writing nothing.\n */\nfunction popping(timeline: AnyTimeline, s: PieChartValues, pop: (index: number) => number) {\n const from = s.data.map((_, i) => popAt(s, i))\n const targets = s.data.map((_, i) => clampUnit(pop(i)))\n if (from.every((value, i) => value === targets[i])) return timeline.wait(timeline.duration)\n return timeline.animate((t) => ({ pops: from.map((value, i) => value + (targets[i] - value) * t) }))\n}\n\n/** The ring and the legend, found through the chart's children: the last row of them is the one it built. */\nfunction partsOf(children: readonly ChildView[]) {\n for (let i = children.length - 1; i >= 0; i--) {\n const row = children[i]\n const ring = Rect.is(row) ? row.children.find(PieRing.is) : undefined\n if (ring) return { ring, legend: row.children.find(PieLegend.is) ?? null }\n }\n return null\n}\n\n/**\n * A pie or donut chart, composed from two parts laid out by a horizontal-flow\n * {@link Rect} — the ring filling whatever the hugging legend leaves, both\n * centred:\n *\n * - {@link PieRing} — the wedges, each sized by its share of the total, with its\n * label inside its own band or pinned outside on a leader line, and the\n * summary in the hole.\n * - {@link PieLegend} — a column of swatch-and-caption rows naming each slice.\n *\n * **One mark per row, not per column**: a pie's palette is indexed by row and\n * there is nothing to hide, since dropping a slice would re-cut every other\n * wedge. `selectSlice` and its siblings are what a pie has instead — a slice\n * swells out of the ring and everything else dims behind it.\n *\n * **Resting state is the finished chart**; `enter` arms every part on its first\n * frame. The one exception is the hole's summary, which a scene can hold back\n * with `showCenter: false`, because it is usually the chart's *conclusion*.\n */\nexport const PieChart = component2d({\n key: \"pieChart\",\n state: {\n data: fresh<DataRecord[]>(() => []),\n categoryField: \"\",\n valueField: \"\",\n theme: Types.custom<PieChartTheme, PieChartConfig>(DEFAULT_PIE_CHART_THEME, { resolve: resolvePieChartTheme, lerp: lerpPieChartTheme }),\n labelMode: Types.custom<PieLabelMode>(\"percent\"),\n labelPlacement: Types.custom<PieLabelPlacement>(\"leader\"),\n gap: 40,\n centerLabel: \"\",\n centerCaption: \"\",\n /**\n * How far each slice is pushed out of the ring, `0`–`1` per index. Held here\n * rather than on the ring because two parts read it: the wedges grow out by\n * it, and the legend entries dim by it. Seeded flush, one to a row.\n */\n pops: fresh<number[]>(() => []),\n /** Read as the chart is built: the legend is a part the chart builds or doesn't. */\n showLegend: Types.boolean(true, { animatable: false }),\n /** Read as the chart is built: whether the hole's summary is drawn at rest. */\n showCenter: Types.boolean(true, { animatable: false }),\n },\n defaults: {\n // Its parts fill it; a node made of them would hug them.\n width: \"fill\",\n height: \"fill\",\n pops: (state) => slicesOf(state).map(() => 0),\n },\n allowedChildren: [],\n // The slice *structure* (one label and one legend row per row) is read once;\n // the slices themselves, their paint and the selection are bound, so a theme\n // tween re-paints and a `set` of `data` re-cuts the wedges.\n children: (state) => {\n const theme = () => state.theme\n const slices = () => slicesOf(state)\n const parts: Node2D[] = [\n new PieRing({\n width: \"fill\",\n height: \"fill\",\n slices,\n ring: () => theme().ring,\n label: () => theme().label,\n centerStyle: () => theme().center,\n labelMode: state.labelMode,\n labelPlacement: state.labelPlacement,\n pops: () => state.pops,\n dimOpacity: () => theme().spotlight.dimOpacity,\n centerLabel: state.centerLabel,\n centerCaption: state.centerCaption,\n centerReveal: state.showCenter === false ? 0 : 1,\n }) as unknown as Node2D,\n ]\n if (state.showLegend !== false) {\n parts.push(\n new PieLegend({\n slices,\n legend: () => theme().legend,\n dims: () => sliceDims(state),\n // The column's spacing is the theme's as the chart was built, like the\n // entries' type and chips.\n gap: state.theme.legend.itemGap / 3,\n padding: state.theme.legend.padding,\n }) as unknown as Node2D\n )\n }\n const row = new Rect({ flow: \"horizontal\", width: \"fill\", height: \"fill\", gap: state.gap, align: \"center\", children: parts })\n return asParts([row as unknown as Node2D])\n },\n commands: {\n /**\n * Animate the whole chart in: the wedges sweep in clockwise from the theme's\n * `startAngle`, then the labels fade up and the legend staggers in together.\n * Its first frame arms the wedges, the labels and the legend; the hole's\n * summary keeps its own signal. `duration` is the **whole** entrance.\n */\n enter: {\n args: {},\n // Held from the chart's arrival, so it never shows finished ahead of its draw.\n entrance: true,\n easing: easeOut(),\n run: (timeline, _state, children) => {\n const parts = partsOf(children)\n if (!parts) return timeline.wait(timeline.duration)\n const { ring, legend } = parts\n const sweep = timeline.duration * SWEEP_SHARE\n const labels = timeline.duration * LABEL_SHARE\n const at0 = (piece: Beat[\"piece\"]): Beat => ({ at: 0, duration: 0, piece })\n return beats(timeline, [\n at0(ring.to({ growth: 0, labelReveal: 0 })),\n ...(legend ? [at0(legend.run(\"arm\"))] : []),\n { at: 0, duration: sweep, piece: ring.run(\"sweep\") },\n { at: sweep, duration: labels, piece: ring.run(\"revealLabels\") },\n ...(legend ? [{ at: sweep, duration: labels, piece: legend.run(\"enter\") }] : []),\n ])\n },\n },\n /**\n * Fade the hole's summary up. Held out of `enter` on purpose: what sits in a\n * donut's hole is usually the chart's conclusion, so the scene lands it on\n * its own beat. Pair it with `showCenter: false`.\n */\n revealCenter: {\n args: {},\n easing: easeOut(),\n run: (timeline, _state, children) => partsOf(children)?.ring.run(\"revealCenter\") ?? timeline.wait(timeline.duration),\n },\n /** Grow slice `index` out past the ring's rim and dim the rest, leaving any other selected slice where it is. */\n selectSlice: {\n args: { sliceIndex: SLICE_ARG },\n easing: easeOut(),\n run: (timeline, state) => {\n const index = timeline.args.sliceIndex\n return popping(timeline, state, (i) => (i === index ? 1 : popAt(state, i)))\n },\n },\n /** Settle slice `index` back flush with the ring. */\n deselectSlice: {\n args: { sliceIndex: SLICE_ARG },\n easing: easeOut(),\n run: (timeline, state) => {\n const index = timeline.args.sliceIndex\n return popping(timeline, state, (i) => (i === index ? 0 : popAt(state, i)))\n },\n },\n /** Move the focus to slice `index` alone, settling whatever else was out. */\n selectOnlySlice: {\n args: { sliceIndex: SLICE_ARG },\n easing: easeOut(),\n run: (timeline, state) => {\n const index = timeline.args.sliceIndex\n return popping(timeline, state, (i) => (i === index ? 1 : 0))\n },\n },\n /** Settle every slice flush and bring the whole chart back to full. */\n clearSelection: {\n args: {},\n easing: easeOut(),\n run: (timeline, state) => popping(timeline, state, () => 0),\n },\n },\n})\n\nexport type PieChart = NodeOf<typeof PieChart>\n"]}