@motionscript/charts 0.1.0 → 0.1.1-alpha.1

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 (215) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +98 -28
  3. package/dist/bar-chart/bar-chart.d.ts +242 -119
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +231 -260
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +16 -38
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/category-axis.js +59 -68
  10. package/dist/bar-chart/category-axis.js.map +1 -1
  11. package/dist/bar-chart/chart-bars.d.ts +60 -91
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +88 -125
  14. package/dist/bar-chart/chart-bars.js.map +1 -1
  15. package/dist/bar-chart/index.d.ts +5 -13
  16. package/dist/bar-chart/index.d.ts.map +1 -1
  17. package/dist/bar-chart/index.js +2 -9
  18. package/dist/bar-chart/index.js.map +1 -1
  19. package/dist/bar-chart/shared.d.ts +40 -132
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +70 -158
  22. package/dist/bar-chart/shared.js.map +1 -1
  23. package/dist/browser/chunks/chunk-R3Q5EE6E.js +2 -0
  24. package/dist/browser/chunks/chunk-R3Q5EE6E.js.map +7 -0
  25. package/dist/browser/index.js +1 -1
  26. package/dist/browser/index.js.map +4 -4
  27. package/dist/browser/kit.js +1 -1
  28. package/dist/browser/manifest.json +1 -1
  29. package/dist/engine.d.ts +8 -17
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +8 -17
  32. package/dist/engine.js.map +1 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/kit/cartesian.d.ts +49 -35
  38. package/dist/kit/cartesian.d.ts.map +1 -1
  39. package/dist/kit/cartesian.js +80 -42
  40. package/dist/kit/cartesian.js.map +1 -1
  41. package/dist/kit/chart-canvas.d.ts +37 -40
  42. package/dist/kit/chart-canvas.d.ts.map +1 -1
  43. package/dist/kit/chart-canvas.js +53 -47
  44. package/dist/kit/chart-canvas.js.map +1 -1
  45. package/dist/kit/chart-legend.d.ts +16 -59
  46. package/dist/kit/chart-legend.d.ts.map +1 -1
  47. package/dist/kit/chart-legend.js +38 -76
  48. package/dist/kit/chart-legend.js.map +1 -1
  49. package/dist/kit/entries.d.ts +2 -0
  50. package/dist/kit/entries.d.ts.map +1 -1
  51. package/dist/kit/entries.js +2 -0
  52. package/dist/kit/entries.js.map +1 -1
  53. package/dist/kit/horizontal-axis.d.ts +77 -0
  54. package/dist/kit/horizontal-axis.d.ts.map +1 -0
  55. package/dist/kit/horizontal-axis.js +99 -0
  56. package/dist/kit/horizontal-axis.js.map +1 -0
  57. package/dist/kit/motion.d.ts +16 -3
  58. package/dist/kit/motion.d.ts.map +1 -1
  59. package/dist/kit/motion.js +15 -1
  60. package/dist/kit/motion.js.map +1 -1
  61. package/dist/{pie-chart → kit}/pie-legend.d.ts +14 -29
  62. package/dist/kit/pie-legend.d.ts.map +1 -0
  63. package/dist/kit/pie-legend.js +93 -0
  64. package/dist/kit/pie-legend.js.map +1 -0
  65. package/dist/kit/polar.d.ts +31 -0
  66. package/dist/kit/polar.d.ts.map +1 -0
  67. package/dist/kit/polar.js +33 -0
  68. package/dist/kit/polar.js.map +1 -0
  69. package/dist/kit/shared.d.ts +60 -214
  70. package/dist/kit/shared.d.ts.map +1 -1
  71. package/dist/kit/shared.js +91 -291
  72. package/dist/kit/shared.js.map +1 -1
  73. package/dist/kit/style.d.ts +254 -0
  74. package/dist/kit/style.d.ts.map +1 -0
  75. package/dist/kit/style.js +197 -0
  76. package/dist/kit/style.js.map +1 -0
  77. package/dist/kit/vertical-axis.d.ts +36 -45
  78. package/dist/kit/vertical-axis.d.ts.map +1 -1
  79. package/dist/kit/vertical-axis.js +69 -81
  80. package/dist/kit/vertical-axis.js.map +1 -1
  81. package/dist/line-chart/chart-line.d.ts +107 -40
  82. package/dist/line-chart/chart-line.d.ts.map +1 -1
  83. package/dist/line-chart/chart-line.js +172 -89
  84. package/dist/line-chart/chart-line.js.map +1 -1
  85. package/dist/line-chart/chart-region.d.ts +14 -27
  86. package/dist/line-chart/chart-region.d.ts.map +1 -1
  87. package/dist/line-chart/chart-region.js +13 -28
  88. package/dist/line-chart/chart-region.js.map +1 -1
  89. package/dist/line-chart/index.d.ts +5 -18
  90. package/dist/line-chart/index.d.ts.map +1 -1
  91. package/dist/line-chart/index.js +3 -16
  92. package/dist/line-chart/index.js.map +1 -1
  93. package/dist/line-chart/line-chart.d.ts +272 -119
  94. package/dist/line-chart/line-chart.d.ts.map +1 -1
  95. package/dist/line-chart/line-chart.js +309 -257
  96. package/dist/line-chart/line-chart.js.map +1 -1
  97. package/dist/line-chart/shared.d.ts +73 -130
  98. package/dist/line-chart/shared.d.ts.map +1 -1
  99. package/dist/line-chart/shared.js +195 -156
  100. package/dist/line-chart/shared.js.map +1 -1
  101. package/dist/line-chart/x-scale.d.ts +16 -55
  102. package/dist/line-chart/x-scale.d.ts.map +1 -1
  103. package/dist/line-chart/x-scale.js +44 -111
  104. package/dist/line-chart/x-scale.js.map +1 -1
  105. package/dist/nodes.d.ts +3 -13
  106. package/dist/nodes.d.ts.map +1 -1
  107. package/dist/nodes.js +9 -13
  108. package/dist/nodes.js.map +1 -1
  109. package/dist/pie-chart/index.d.ts +3 -15
  110. package/dist/pie-chart/index.d.ts.map +1 -1
  111. package/dist/pie-chart/index.js +1 -11
  112. package/dist/pie-chart/index.js.map +1 -1
  113. package/dist/pie-chart/pie-chart.d.ts +147 -85
  114. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  115. package/dist/pie-chart/pie-chart.js +65 -86
  116. package/dist/pie-chart/pie-chart.js.map +1 -1
  117. package/dist/pie-chart/pie-ring.d.ts +65 -196
  118. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  119. package/dist/pie-chart/pie-ring.js +154 -292
  120. package/dist/pie-chart/pie-ring.js.map +1 -1
  121. package/dist/pie-chart/shared.d.ts +78 -172
  122. package/dist/pie-chart/shared.d.ts.map +1 -1
  123. package/dist/pie-chart/shared.js +54 -177
  124. package/dist/pie-chart/shared.js.map +1 -1
  125. package/dist/radar-chart/index.d.ts +5 -0
  126. package/dist/radar-chart/index.d.ts.map +1 -0
  127. package/dist/radar-chart/index.js +5 -0
  128. package/dist/radar-chart/index.js.map +1 -0
  129. package/dist/radar-chart/radar-chart.d.ts +218 -0
  130. package/dist/radar-chart/radar-chart.d.ts.map +1 -0
  131. package/dist/radar-chart/radar-chart.js +224 -0
  132. package/dist/radar-chart/radar-chart.js.map +1 -0
  133. package/dist/radar-chart/radar-grid.d.ts +43 -0
  134. package/dist/radar-chart/radar-grid.d.ts.map +1 -0
  135. package/dist/radar-chart/radar-grid.js +61 -0
  136. package/dist/radar-chart/radar-grid.js.map +1 -0
  137. package/dist/radar-chart/radar-series.d.ts +59 -0
  138. package/dist/radar-chart/radar-series.d.ts.map +1 -0
  139. package/dist/radar-chart/radar-series.js +71 -0
  140. package/dist/radar-chart/radar-series.js.map +1 -0
  141. package/dist/radar-chart/shared.d.ts +73 -0
  142. package/dist/radar-chart/shared.d.ts.map +1 -0
  143. package/dist/radar-chart/shared.js +123 -0
  144. package/dist/radar-chart/shared.js.map +1 -0
  145. package/dist/radial-chart/index.d.ts +6 -0
  146. package/dist/radial-chart/index.d.ts.map +1 -0
  147. package/dist/radial-chart/index.js +4 -0
  148. package/dist/radial-chart/index.js.map +1 -0
  149. package/dist/radial-chart/radial-chart.d.ts +216 -0
  150. package/dist/radial-chart/radial-chart.d.ts.map +1 -0
  151. package/dist/radial-chart/radial-chart.js +174 -0
  152. package/dist/radial-chart/radial-chart.js.map +1 -0
  153. package/dist/radial-chart/radial-rings.d.ts +121 -0
  154. package/dist/radial-chart/radial-rings.d.ts.map +1 -0
  155. package/dist/radial-chart/radial-rings.js +273 -0
  156. package/dist/radial-chart/radial-rings.js.map +1 -0
  157. package/dist/radial-chart/shared.d.ts +73 -0
  158. package/dist/radial-chart/shared.d.ts.map +1 -0
  159. package/dist/radial-chart/shared.js +79 -0
  160. package/dist/radial-chart/shared.js.map +1 -0
  161. package/package.json +4 -4
  162. package/registry.json +21 -6
  163. package/src/bar-chart/bar-chart.ts +274 -315
  164. package/src/bar-chart/category-axis.ts +53 -88
  165. package/src/bar-chart/chart-bars.ts +108 -205
  166. package/src/bar-chart/index.ts +8 -19
  167. package/src/bar-chart/shared.ts +81 -309
  168. package/src/engine.ts +8 -17
  169. package/src/index.ts +2 -0
  170. package/src/kit/cartesian.ts +118 -55
  171. package/src/kit/chart-canvas.ts +50 -80
  172. package/src/kit/chart-legend.ts +36 -97
  173. package/src/kit/entries.ts +4 -1
  174. package/src/kit/horizontal-axis.ts +114 -0
  175. package/src/kit/motion.ts +18 -2
  176. package/src/{pie-chart → kit}/pie-legend.ts +25 -65
  177. package/src/kit/polar.ts +54 -0
  178. package/src/kit/shared.ts +104 -487
  179. package/src/kit/style.ts +257 -0
  180. package/src/kit/vertical-axis.ts +81 -115
  181. package/src/line-chart/chart-line.ts +185 -91
  182. package/src/line-chart/chart-region.ts +14 -31
  183. package/src/line-chart/index.ts +14 -22
  184. package/src/line-chart/line-chart.ts +377 -315
  185. package/src/line-chart/shared.ts +229 -304
  186. package/src/line-chart/x-scale.ts +44 -121
  187. package/src/nodes.ts +9 -13
  188. package/src/pie-chart/index.ts +3 -35
  189. package/src/pie-chart/pie-chart.ts +83 -134
  190. package/src/pie-chart/pie-ring.ts +162 -368
  191. package/src/pie-chart/shared.ts +92 -379
  192. package/src/radar-chart/index.ts +4 -0
  193. package/src/radar-chart/radar-chart.ts +287 -0
  194. package/src/radar-chart/radar-grid.ts +78 -0
  195. package/src/radar-chart/radar-series.ts +88 -0
  196. package/src/radar-chart/shared.ts +146 -0
  197. package/src/radial-chart/index.ts +5 -0
  198. package/src/radial-chart/radial-chart.ts +210 -0
  199. package/src/radial-chart/radial-rings.ts +328 -0
  200. package/src/radial-chart/shared.ts +114 -0
  201. package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
  202. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
  203. package/dist/kit/chart-border.d.ts +0 -27
  204. package/dist/kit/chart-border.d.ts.map +0 -1
  205. package/dist/kit/chart-border.js +0 -28
  206. package/dist/kit/chart-border.js.map +0 -1
  207. package/dist/line-chart/horizontal-axis.d.ts +0 -79
  208. package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
  209. package/dist/line-chart/horizontal-axis.js +0 -129
  210. package/dist/line-chart/horizontal-axis.js.map +0 -1
  211. package/dist/pie-chart/pie-legend.d.ts.map +0 -1
  212. package/dist/pie-chart/pie-legend.js +0 -117
  213. package/dist/pie-chart/pie-legend.js.map +0 -1
  214. package/src/kit/chart-border.ts +0 -50
  215. package/src/line-chart/horizontal-axis.ts +0 -151
@@ -0,0 +1,210 @@
1
+ import {
2
+ Rect,
3
+ Types,
4
+ clamp,
5
+ component2d,
6
+ easeOut,
7
+ type ChildView,
8
+ type DataRecord,
9
+ type NodeOf,
10
+ // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
11
+ type Node2D,
12
+ type Node2DProps as _Node2DProps,
13
+ type Fill as _Fill,
14
+ type FillResolved as _FillResolved,
15
+ } from "@motionscript/sdk"
16
+
17
+ import { SERIES_ARG, asParts, fresh } from "../kit/entries"
18
+ import { beats, type AnyTimeline, type Beat } from "../kit/motion"
19
+ import { CHART_STYLE_FAMILY, CHART_STYLE_PRESETS } from "../kit/style"
20
+ import { PieLegend } from "../kit/pie-legend"
21
+ import { PIE_SPOTLIGHT_DIM, selectionFade } from "../kit/polar"
22
+ import { RadialRings } from "./radial-rings"
23
+ import {
24
+ RADIAL_STYLE_GROUPS,
25
+ centerOf,
26
+ legendOf,
27
+ marksOf,
28
+ resolveRadialRows,
29
+ ringLabelOf,
30
+ type RadialRow,
31
+ type RadialStyle,
32
+ } from "./shared"
33
+
34
+ export interface RadialChartValues {
35
+ readonly data: DataRecord[]
36
+ readonly categoryField: string
37
+ readonly valueField: string
38
+ readonly max: number
39
+ readonly style: RadialStyle
40
+ readonly pops: number[]
41
+ }
42
+
43
+ /** How `enter` divides its duration: rings sweeping in, then captions and legend together. Sums to 1. */
44
+ const SWEEP_SHARE = 0.7
45
+ const LABEL_SHARE = 0.3
46
+
47
+ function rowsOf(s: RadialChartValues): RadialRow[] {
48
+ return resolveRadialRows(s.data, s.categoryField, s.valueField, s.style)
49
+ }
50
+
51
+ function popAt(s: RadialChartValues, index: number): number {
52
+ return clamp(s.pops[index] ?? 0, 0, 1)
53
+ }
54
+
55
+ /** Per-row opacity for the legend: 1 while nothing is selected and for the selected ring, falling toward the dim for the rest. */
56
+ function ringDims(s: RadialChartValues): number[] {
57
+ let focus = 0
58
+ for (let i = 0; i < s.data.length; i++) focus = Math.max(focus, popAt(s, i))
59
+ return s.data.map((_, i) => selectionFade(focus, RING_DIM, popAt(s, i)))
60
+ }
61
+
62
+ const RING_DIM = Math.max(PIE_SPOTLIGHT_DIM, 0.25)
63
+
64
+ function popping(timeline: AnyTimeline, s: RadialChartValues, pop: (index: number) => number) {
65
+ const from = s.data.map((_, i) => popAt(s, i))
66
+ const targets = s.data.map((_, i) => clamp(pop(i), 0, 1))
67
+ if (from.every((value, i) => value === targets[i])) return timeline.wait(timeline.duration)
68
+ return timeline.animate((t) => ({ pops: from.map((value, i) => value + (targets[i] - value) * t) }))
69
+ }
70
+
71
+ /** The rings and legend among the children; the last row is the one the chart built. */
72
+ function partsOf(children: readonly ChildView[]) {
73
+ for (let i = children.length - 1; i >= 0; i--) {
74
+ const row = children[i]
75
+ const rings = Rect.is(row) ? row.children.find(RadialRings.is) : undefined
76
+ if (rings) return { rings, legend: row.children.find(PieLegend.is) ?? null }
77
+ }
78
+ return null
79
+ }
80
+
81
+ /**
82
+ * A radial bar chart: one concentric ring per row, the first row outermost,
83
+ * each an arc from `startAngle` sweeping `value / max` of `sweep` degrees.
84
+ * Rows are the marks and none can be hidden; `selectRing` thickens a ring and
85
+ * dims the rest.
86
+ */
87
+ export const RadialChart = component2d({
88
+ key: "radialChart",
89
+ state: {
90
+ data: fresh<DataRecord[]>(() => [], { kind: "dataset" }),
91
+ /** Ring caption, used on the ring labels and the legend. */
92
+ categoryField: "",
93
+ /** Ring magnitude: the arc's share of the full sweep. */
94
+ valueField: "",
95
+ /** The value a full sweep stands for. 0 lets the largest value fill its ring. */
96
+ max: Types.number(0, { min: 0, step: 1 }),
97
+ style: Types.style(CHART_STYLE_FAMILY, RADIAL_STYLE_GROUPS, { presets: CHART_STYLE_PRESETS }),
98
+ /** Gap between the rings and the legend, in px. */
99
+ gap: 40,
100
+ /** Text set in the centre hole. */
101
+ centerLabel: "",
102
+ /** Smaller second line under `centerLabel`. */
103
+ centerCaption: "",
104
+ /** How far each ring is emphasised, `0`-`1` per index. Here because the rings and the legend both read it. */
105
+ pops: fresh<number[]>(() => []),
106
+ /** Read as the chart is built: whether the centre summary is drawn at rest. A scene that wants it to land later sets `false` and runs `revealCenter`. */
107
+ showCenter: Types.boolean(true, { animatable: false }),
108
+ },
109
+ defaults: {
110
+ width: "fill",
111
+ height: "fill",
112
+ pops: (state) => state.data.map(() => 0),
113
+ },
114
+ allowedChildren: [],
115
+ children: (state) => {
116
+ const style = () => state.style
117
+ const rows = () => rowsOf(state)
118
+ const parts: Node2D[] = [
119
+ new RadialRings({
120
+ width: "fill",
121
+ height: "fill",
122
+ rows,
123
+ max: () => state.max,
124
+ marks: () => marksOf(style()),
125
+ label: () => ringLabelOf(style()),
126
+ centerStyle: () => centerOf(style()),
127
+ trackFill: () => style().track,
128
+ ringLabels: () => style().ringLabels,
129
+ pops: () => state.pops,
130
+ dimOpacity: RING_DIM,
131
+ centerLabel: state.centerLabel,
132
+ centerCaption: state.centerCaption,
133
+ centerReveal: state.showCenter === false ? 0 : 1,
134
+ }) as unknown as Node2D,
135
+ ]
136
+ const position = state.style.legend
137
+ if (position !== "none") {
138
+ const side = position === "right"
139
+ const config = legendOf(state.style)
140
+ const legend = new PieLegend({
141
+ slices: rows,
142
+ legend: () => legendOf(style()),
143
+ dims: () => ringDims(state),
144
+ flow: side ? "vertical" : "horizontal",
145
+ align: "center",
146
+ gap: side ? config.itemGap / 3 : config.itemGap,
147
+ padding: 0,
148
+ }) as unknown as Node2D
149
+ if (position === "top") parts.unshift(legend)
150
+ else parts.push(legend)
151
+ }
152
+ const row = new Rect({
153
+ flow: position === "right" || position === "none" ? "horizontal" : "vertical",
154
+ width: "fill",
155
+ height: "fill",
156
+ gap: state.gap,
157
+ padding: 24,
158
+ align: "center",
159
+ fill: () => state.style.background,
160
+ children: parts,
161
+ })
162
+ return asParts([row as unknown as Node2D])
163
+ },
164
+ commands: {
165
+ /** Rings sweep in from `startAngle`, outer ring first, then captions fade up and the legend staggers in. `duration` is the whole entrance; the centre summary keeps its own signal. */
166
+ enter: {
167
+ args: {},
168
+ entrance: true,
169
+ easing: easeOut(),
170
+ run: (timeline, _state, children) => {
171
+ const parts = partsOf(children)
172
+ if (!parts) return timeline.wait(timeline.duration)
173
+ const { rings, legend } = parts
174
+ const sweep = timeline.duration * SWEEP_SHARE
175
+ const labels = timeline.duration * LABEL_SHARE
176
+ const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
177
+ return beats(timeline, [
178
+ at0(rings.to({ growth: 0, labelReveal: 0 })),
179
+ ...(legend ? [at0(legend.run("arm"))] : []),
180
+ { at: 0, duration: sweep, piece: rings.run("sweep") },
181
+ { at: sweep, duration: labels, piece: rings.run("revealLabels") },
182
+ ...(legend ? [{ at: sweep, duration: labels, piece: legend.run("enter") }] : []),
183
+ ])
184
+ },
185
+ },
186
+ /** Fade the centre summary up. Not part of `enter`: it is usually the conclusion, which a scene lands on its own beat. */
187
+ revealCenter: {
188
+ args: {},
189
+ easing: easeOut(),
190
+ run: (timeline, _state, children) => partsOf(children)?.rings.run("revealCenter") ?? timeline.wait(timeline.duration),
191
+ },
192
+ /** Thicken ring `ringIndex` and dim the rest, leaving any other selected ring where it is. */
193
+ selectRing: {
194
+ args: { ringIndex: SERIES_ARG },
195
+ easing: easeOut(),
196
+ run: (timeline, state) => {
197
+ const index = timeline.args.ringIndex
198
+ return popping(timeline, state, (i) => (i === index ? 1 : popAt(state, i)))
199
+ },
200
+ },
201
+ /** Settle every ring and bring the whole chart back to full. */
202
+ clearSelection: {
203
+ args: {},
204
+ easing: easeOut(),
205
+ run: (timeline, state) => popping(timeline, state, () => 0),
206
+ },
207
+ },
208
+ })
209
+
210
+ export type RadialChart = NodeOf<typeof RadialChart>
@@ -0,0 +1,328 @@
1
+ import {
2
+ Graphics2D,
3
+ PathBuilder,
4
+ Rect,
5
+ Text,
6
+ Types,
7
+ clamp,
8
+ component2d,
9
+ fillOps,
10
+ textOps,
11
+ type NodeOf,
12
+ // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
13
+ type Node2D,
14
+ type Node2DProps as _Node2DProps,
15
+ type Fill as _Fill,
16
+ type FillResolved as _FillResolved,
17
+ type TextStyle,
18
+ type Vector2,
19
+ } from "@motionscript/sdk"
20
+
21
+ import { asParts, fresh } from "../kit/entries"
22
+ import { frameEase } from "../kit/motion"
23
+ import { loadPaints } from "../kit/paints"
24
+ import { DEG, ringFills, ringPoint, selectionFade, type CenterConfig } from "../kit/polar"
25
+ import { radialFractions, type RadialMarks, type RadialRow } from "./shared"
26
+
27
+ /** Share of the entrance one ring's sweep starts after the last. */
28
+ const STAGGER = 0.12
29
+ /** A selected ring's thickness grows by this share of itself, on each side. */
30
+ const GROW = 0.12
31
+ const ARC_STEP = 4 * DEG
32
+ const LABEL_PAD = 10
33
+ const CENTER_FIT = 0.78
34
+ const GLYPH_RATIO = 0.62
35
+ /** How far the radius is held in from the box, for the sake of a ring's growth. */
36
+ const EDGE = 4
37
+
38
+ const DEFAULT_MARKS: RadialMarks = { innerRadius: 0.3, ringGap: 8, sweep: 360, startAngle: 0, track: true, roundCaps: true }
39
+ const DEFAULT_LABEL: TextStyle = { fontSize: 28, fontWeight: 500, textAlign: "center", fill: "#a1a1aa" }
40
+ const DEFAULT_CENTER: CenterConfig = {
41
+ labelStyle: { fontSize: 50, fontWeight: 800, textAlign: "center", fill: "#f4f4f5" },
42
+ captionStyle: { fontSize: 22, fontWeight: 500, textAlign: "center", fill: "#a1a1aa" },
43
+ }
44
+
45
+ interface RingsState {
46
+ readonly rows: RadialRow[]
47
+ readonly max: number
48
+ readonly marks: RadialMarks
49
+ readonly label: TextStyle
50
+ readonly centerStyle: CenterConfig
51
+ readonly ringLabels: boolean
52
+ readonly pops: readonly number[]
53
+ readonly dimOpacity: number
54
+ readonly centerLabel: string
55
+ readonly centerCaption: string
56
+ }
57
+
58
+ function sizeOf(style: TextStyle, fallback: number): number {
59
+ return typeof style.fontSize === "number" ? style.fontSize : fallback
60
+ }
61
+
62
+ /** Every radius and angle for one box, derived wherever read so the drawing and the labels cannot disagree. */
63
+ export class RadialGeometry {
64
+ constructor(
65
+ private readonly s: RingsState,
66
+ private readonly box: { readonly width: number; readonly height: number }
67
+ ) {}
68
+
69
+ get count(): number {
70
+ return this.s.rows.length
71
+ }
72
+
73
+ get outerRadius(): number {
74
+ return Math.max(0, Math.min(this.box.width, this.box.height) / 2 - EDGE)
75
+ }
76
+
77
+ get holeRadius(): number {
78
+ return clamp(this.s.marks.innerRadius, 0, 0.9) * this.outerRadius
79
+ }
80
+
81
+ /** Radial depth of one ring. */
82
+ get thickness(): number {
83
+ const n = this.count
84
+ if (n === 0) return 0
85
+ return Math.max(0, (this.outerRadius - this.holeRadius - (n - 1) * Math.max(0, this.s.marks.ringGap)) / n)
86
+ }
87
+
88
+ /** Rim of ring `index`; ring 0 is the outermost. */
89
+ ringOuter(index: number): number {
90
+ return this.outerRadius - index * (this.thickness + Math.max(0, this.s.marks.ringGap))
91
+ }
92
+
93
+ ringMid(index: number): number {
94
+ return this.ringOuter(index) - this.thickness / 2
95
+ }
96
+
97
+ get fractions(): number[] {
98
+ return radialFractions(this.s.rows, this.s.max)
99
+ }
100
+
101
+ get startRad(): number {
102
+ return this.s.marks.startAngle * DEG
103
+ }
104
+
105
+ get sweepRad(): number {
106
+ return clamp(this.s.marks.sweep, 0, 360) * DEG
107
+ }
108
+
109
+ popOf(index: number): number {
110
+ return clamp(this.s.pops[index] ?? 0, 0, 1)
111
+ }
112
+
113
+ get selectionStrength(): number {
114
+ let strongest = 0
115
+ for (let i = 0; i < this.count; i++) strongest = Math.max(strongest, this.popOf(i))
116
+ return strongest
117
+ }
118
+
119
+ ringOpacity(index: number): number {
120
+ return selectionFade(this.selectionStrength, this.s.dimOpacity, this.popOf(index))
121
+ }
122
+
123
+ /** How far ring `index`'s sweep has come at entrance progress `growth`: rings start in turn, outer first. */
124
+ progress(index: number, growth: number): number {
125
+ const total = 1 + STAGGER * Math.max(0, this.count - 1)
126
+ return clamp(growth * total - index * STAGGER, 0, 1)
127
+ }
128
+
129
+ get labelFont(): number {
130
+ return Math.max(1, Math.min(sizeOf(this.s.label, 28), this.thickness * 0.62))
131
+ }
132
+
133
+ /** Where ring `index`'s caption is pinned: just behind its start, reading away from the sweep. */
134
+ labelAnchor(index: number): { at: Vector2; side: 1 | -1 } {
135
+ const a = this.startRad
136
+ const back = { x: -Math.cos(a), y: Math.sin(a) }
137
+ const p = ringPoint(this.ringMid(index), a)
138
+ const side: 1 | -1 = back.x <= 0 ? -1 : 1
139
+ return { at: { x: p.x + back.x * LABEL_PAD, y: p.y + back.y * LABEL_PAD }, side }
140
+ }
141
+
142
+ get centerFont(): number {
143
+ const { centerStyle, centerLabel, centerCaption } = this.s
144
+ const wanted = sizeOf(centerStyle.labelStyle, 50)
145
+ const hole = this.holeRadius
146
+ if (hole <= 0) return wanted
147
+ const ratio = sizeOf(centerStyle.captionStyle, 22) / Math.max(1, wanted)
148
+ const glyphs = Math.max(1, centerLabel.length, centerCaption.length * ratio)
149
+ return Math.min(wanted, (hole * 2 * CENTER_FIT) / (glyphs * GLYPH_RATIO))
150
+ }
151
+ }
152
+
153
+ /**
154
+ * An arc band between `rIn` and `rOut` over `a0`..`a1` (radians clockwise from
155
+ * 12 o'clock), the ends squared or capped with half circles. A band shorter
156
+ * than its caps is two overlapping caps, which fill as a pill.
157
+ */
158
+ export function bandPath(a0: number, a1: number, rIn: number, rOut: number, round: boolean): PathBuilder {
159
+ const path = new PathBuilder()
160
+ const mid = (rIn + rOut) / 2
161
+ const cap = (rOut - rIn) / 2
162
+ const delta = round && mid > 0 ? Math.min(cap / mid, (a1 - a0) / 2) : 0
163
+ const from = a0 + delta
164
+ const to = a1 - delta
165
+ const steps = Math.max(2, Math.ceil(Math.abs(to - from) / ARC_STEP))
166
+ const arc = (r: number, x: number, y: number, first: boolean) => {
167
+ for (let i = 0; i <= steps; i++) {
168
+ const p = ringPoint(r, x + ((y - x) * i) / steps)
169
+ if (first && i === 0) path.moveTo(p.x, p.y)
170
+ else path.lineTo(p.x, p.y)
171
+ }
172
+ }
173
+ // The end at `angle` rounded through a tip `direction` along the sweep.
174
+ const semicircle = (angle: number, direction: 1 | -1, fromOuter: boolean) => {
175
+ const centre = ringPoint(mid, angle)
176
+ const radial = { x: Math.sin(angle), y: Math.cos(angle) }
177
+ const tangent = { x: Math.cos(angle) * direction, y: -Math.sin(angle) * direction }
178
+ const turns = 12
179
+ for (let i = 1; i < turns; i++) {
180
+ const phi = (Math.PI * i) / turns
181
+ const r = fromOuter ? Math.cos(phi) : -Math.cos(phi)
182
+ path.lineTo(centre.x + cap * (radial.x * r + tangent.x * Math.sin(phi)), centre.y + cap * (radial.y * r + tangent.y * Math.sin(phi)))
183
+ }
184
+ }
185
+ arc(rOut, from, to, true)
186
+ if (round) semicircle(to, 1, true)
187
+ arc(rIn, to, from, false)
188
+ if (round) semicircle(from, -1, false)
189
+ path.close()
190
+ return path
191
+ }
192
+
193
+ const NO_BOX = { width: 0, height: 0 }
194
+
195
+ function labelBlocks(s: RingsState & { readonly labelReveal: number; readonly centerReveal: number }) {
196
+ const blocks: Node2D[] = []
197
+ const geometry = () => new RadialGeometry(s, (blocks[0]?.parent as Node2D | null | undefined)?.layoutBounds ?? NO_BOX)
198
+ const side = geometry().labelAnchor(0).side
199
+
200
+ s.rows.forEach((_, index) => {
201
+ const anchor = () => geometry().labelAnchor(index).at
202
+ blocks.push(
203
+ new Rect({
204
+ flow: "vertical",
205
+ width: "hug",
206
+ height: "hug",
207
+ align: side > 0 ? "centerLeft" : "centerRight",
208
+ opacity: () => (s.ringLabels ? clamp(s.labelReveal, 0, 1) * geometry().ringOpacity(index) : 0),
209
+ centerLeft: side > 0 ? anchor : undefined,
210
+ centerRight: side > 0 ? undefined : anchor,
211
+ children: [
212
+ new Text({
213
+ ...textOps.styleProps(s.label),
214
+ text: () => s.rows[index]?.category ?? "",
215
+ textAlign: side > 0 ? "left" : "right",
216
+ fontSize: () => geometry().labelFont,
217
+ }),
218
+ ],
219
+ })
220
+ )
221
+ })
222
+
223
+ if (s.centerLabel !== "" || s.centerCaption !== "") {
224
+ const children: Node2D[] = []
225
+ if (s.centerLabel !== "") {
226
+ children.push(new Text({ ...textOps.styleProps(s.centerStyle.labelStyle), text: s.centerLabel, fontSize: () => geometry().centerFont }))
227
+ }
228
+ if (s.centerCaption !== "") {
229
+ children.push(
230
+ new Text({
231
+ ...textOps.styleProps(s.centerStyle.captionStyle),
232
+ text: s.centerCaption,
233
+ fontSize: () => geometry().centerFont * (sizeOf(s.centerStyle.captionStyle, 22) / sizeOf(s.centerStyle.labelStyle, 50)),
234
+ })
235
+ )
236
+ }
237
+ blocks.push(
238
+ new Rect({
239
+ flow: "vertical",
240
+ width: "hug",
241
+ height: "hug",
242
+ gap: 2,
243
+ align: "center",
244
+ opacity: () => clamp(s.centerReveal, 0, 1),
245
+ center: { x: 0, y: 0 },
246
+ children,
247
+ })
248
+ )
249
+ }
250
+ return asParts(blocks)
251
+ }
252
+
253
+ /**
254
+ * The rings: row `i` is the `i`-th ring from the outside, an arc from
255
+ * `startAngle` whose sweep is `value / max` of the full sweep, over an
256
+ * optional full-sweep track. A selected ring thickens and the rest dims.
257
+ */
258
+ export const RadialRings = component2d({
259
+ key: "radialRings",
260
+ state: {
261
+ rows: fresh<RadialRow[]>(() => []),
262
+ max: 0,
263
+ marks: Types.custom<RadialMarks>(DEFAULT_MARKS),
264
+ label: Types.custom<TextStyle>(DEFAULT_LABEL),
265
+ centerStyle: Types.custom<CenterConfig>(DEFAULT_CENTER),
266
+ /** The empty-ring colour drawn under each arc. */
267
+ trackFill: Types.fill("white/15"),
268
+ ringLabels: Types.boolean(false),
269
+ /** Entrance progress `[0, 1]`: rings sweep in turn. */
270
+ growth: 1,
271
+ labelReveal: 1,
272
+ pops: fresh<number[]>(() => []),
273
+ dimOpacity: 0.25,
274
+ centerLabel: "",
275
+ centerCaption: "",
276
+ centerReveal: 1,
277
+ },
278
+ defaults: { width: "fill", height: "fill" },
279
+ children: (state) => labelBlocks(state),
280
+ load: (loader, state) => loadPaints(loader, { fills: [...ringFills(state.rows), fillOps.resolve(state.trackFill)] }),
281
+ commands: {
282
+ /** Sweep the rings in, outer first. */
283
+ sweep: (timeline) => {
284
+ const ease = frameEase(timeline)
285
+ return timeline.animate((t) => ({ growth: ease(t) }))
286
+ },
287
+ /** Fade the ring captions up. */
288
+ revealLabels: (timeline) => {
289
+ const ease = frameEase(timeline)
290
+ return timeline.animate((t) => ({ labelReveal: ease(t) }))
291
+ },
292
+ /** Fade the centre summary up. */
293
+ revealCenter: (timeline, state) => {
294
+ const ease = frameEase(timeline)
295
+ const from = clamp(state.centerReveal, 0, 1)
296
+ return timeline.animate((t) => ({ centerReveal: from + (1 - from) * ease(t) }))
297
+ },
298
+ },
299
+ render: (ctx, state) => {
300
+ const g = new RadialGeometry(state, ctx.size)
301
+ const thickness = g.thickness
302
+ if (g.outerRadius > 0 && g.count > 0 && thickness > 0) {
303
+ const half = g.outerRadius + EDGE + 1
304
+ const frame = { centerBounds: [-half, -half, half, half] as [number, number, number, number], width: half * 2, height: half * 2 }
305
+ const fills = ringFills(state.rows)
306
+ const fractions = g.fractions
307
+ const a0 = g.startRad
308
+ const round = state.marks.roundCaps
309
+ // Selected rings last, so a thickened ring is never under its neighbour.
310
+ const order = [...state.rows.keys()].sort((a, b) => g.popOf(a) - g.popOf(b))
311
+ for (const i of order) {
312
+ const grow = thickness * GROW * g.popOf(i)
313
+ const rOut = g.ringOuter(i) + grow
314
+ const rIn = g.ringOuter(i) - thickness - grow
315
+ const opacity = g.ringOpacity(i)
316
+ if (state.marks.track && g.sweepRad > 0) {
317
+ ctx.draw(new Graphics2D().path(bandPath(a0, a0 + g.sweepRad, rIn, rOut, round).toPathState(frame)).fill(state.trackFill).opacity(opacity))
318
+ }
319
+ const span = g.sweepRad * fractions[i] * g.progress(i, state.growth)
320
+ if (span <= 1e-4) continue
321
+ ctx.draw(new Graphics2D().path(bandPath(a0, a0 + span, rIn, rOut, round).toPathState(frame)).fill(fills[i] ?? []).opacity(opacity))
322
+ }
323
+ }
324
+ ctx.children()
325
+ },
326
+ })
327
+
328
+ export type RadialRings = NodeOf<typeof RadialRings>
@@ -0,0 +1,114 @@
1
+ import { Types, clamp, type DataRecord, type TextStyle, type Fill as _Fill, type FillResolved as _FillResolved } from "@motionscript/sdk"
2
+
3
+ import { CHART_COLORS, CHART_LEGEND, CHART_TEXT, legendConfigOf, type ChartStyleBase, paletteFill } from "../kit/style"
4
+ import type { LegendConfig } from "../kit/shared"
5
+ import { num, text } from "../kit/shared"
6
+ import type { CenterConfig, PieSlice } from "../kit/polar"
7
+
8
+ /** One ring: a row's caption, magnitude and paint. The same shape as a pie's slice, so the legend is shared. */
9
+ export type RadialRow = PieSlice
10
+
11
+ export function resolveRadialRows(data: readonly DataRecord[], category: string, value: string, style: Pick<ChartStyleBase, "palette">): RadialRow[] {
12
+ return data.map((row, i) => ({ category: text(row, category), value: num(row, value), fill: paletteFill(style, i) }))
13
+ }
14
+
15
+ /** A row's plotted value: negatives and non-finite values count as 0. */
16
+ export function rowValue(row: RadialRow): number {
17
+ return row.value > 0 && Number.isFinite(row.value) ? row.value : 0
18
+ }
19
+
20
+ /** What a full sweep stands for: `max` when it is positive, else the largest value (1 when every value is 0). */
21
+ export function radialMax(rows: readonly RadialRow[], max: number): number {
22
+ if (max > 0 && Number.isFinite(max)) return max
23
+ let largest = 0
24
+ for (const row of rows) largest = Math.max(largest, rowValue(row))
25
+ return largest > 0 ? largest : 1
26
+ }
27
+
28
+ /** The share of a full sweep each row fills, `[0, 1]`. */
29
+ export function radialFractions(rows: readonly RadialRow[], max: number): number[] {
30
+ const top = radialMax(rows, max)
31
+ return rows.map((row) => clamp(rowValue(row) / top, 0, 1))
32
+ }
33
+
34
+ /** The ring geometry and look, from `style`. */
35
+ export interface RadialMarks {
36
+ innerRadius: number
37
+ ringGap: number
38
+ /** Degrees of a full ring. */
39
+ sweep: number
40
+ /** Degrees clockwise from 12 o'clock. */
41
+ startAngle: number
42
+ track: boolean
43
+ roundCaps: boolean
44
+ }
45
+
46
+ export interface RadialStyle extends ChartStyleBase {
47
+ readonly innerRadius: number
48
+ readonly ringGap: number
49
+ readonly sweep: number
50
+ readonly startAngle: number
51
+ readonly track: _FillResolved[]
52
+ readonly roundCaps: boolean
53
+ readonly ringLabels: boolean
54
+ }
55
+
56
+ export const RADIAL_STYLE_GROUPS = {
57
+ colors: CHART_COLORS,
58
+ text: CHART_TEXT,
59
+ marks: {
60
+ /** Where the innermost ring starts, as a fraction of the outer radius. */
61
+ innerRadius: Types.number(0.3, { min: 0, max: 0.9, step: 0.05 }),
62
+ /** Space between neighbouring rings, px. */
63
+ ringGap: Types.number(8, { min: 0, max: 60, step: 1, unit: "px" }),
64
+ /** Degrees a full ring spans: 360 a circle, 270 a gauge-like open ring. */
65
+ sweep: Types.number(360, { min: 30, max: 360, step: 5, unit: "deg" }),
66
+ /** Degrees clockwise from 12 o'clock where every ring begins. */
67
+ startAngle: Types.number(0, { min: -360, max: 360, step: 5, unit: "deg" }),
68
+ /** The full ring under each arc; empty draws none. */
69
+ track: Types.fill("white/10"),
70
+ /** Round the ends of the arcs and tracks. */
71
+ roundCaps: Types.boolean(true),
72
+ },
73
+ labels: {
74
+ /** The category written at each ring's start, in the muted colour. */
75
+ ringLabels: Types.boolean(false),
76
+ },
77
+ legend: CHART_LEGEND,
78
+ }
79
+
80
+ export function marksOf(style: RadialStyle): RadialMarks {
81
+ return {
82
+ innerRadius: style.innerRadius,
83
+ ringGap: style.ringGap,
84
+ sweep: style.sweep,
85
+ startAngle: style.startAngle,
86
+ track: style.track.length > 0,
87
+ roundCaps: style.roundCaps,
88
+ }
89
+ }
90
+
91
+ export function legendOf(style: RadialStyle): LegendConfig {
92
+ return legendConfigOf(style)
93
+ }
94
+
95
+ function face(style: RadialStyle): Pick<TextStyle, "fontFamily"> {
96
+ return style.fontFamily ? { fontFamily: style.fontFamily } : {}
97
+ }
98
+
99
+ export function ringLabelOf(style: RadialStyle): TextStyle {
100
+ return { ...face(style), fontSize: style.fontSize, fontWeight: style.fontWeight, textAlign: "center", fill: style.muted }
101
+ }
102
+
103
+ export function centerOf(style: RadialStyle): CenterConfig {
104
+ return {
105
+ labelStyle: {
106
+ ...face(style),
107
+ fontSize: Math.round(style.fontSize * 1.8),
108
+ fontWeight: Math.min(900, style.fontWeight + 300),
109
+ textAlign: "center",
110
+ fill: style.foreground,
111
+ },
112
+ captionStyle: { ...face(style), fontSize: Math.round(style.fontSize * 0.8), fontWeight: style.fontWeight, textAlign: "center", fill: style.muted },
113
+ }
114
+ }
@@ -1,2 +0,0 @@
1
- import{fillOps as b,lerpNumber as y,shadowOps as M,strokeOps as g,insetsOps as w,textOps as S}from"@motionscript/sdk";import{niceStep as ft,enterBudget as Z}from"@motionscript/sdk/component";function ge(e,t){return{label:e?.label??t?.label??"",range:e?.range??t?.range,ticks:e?.ticks??t?.ticks,format:e?.format??t?.format}}function D(e,t,r){return{label:r<.5?e.label:t.label,range:xe(e.range,t.range,r),ticks:me(e.ticks,t.ticks,r),format:r<.5?e.format:t.format}}function xe(e,t,r){return!e||!t?r<.5?e:t:[y(e[0],t[0],r),y(e[1],t[1],r)]}function me(e,t,r){if(!e||!t)return r<.5?e:t;let n=Math.max(e.length,t.length),i=[];for(let o=0;o<n;o++)i.push(y(e[o]??t[o],t[o]??e[o],r));return i}function Be(e,t){if(t?.range===void 0&&t?.ticks===void 0)return;let r=ge(t,e);if(t.ticks===void 0&&t.range!==void 0){let n=Math.max(2,e.ticks?.length??2);r.ticks=Q(t.range,n-1)}return r}function B(e){return e.blend}function Ye(e,t,r){let n=t.range!==e.range,i=n&&t.range&&t.ticks&&t.ticks===e.ticks?{...t,ticks:Q(t.range,Math.max(2,t.ticks.length)-1)}:t,o=B(e)!==void 0,l=o||n||i.ticks!==e.ticks,s=o||e.range===void 0||e.ticks===void 0||i.range===void 0||i.ticks===void 0;return!l||!s?D(e,i,r):r===0?e:r===1?i:{...D(e,i,r),blend:{from:e,to:i,t:r}}}function X(e,t){let r=B(e);return r?D(X(r.from,t),X(r.to,t),r.t):t(e)}var E={plotFill:"#15130e",borderColor:"#4a4036",gridColor:"#2c2820",markerColor:"#8a7f70",regionColor:"#d99a3a"},Y=["#d9603b","#8194ad","#e0a93b","#5e9d6b","#9ec85a"],$=.2;function $e(e){return e<0?0:e>1?1:e}function He(e,t){if(t===void 0)return 0;let r=e[t];return typeof r=="number"&&Number.isFinite(r)?r:0}function Ve(e,t){if(t===void 0||t==="")return"";let r=e[t];return r==null?"":String(r)}function We(e,t=[]){if(e.length===0)return[];let r=new Set(t),n=new Set,i=[];for(let o of e)for(let l of Object.keys(o))!r.has(l)&&!n.has(l)&&(n.add(l),i.push(l));return i.filter(o=>{let l=!1;for(let s of e){let a=s[o];if(!(a===void 0||a==="")){if(typeof a!="number")return!1;l=!0}}return l})}var H={fontSize:24,fontWeight:700,textAlign:"center",fill:E.markerColor},V={fontSize:30,fontWeight:600,textAlign:"center",fill:E.markerColor},W={weight:2,fill:E.gridColor},j={labelStyle:H,titleStyle:V,lineStyle:W,tickPadding:20,titlePadding:4},O={labelStyle:{...H,fontWeight:500,textAlign:"right"},titleStyle:V,lineStyle:W,tickPadding:40,titlePadding:24},v={fill:[],stroke:{weight:6,fill:E.borderColor},shadow:[]},K={padding:0,textStyle:{fontSize:30,fontWeight:600,textAlign:"left",fill:E.markerColor},itemGap:44,markerStyle:{width:60,height:24,borderRadius:6,stroke:[],shadow:[]}},je=Y.map(e=>({weight:4,fill:e})),Ke=Y.map(e=>e);function Je(e,t){return{labelStyle:S.resolveStyle(e?.labelStyle,t.labelStyle),titleStyle:S.resolveStyle(e?.titleStyle,t.titleStyle),lineStyle:g.resolve(e?.lineStyle??t.lineStyle),tickPadding:w.resolve(e?.tickPadding??t.tickPadding,t.tickPadding),titlePadding:w.resolve(e?.titlePadding??t.titlePadding,t.titlePadding)}}function Qe(e,t,r){return{labelStyle:S.lerpStyle(e.labelStyle,t.labelStyle,r),titleStyle:S.lerpStyle(e.titleStyle,t.titleStyle,r),lineStyle:g.lerp(e.lineStyle,t.lineStyle,r),tickPadding:w.lerp(e.tickPadding,t.tickPadding,r),titlePadding:w.lerp(e.titlePadding,t.titlePadding,r)}}function Ze(e,t){return{fill:b.resolve(e?.fill??t.fill),stroke:g.resolve(e?.stroke??t.stroke),shadow:M.resolve(e?.shadow??t.shadow)}}function qe(e,t,r){return{fill:b.lerp(e.fill,t.fill,r),stroke:g.lerp(e.stroke,t.stroke,r),shadow:M.lerp(e.shadow,t.shadow,r)}}function ye(e,t){return{width:e?.width??t.width,height:e?.height??t.height,borderRadius:e?.borderRadius??t.borderRadius,stroke:g.resolve(e?.stroke??t.stroke),shadow:M.resolve(e?.shadow??t.shadow)}}function ke(e,t,r){return{width:y(e.width,t.width,r),height:y(e.height,t.height,r),borderRadius:y(e.borderRadius,t.borderRadius,r),stroke:g.lerp(e.stroke,t.stroke,r),shadow:M.lerp(e.shadow,t.shadow,r)}}function et(e,t){return{padding:w.resolve(e?.padding??t.padding,t.padding),textStyle:S.resolveStyle(e?.textStyle,t.textStyle),itemGap:e?.itemGap??t.itemGap,markerStyle:ye(e?.markerStyle,t.markerStyle)}}function tt(e,t,r){return{padding:w.lerp(e.padding,t.padding,r),textStyle:S.lerpStyle(e.textStyle,t.textStyle,r),itemGap:y(e.itemGap,t.itemGap,r),markerStyle:ke(e.markerStyle,t.markerStyle,r)}}function rt(e,t){return(e??t).map(r=>g.resolve(r))}function nt(e,t,r){let n=Math.max(e.length,t.length),i=[];for(let o=0;o<n;o++){let l=e[o]??t[o],s=t[o]??e[o];i.push(g.lerp(l,s,r))}return i}function ot(e,t){return(e??t).map(r=>b.resolve(r))}function it(e,t,r){let n=Math.max(e.length,t.length),i=[];for(let o=0;o<n;o++){let l=e[o]??t[o],s=t[o]??e[o];i.push(b.lerp(l,s,r))}return i}function we(e,t,r){let n=e.length>0?e[r%e.length]:{weight:4,fill:"white"};if(t.fill==null)return n;let i=g.resolve(n);if(i.length===0)return{weight:4,fill:t.fill};let o=[...i];return o[o.length-1]={...o[o.length-1],fill:b.resolve(t.fill)},o}function lt(e,t,r){let n=g.resolve(we(e,t,r));return n[n.length-1]?.fill??"white"}function st(e,t,r){return t??(e.length===0?"white":e[r%e.length])}function J(e,t){if(Number.isInteger(e))return String(e);let r=Math.max(1,t===void 0?1:Se(t));return e.toFixed(r).replace(/(\.\d*?)0+$/,"$1").replace(/\.$/,"")}function Se(e){if(!(e>0)||!Number.isFinite(e))return 1;for(let t=0;t<=6;t++){let r=e*10**t;if(Math.abs(r-Math.round(r))<1e-6*Math.max(1,r))return t}return Math.min(6,Math.max(1,Math.ceil(-Math.log10(e))+1))}function at(e){let t=[...e].filter(Number.isFinite).sort((n,i)=>n-i),r;for(let n=1;n<t.length;n++){let i=t[n]-t[n-1];i>1e-12&&(r===void 0||i<r)&&(r=i)}return r}function ct(e){let t=e*100;return Number.isInteger(t)?`${t}%`:`${t.toFixed(1).replace(/\.0$/,"")}%`}function Q([e,t],r){let n=Math.max(2,Math.round(r));return Array.from({length:n+1},(i,o)=>e+(t-e)/n*o)}import{Rect as T}from"@motionscript/sdk";import{Rect as re,RotatedBox as Te,Text as ne,Types as G,component2d as Le,insetsOps as oe,textOps as ie}from"@motionscript/sdk";import{Types as be}from"@motionscript/sdk";function h(e,t){return be.custom(e,t)}function Ee(e){return e}import{Types as ve,easeOut as Ae}from"@motionscript/sdk";var Pe=e=>e;function A(e){return e.easing?Pe:Ae()}function q(e,t){let r=e.duration,n=t.reduce((o,l)=>Math.max(o,l.at+l.duration),0),i=n>r&&n>0?r/n:1;return e.together(t.map(({at:o,duration:l,piece:s})=>{let a=o*i,f=l*i;return e.sequence([e.wait(a),s,e.wait(Math.max(0,r-a-f))])}))}var R={opacity:1,shift:0};function ee(){return ve.custom({},{private:!0,resolve:(e,t)=>({...t,...e})})}function P(e,t){return e[t]??R}function te(e,t){let r={};for(let n of e)r[n]=t;return r}function C(e,t,r,n){return e.animate(i=>{let o=n(i);return{arrivals:{[t]:{opacity:r.opacity+(R.opacity-r.opacity)*o,shift:r.shift+(R.shift-r.shift)*o}}}})}var _e=24;function Me(e,t){let r=e.markers.indexOf(t);return(r>=0?e.captions[r]:void 0)??J(t)}var le=Le({key:"verticalAxis",state:{range:h(()=>[0,1]),markers:h(()=>[]),captions:h(()=>[]),axis:G.custom(O),label:"",arrivals:ee()},defaults:{width:"fill",height:"fill"},children:e=>{let t=oe.resolve(e.axis.tickPadding).right,r=oe.resolve(e.axis.titlePadding).right,n=[],i=()=>n[0]?.parent?.layoutBounds??{width:0,height:0},o=()=>[...e.markers].sort((f,c)=>c-f),l=f=>{if(f===void 0)return 0;let[c,p]=e.range;return p<=c?0:(f-c)/(p-c)},s=e.markers.map((f,c)=>{let p=new ne({...ie.styleProps(e.axis.labelStyle),text:()=>{let u=o()[c];return u===void 0?"":Me(e,u)},y:()=>{let u=i().height,d=(l(o()[c])-.5)*u,m=u/2-p.layoutBounds.height/2;return Math.min(d,m)},opacity:()=>P(e.arrivals,c).opacity,x:()=>P(e.arrivals,c).shift});return p}),a=[];return e.label!==""&&a.push(new Te({angle:-90,width:"hug",height:"hug",children:new ne({...ie.styleProps(e.axis.titleStyle),text:e.label,opacity:()=>P(e.arrivals,"label").opacity,x:()=>P(e.arrivals,"label").shift})})),a.push(new re({height:"fill",width:"hug",padding:{right:t},children:s})),n.push(new re({flow:"horizontal",gap:r,children:a})),n},commands:{enter:{args:{stagger:G.custom(void 0),slide:G.number(_e)},run:(e,t)=>{let{stagger:r,slide:n}=e.args,i=t.markers.length,o=t.label!=="",l={opacity:0,shift:n},s=[...t.markers.keys(),...o?["label"]:[]],a=e.animate(()=>({arrivals:te(s,l)}));if(i===0)return a;let{step:f,per:c,caption:p}=Z(e.duration,i,o,r),u=A(e),d=t.markers.map((k,N)=>({at:(i-1-N)*f,duration:c,piece:C(e,N,l,u)})),m=o?[{at:(i-1)*f+c,duration:p,piece:C(e,"label",l,u)}]:[];return e.together([a,q(e,[...d,...m])])}}}});var Oe=24;function vt(e){return new T({flow:"vertical",width:"fill",height:"fill",padding:{top:Oe},children:[new T({flow:"horizontal",height:"fill",width:"fill",children:[e.axis,e.plot]}),new T({flow:"horizontal",width:"fill",children:[e.gutter,new T({flow:"vertical",width:"fill",gap:20,children:[...e.below]})]})]})}function At(e){for(let t=e.length-1;t>=0;t--){let r=e[t];if(!T.is(r))continue;let[n,i]=r.children,o=n?.children[0],l=n?.children[1],s=i?.children[1];if(!(!o||!le.is(o)||!l||!s))return{axis:o,plot:l.children,below:s.children}}return null}function Pt(e){let t=e?.layoutBounds;return{width:t?.width??0,height:t?.height??0}}import{Graphics2D as Fe,Types as De,component2d as Re,strokeOps as se}from"@motionscript/sdk";function F(e,t){for(let r of t.fills??[])e.fill(r);for(let r of t.strokes??[])for(let n of r)e.fill(n.fill);for(let r of t.shadows??[])for(let n of r)e.fill(n.fill)}var Ft=Re({key:"chartBorder",state:{stroke:De.custom(v.stroke,{resolve:se.resolve,lerp:se.lerp})},load:(e,t)=>F(e,{strokes:[t.stroke]}),render:(e,t)=>{let{width:r,height:n}=e.size;r<=0||n<=0||e.draw(new Fe().rect({x:0,y:0,width:r,height:n}).stroke(t.stroke))}});import{Graphics2D as I,Types as z,component2d as Ce,fillOps as ae,shadowOps as ce,strokeOps as de}from"@motionscript/sdk";var he=e=>z.custom(e,{resolve:de.resolve,lerp:de.lerp}),zt=Ce({key:"chartCanvas",state:{fill:z.custom(v.fill,{resolve:ae.resolve,lerp:ae.lerp}),shadow:z.custom(v.shadow,{resolve:ce.resolve,lerp:ce.lerp}),xGridStroke:he(j.lineStyle),yGridStroke:he(O.lineStyle),xMarkers:h(()=>[]),yMarkers:h(()=>[]),xExtent:h(()=>[0,1]),yExtent:h(()=>[0,1])},load:(e,t)=>F(e,{fills:[t.fill],shadows:[t.shadow],strokes:[t.xGridStroke,t.yGridStroke]}),render:(e,t)=>{let{width:r,height:n}=e.size;if(r<=0||n<=0)return;e.draw(new I().rect({x:0,y:0,width:r,height:n}).shadow(t.shadow).fill(t.fill));let[i,o]=t.xExtent,[l,s]=t.yExtent,a=d=>o<=i?0:(d-i)/(o-i),f=d=>s<=l?0:(d-l)/(s-l),c=d=>d>1e-6&&d<1-1e-6,p=t.xGridStroke[0];if(p){let d=new I;for(let m of t.xMarkers){let k=a(m);c(k)&&d.rect({x:-r/2+k*r,y:0,width:p.weight,height:n})}e.draw(d.fill(p.fill))}let u=t.yGridStroke[0];if(u){let d=new I;for(let m of t.yMarkers){let k=f(m);c(k)&&d.rect({x:0,y:-n/2+k*n,width:r,height:u.weight})}e.draw(d.fill(u.fill))}}});import{Flow as _,Paint as x,Rect as Ge,Shapes as U,Text as Ie,Types as L,component2d as ue,textOps as ze}from"@motionscript/sdk";var pe={...x.state,..._.state,...U.corners},Ue=ue({key:"legendEntry",state:{...pe,shown:L.number(1)},commands:{...x.commands},measure:(e,t,r)=>{let n=_.measure(e,t,r,x.inset(t));return t.shown>=1?n:{...n,width:t.shown*n.width}},layout:(e,t,r)=>_.layout(e,t,r,x.inset(t)),render:(e,t)=>x.draw(e,t,U.rect(e.size,t))});function Ne(e,t){if(Array.isArray(e))return e;let r=[...t??[]];for(let[n,i]of Object.entries(e))r[Number(n)]=i;return r}var $t=ue({key:"chartLegend",state:{...pe,series:h(()=>[]),swatches:h(()=>[]),legend:L.custom(K),reveal:h(()=>[],{resolve:Ne})},defaults:{flow:"horizontal",width:"hug",height:"hug",gap:e=>e.legend.itemGap,padding:e=>e.legend.padding,reveal:e=>e.series.map(t=>t.enabled??!0?1:0)},children:e=>{let t=e.legend.markerStyle;return e.series.map((r,n)=>Ue({flow:"horizontal",gap:12,align:"center",width:"hug",height:"hug",opacity:()=>e.reveal[n]??0,gapScale:()=>e.reveal[n]??0,shown:()=>e.reveal[n]??0,children:[Ie({...ze.styleProps(e.legend.textStyle),text:r.label}),Ge({width:t.width,height:t.height,cornerRadius:t.borderRadius,fill:e.swatches[n]??"white",stroke:t.stroke,shadow:t.shadow})]}))},commands:{...x.commands,showSeries:{args:{index:L.number(0,{min:0,step:1})},run:(e,t)=>e.args.index in t.series?e.animate(fe(t,e.args.index,1,A(e))):e.wait(e.duration)},hideSeries:{args:{index:L.number(0,{min:0,step:1})},run:(e,t)=>e.args.index in t.series?e.animate(fe(t,e.args.index,0,A(e))):e.wait(e.duration)},spotlightSeries:{args:{index:L.number(0,{min:0,step:1})},run:(e,t,r)=>e.together(r.flatMap((n,i)=>(t.reveal[i]??0)>0?[n.to({opacity:i===e.args.index?1:$})]:[]))}},measure:(e,t,r)=>_.measure(e,t,r,x.inset(t)),layout:(e,t,r)=>_.layout(e,t,r,x.inset(t)),render:(e,t)=>x.draw(e,t,U.rect(e.size,t))});function fe(e,t,r,n){let i=e.reveal[t]??0;return o=>({reveal:{[t]:i+(r-i)*n(o)}})}function Ht(e,t){return(e.reveal[t]??0)>0}export*from"@motionscript/sdk/component";export{h as a,Ee as b,A as c,q as d,R as e,ee as f,P as g,te as h,C as i,ge as j,D as k,Be as l,Ye as m,X as n,E as o,Y as p,$ as q,$e as r,He as s,Ve as t,We as u,j as v,O as w,v as x,K as y,je as z,Ke as A,Je as B,Qe as C,Ze as D,qe as E,et as F,tt as G,rt as H,nt as I,ot as J,it as K,we as L,lt as M,st as N,J as O,at as P,ct as Q,Q as R,ft as S,Z as T,Me as U,le as V,Oe as W,vt as X,At as Y,Pt as Z,F as _,Ft as $,zt as aa,$t as ba,Ht as ca};
2
- //# sourceMappingURL=chunk-TJNHLQGV.js.map