@motionscript/charts 0.1.0 → 0.1.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bar-chart/bar-chart.d.ts +22 -93
  3. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  4. package/dist/bar-chart/bar-chart.js +48 -121
  5. package/dist/bar-chart/bar-chart.js.map +1 -1
  6. package/dist/bar-chart/category-axis.d.ts +11 -38
  7. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  8. package/dist/bar-chart/category-axis.js +15 -38
  9. package/dist/bar-chart/category-axis.js.map +1 -1
  10. package/dist/bar-chart/chart-bars.d.ts +43 -74
  11. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  12. package/dist/bar-chart/chart-bars.js +31 -59
  13. package/dist/bar-chart/chart-bars.js.map +1 -1
  14. package/dist/bar-chart/index.d.ts +2 -10
  15. package/dist/bar-chart/index.d.ts.map +1 -1
  16. package/dist/bar-chart/index.js +0 -7
  17. package/dist/bar-chart/index.js.map +1 -1
  18. package/dist/bar-chart/shared.d.ts +27 -90
  19. package/dist/bar-chart/shared.d.ts.map +1 -1
  20. package/dist/bar-chart/shared.js +15 -54
  21. package/dist/bar-chart/shared.js.map +1 -1
  22. package/dist/browser/chunks/chunk-4SMRCU24.js +2 -0
  23. package/dist/browser/chunks/chunk-4SMRCU24.js.map +7 -0
  24. package/dist/browser/index.js +1 -1
  25. package/dist/browser/index.js.map +3 -3
  26. package/dist/browser/kit.js +1 -1
  27. package/dist/browser/manifest.json +1 -1
  28. package/dist/engine.d.ts +4 -16
  29. package/dist/engine.d.ts.map +1 -1
  30. package/dist/engine.js +4 -16
  31. package/dist/engine.js.map +1 -1
  32. package/dist/kit/cartesian.d.ts +8 -28
  33. package/dist/kit/cartesian.d.ts.map +1 -1
  34. package/dist/kit/cartesian.js +7 -19
  35. package/dist/kit/cartesian.js.map +1 -1
  36. package/dist/kit/chart-border.d.ts +9 -18
  37. package/dist/kit/chart-border.d.ts.map +1 -1
  38. package/dist/kit/chart-border.js +4 -9
  39. package/dist/kit/chart-border.js.map +1 -1
  40. package/dist/kit/chart-canvas.d.ts +25 -36
  41. package/dist/kit/chart-canvas.d.ts.map +1 -1
  42. package/dist/kit/chart-canvas.js +13 -13
  43. package/dist/kit/chart-canvas.js.map +1 -1
  44. package/dist/kit/chart-legend.d.ts +14 -61
  45. package/dist/kit/chart-legend.d.ts.map +1 -1
  46. package/dist/kit/chart-legend.js +17 -65
  47. package/dist/kit/chart-legend.js.map +1 -1
  48. package/dist/kit/entries.d.ts +2 -0
  49. package/dist/kit/entries.d.ts.map +1 -1
  50. package/dist/kit/entries.js +2 -0
  51. package/dist/kit/entries.js.map +1 -1
  52. package/dist/kit/motion.d.ts +11 -3
  53. package/dist/kit/motion.d.ts.map +1 -1
  54. package/dist/kit/motion.js +7 -1
  55. package/dist/kit/motion.js.map +1 -1
  56. package/dist/kit/shared.d.ts +60 -175
  57. package/dist/kit/shared.d.ts.map +1 -1
  58. package/dist/kit/shared.js +40 -157
  59. package/dist/kit/shared.js.map +1 -1
  60. package/dist/kit/vertical-axis.d.ts +16 -45
  61. package/dist/kit/vertical-axis.d.ts.map +1 -1
  62. package/dist/kit/vertical-axis.js +17 -42
  63. package/dist/kit/vertical-axis.js.map +1 -1
  64. package/dist/line-chart/chart-line.d.ts +16 -41
  65. package/dist/line-chart/chart-line.d.ts.map +1 -1
  66. package/dist/line-chart/chart-line.js +14 -43
  67. package/dist/line-chart/chart-line.js.map +1 -1
  68. package/dist/line-chart/chart-region.d.ts +14 -27
  69. package/dist/line-chart/chart-region.d.ts.map +1 -1
  70. package/dist/line-chart/chart-region.js +8 -22
  71. package/dist/line-chart/chart-region.js.map +1 -1
  72. package/dist/line-chart/horizontal-axis.d.ts +16 -43
  73. package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
  74. package/dist/line-chart/horizontal-axis.js +13 -42
  75. package/dist/line-chart/horizontal-axis.js.map +1 -1
  76. package/dist/line-chart/index.d.ts +1 -13
  77. package/dist/line-chart/index.d.ts.map +1 -1
  78. package/dist/line-chart/index.js +0 -12
  79. package/dist/line-chart/index.js.map +1 -1
  80. package/dist/line-chart/line-chart.d.ts +23 -109
  81. package/dist/line-chart/line-chart.d.ts.map +1 -1
  82. package/dist/line-chart/line-chart.js +45 -141
  83. package/dist/line-chart/line-chart.js.map +1 -1
  84. package/dist/line-chart/shared.d.ts +33 -107
  85. package/dist/line-chart/shared.d.ts.map +1 -1
  86. package/dist/line-chart/shared.js +28 -81
  87. package/dist/line-chart/shared.js.map +1 -1
  88. package/dist/line-chart/x-scale.d.ts +14 -55
  89. package/dist/line-chart/x-scale.d.ts.map +1 -1
  90. package/dist/line-chart/x-scale.js +29 -111
  91. package/dist/line-chart/x-scale.js.map +1 -1
  92. package/dist/nodes.d.ts +0 -12
  93. package/dist/nodes.d.ts.map +1 -1
  94. package/dist/nodes.js +0 -12
  95. package/dist/nodes.js.map +1 -1
  96. package/dist/pie-chart/index.d.ts +1 -12
  97. package/dist/pie-chart/index.d.ts.map +1 -1
  98. package/dist/pie-chart/index.js +0 -9
  99. package/dist/pie-chart/index.js.map +1 -1
  100. package/dist/pie-chart/pie-chart.d.ts +37 -71
  101. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  102. package/dist/pie-chart/pie-chart.js +31 -65
  103. package/dist/pie-chart/pie-chart.js.map +1 -1
  104. package/dist/pie-chart/pie-legend.d.ts +11 -26
  105. package/dist/pie-chart/pie-legend.d.ts.map +1 -1
  106. package/dist/pie-chart/pie-legend.js +14 -39
  107. package/dist/pie-chart/pie-legend.js.map +1 -1
  108. package/dist/pie-chart/pie-ring.d.ts +50 -192
  109. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  110. package/dist/pie-chart/pie-ring.js +75 -247
  111. package/dist/pie-chart/pie-ring.js.map +1 -1
  112. package/dist/pie-chart/shared.d.ts +45 -131
  113. package/dist/pie-chart/shared.d.ts.map +1 -1
  114. package/dist/pie-chart/shared.js +29 -72
  115. package/dist/pie-chart/shared.js.map +1 -1
  116. package/package.json +4 -4
  117. package/src/bar-chart/bar-chart.ts +51 -165
  118. package/src/bar-chart/category-axis.ts +16 -57
  119. package/src/bar-chart/chart-bars.ts +43 -120
  120. package/src/bar-chart/index.ts +2 -10
  121. package/src/bar-chart/shared.ts +29 -97
  122. package/src/engine.ts +4 -16
  123. package/src/kit/cartesian.ts +11 -26
  124. package/src/kit/chart-border.ts +6 -20
  125. package/src/kit/chart-canvas.ts +15 -40
  126. package/src/kit/chart-legend.ts +17 -86
  127. package/src/kit/entries.ts +4 -1
  128. package/src/kit/motion.ts +11 -2
  129. package/src/kit/shared.ts +58 -219
  130. package/src/kit/vertical-axis.ts +18 -71
  131. package/src/line-chart/chart-line.ts +16 -44
  132. package/src/line-chart/chart-region.ts +10 -23
  133. package/src/line-chart/horizontal-axis.ts +13 -42
  134. package/src/line-chart/index.ts +1 -13
  135. package/src/line-chart/line-chart.ts +50 -184
  136. package/src/line-chart/shared.ts +51 -145
  137. package/src/line-chart/x-scale.ts +31 -121
  138. package/src/nodes.ts +0 -12
  139. package/src/pie-chart/index.ts +1 -12
  140. package/src/pie-chart/pie-chart.ts +34 -102
  141. package/src/pie-chart/pie-legend.ts +14 -55
  142. package/src/pie-chart/pie-ring.ts +80 -307
  143. package/src/pie-chart/shared.ts +50 -139
  144. package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
  145. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
@@ -1,17 +1,16 @@
1
1
  import {
2
2
  Rect,
3
3
  Types,
4
+ clamp,
4
5
  component2d,
5
6
  easeOut,
6
7
  type ChildView,
7
8
  type DataRecord,
8
- type EntryOptions,
9
9
  type NodeOf,
10
- // Named so this component's emitted type reaches `Node2D` through the sdk, not core.
10
+ // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
11
11
  type Node2D,
12
- type Node2DProps,
12
+ type Node2DProps as _Node2DProps,
13
13
  type Size2D,
14
- type Timeline,
15
14
  } from "@motionscript/sdk"
16
15
  import { formatNumber } from "@motionscript/sdk/component"
17
16
 
@@ -19,11 +18,10 @@ import { cartesianFrame, cartesianViews, laidOutSize } from "../kit/cartesian"
19
18
  import { ChartBorder } from "../kit/chart-border"
20
19
  import { ChartCanvas } from "../kit/chart-canvas"
21
20
  import { ChartLegend } from "../kit/chart-legend"
22
- import { asParts, fresh } from "../kit/entries"
23
- import { beats, type Beat } from "../kit/motion"
21
+ import { SERIES_ARG, asParts, fresh } from "../kit/entries"
22
+ import { DEFAULT_MARKERS, ENTER_SHARE, LEGEND_SLIDE, beats, type AnyTimeline, type Beat } from "../kit/motion"
24
23
  import { VerticalAxis } from "../kit/vertical-axis"
25
24
  import {
26
- clampUnit,
27
25
  formatMarker,
28
26
  lerpAxisDomain,
29
27
  lerpAxisEntry,
@@ -34,6 +32,7 @@ import {
34
32
  settleAxisDomain,
35
33
  type AxisDomainConfig,
36
34
  type Range,
35
+ type ResolvedAxisDomain,
37
36
  } from "../kit/shared"
38
37
  import { CategoryAxis } from "./category-axis"
39
38
  import { ChartBars, type BarGeometry } from "./chart-bars"
@@ -51,39 +50,6 @@ import {
51
50
  type BarSeries,
52
51
  } from "./shared"
53
52
 
54
- export interface BarChartProps extends Node2DProps {
55
- /** One row per category — a plain record of field name to value. */
56
- data: DataRecord[]
57
- /**
58
- * Key into each row for its category caption, drawn beneath the baseline.
59
- * Named `categoryField` rather than `category` for the same reason the line
60
- * chart's is `xField`: it names a *column*, not a value.
61
- */
62
- categoryField: string
63
- /** One entry per bar series: which column it plots, its `label`, its `fill`. */
64
- series: BarSeries[]
65
- /**
66
- * When `true` every category's series stack into one column; when `false`
67
- * (the default) each series gets its own bar, grouped side by side within the
68
- * category's slot.
69
- */
70
- stacked?: boolean
71
- /** The value axis's domain: caption, scale range, and tick placement. */
72
- valueAxis?: AxisDomainConfig
73
- /** The category axis's domain. Only its `label` is read — the slots are the rows. */
74
- categoryAxis?: AxisDomainConfig
75
- /**
76
- * The chart's visual theme — bar palette and geometry, axis furniture, plot
77
- * panel paint, legend, value figures. Every branch is optional and deep-merged
78
- * onto {@link DEFAULT_BAR_CHART_THEME}.
79
- */
80
- theme?: BarChartTheme
81
- /** Whether the legend row below the plot is drawn at all. */
82
- showLegend?: boolean
83
- /** Whether each column prints the figure it plots. */
84
- showValues?: boolean
85
- }
86
-
87
53
  /** What a bar chart plots, as its entries hold it. */
88
54
  export interface BarChartValues {
89
55
  readonly data: DataRecord[]
@@ -93,27 +59,8 @@ export interface BarChartValues {
93
59
  }
94
60
 
95
61
  /** The value axis's domain with its range and ticks resolved. */
96
- export type BarChartDomain = AxisDomainConfig & { readonly range: Range; readonly ticks: number[] }
97
-
98
- /** The argument every per-series command takes: an index into `series`, offered by name. */
99
- const SERIES_ARG = Types.custom<number, number, EntryOptions & { readonly min: number; readonly step: number }>(0, { kind: "series", min: 0, step: 1 })
100
-
101
- /** How far the legend starts below its slot when it enters, in px. */
102
- const LEGEND_SLIDE = 20
103
-
104
- /** Fallback marker count when the value axis states no ticks. */
105
- const DEFAULT_MARKERS = 5
106
-
107
- /**
108
- * How `enter` divides its duration between its three beats: the axes arriving,
109
- * the columns rising, and the legend coming up. Shares of the whole, so they
110
- * must sum to 1.
111
- */
112
- const AXES_SHARE = 0.35
113
- const BARS_SHARE = 0.5
114
- const LEGEND_SHARE = 0.15
62
+ export type BarChartDomain = ResolvedAxisDomain
115
63
 
116
- /** What the geometry reads: what the chart plots, its theme and the signals its commands move. */
117
64
  interface BarState extends BarChartValues {
118
65
  readonly theme: BarChartConfig
119
66
  readonly weights: number[]
@@ -121,17 +68,16 @@ interface BarState extends BarChartValues {
121
68
  readonly spotlightReveal: number
122
69
  }
123
70
 
124
- /** The value axis with its range and ticks resolved: the explicit ones, else five across the scale. */
125
71
  function drawnValue(s: BarChartValues, domain: AxisDomainConfig): AxisDomainConfig {
126
72
  const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
127
73
  const range = resolveBarScale(s.data, s.series, s.stacked, domain.range, divisions)
128
74
  return { ...domain, range, ticks: domain.ticks ?? markerValues(range, DEFAULT_MARKERS - 1) }
129
75
  }
130
76
 
131
- /** The last domain worked out from each data array, and what else it was read from. */
77
+ /** The last domain worked out from each data array, and what it was read from. */
132
78
  const domainsDrawn = new WeakMap<DataRecord[], Omit<BarChartValues, "data"> & { readonly domain: BarChartDomain }>()
133
79
 
134
- /** The value axis as a bar chart draws it now, an auto range or tick row read back as itself. */
80
+ /** The value axis as drawn now. */
135
81
  export function barChartDomain(s: BarChartValues): BarChartDomain {
136
82
  const { data, series, stacked, valueAxis } = s
137
83
  const kept = domainsDrawn.get(data)
@@ -141,38 +87,30 @@ export function barChartDomain(s: BarChartValues): BarChartDomain {
141
87
  return domain
142
88
  }
143
89
 
144
- /**
145
- * A value written down as this chart's value axis says: the ticks and the
146
- * figures printed on the columns both read through here, so the two cannot
147
- * disagree about what a number looks like.
148
- */
90
+ /** Ticks and the figures printed on columns both read through here, so they cannot disagree about how a number looks. */
149
91
  function valueLabel(s: BarChartValues, domain: BarChartDomain, value: number): string {
150
92
  const pattern = s.valueAxis.format
151
93
  return pattern === undefined ? formatMarker(value, markerStep(domain.ticks)) : formatNumber(value, pattern)
152
94
  }
153
95
 
154
- /** A value on the plot's **centred** pixel space (origin at its centre, y-up). */
96
+ /** A value in the plot's centred pixel space (origin at its centre, y-up). */
155
97
  function valueToY(domain: BarChartDomain, plot: Size2D, value: number): number {
156
98
  const [min, max] = domain.range
157
99
  if (max <= min) return -plot.height / 2
158
100
  return ((value - min) / (max - min) - 0.5) * plot.height
159
101
  }
160
102
 
161
- /**
162
- * Pixel y of the line every column grows from: zero clamped onto the scale, so
163
- * a cropped axis grows its columns from the plot floor.
164
- */
103
+ /** Pixel y columns grow from: zero clamped onto the scale, so a cropped axis grows them from the plot floor. */
165
104
  function baselinePx(domain: BarChartDomain, plot: Size2D): number {
166
105
  const [min, max] = domain.range
167
106
  return valueToY(domain, plot, Math.min(Math.max(0, min), max))
168
107
  }
169
108
 
170
- /** Series `i`'s live share of the slot; 0 once it has been hidden. */
171
109
  function weightAt(s: BarState, index: number): number {
172
110
  return Math.max(0, s.weights[index] ?? 1)
173
111
  }
174
112
 
175
- /** `row` with each series' value scaled by its live weight: what the stacked spans are measured from. */
113
+ /** `row` with each series' value scaled by its live weight, which stacked spans are measured from. */
176
114
  function weightedRow(s: BarState, row: DataRecord): DataRecord {
177
115
  const scaled: DataRecord = { ...row }
178
116
  s.series.forEach((series, i) => {
@@ -183,10 +121,9 @@ function weightedRow(s: BarState, row: DataRecord): DataRecord {
183
121
  }
184
122
 
185
123
  /**
186
- * Series `index`'s columns, mapped into the plot's centred pixel space. A
187
- * grouped series divides the slot with its siblings by their live weights, a
188
- * stacked one takes the whole slot and runs from the running total below it,
189
- * so hiding a series reflows rather than punching a hole.
124
+ * Series `index`'s columns in centred pixel space. A grouped series divides the
125
+ * slot with its siblings by live weight, a stacked one takes the whole slot, so
126
+ * hiding a series reflows rather than punching a hole.
190
127
  */
191
128
  function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean): BarGeometry[] {
192
129
  const series = s.series[index]
@@ -195,7 +132,7 @@ function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean
195
132
  const n = s.data.length
196
133
  const slotWidth = n > 0 ? plot.width / n : 0
197
134
  const slotCenter = (i: number) => -plot.width / 2 + slotWidth * (i + 0.5)
198
- const inner = slotWidth * clampUnit(s.theme.bar.slotFill)
135
+ const inner = slotWidth * clamp(s.theme.bar.slotFill, 0, 1)
199
136
  const gap = s.theme.bar.groupGap
200
137
  let total = 0
201
138
  for (let i = 0; i < s.series.length; i++) total += weightAt(s, i)
@@ -205,15 +142,14 @@ function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean
205
142
  for (let i = 0; i < index; i++) before += weightAt(s, i)
206
143
 
207
144
  return s.data.map((row, rowIndex) => {
208
- // The stack is built from the *live* contributions, so a series fading out
209
- // lowers the segments above it as it goes.
145
+ // Built from live contributions, so a fading series lowers the segments above it.
210
146
  const spans = barSpans(s.stacked ? weightedRow(s, row) : row, s.series, s.stacked)
211
147
  const span = spans[index] ?? { from: 0, to: 0, value: 0 }
212
148
  const cap = s.stacked ? stackCapIndex(row, s.series) : index
213
149
  const slotLeft = slotCenter(rowIndex) - inner / 2
214
150
  const x = s.stacked ? slotCenter(rowIndex) : slotLeft + unit * before + (unit * weight) / 2
215
151
  const width = s.stacked ? inner : Math.max(0, unit * weight - (s.series.length > 1 ? gap : 0))
216
- // The figure printed is the row's own value, not the weighted one.
152
+ // The printed figure is the row's own value, not the weighted one.
217
153
  const value = num(row, series.field)
218
154
  return {
219
155
  x,
@@ -227,20 +163,14 @@ function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean
227
163
  })
228
164
  }
229
165
 
230
- /**
231
- * How far each category is dimmed by the spotlight, one entry per row: 1 while
232
- * nothing is spotlit, falling to the theme's `dimOpacity` for every category but
233
- * the chosen one as the reveal climbs.
234
- */
166
+ /** Per-row opacity: 1 while nothing is spotlit, falling to the theme's `dimOpacity` for all but the chosen category. */
235
167
  function categoryDims(s: BarState): number[] {
236
- const focus = clampUnit(s.spotlightReveal)
168
+ const focus = clamp(s.spotlightReveal, 0, 1)
237
169
  const dim = s.theme.spotlight.dimOpacity
238
170
  const chosen = s.spotlight
239
171
  return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - dim) * focus))
240
172
  }
241
173
 
242
- type AnyTimeline = Timeline<any, any>
243
-
244
174
  /** The parts a bar chart's commands drive, found through its children. */
245
175
  function partsOf(children: readonly ChildView[]) {
246
176
  const frame = cartesianViews(children)
@@ -256,16 +186,14 @@ function partsOf(children: readonly ChildView[]) {
256
186
  }
257
187
 
258
188
  /**
259
- * Move series `index`'s slot share to `target` with its legend entry, the group
260
- * reflowing. Its columns are drawn for every frame the move covers, and switch
261
- * off only once a hide has landed, so arriving at a time backwards shows what
262
- * arriving forwards does.
189
+ * Move series `index`'s slot share to `target` with its legend entry. Its
190
+ * columns stay drawn for every frame the move covers and switch off only once a
191
+ * hide has landed, so arriving at a time backwards matches arriving forwards.
263
192
  */
264
193
  function toggling(timeline: AnyTimeline, state: BarState & { readonly showing: boolean[] }, children: readonly ChildView[], index: number, target: number) {
265
194
  const parts = partsOf(children)
266
195
  if (!parts || !parts.bars[index]) return timeline.wait(timeline.duration)
267
- // Read through `weightAt`, so a chart whose stored weights are short still
268
- // tweens from the 1 it is drawing at.
196
+ // Through `weightAt`, so short stored weights still tween from the 1 drawn.
269
197
  const from = state.series.map((_, i) => weightAt(state, i))
270
198
  const start = from[index] ?? 1
271
199
  const showing = state.series.map((s, i) => state.showing[i] ?? (s.enabled ?? true))
@@ -282,51 +210,32 @@ function toggling(timeline: AnyTimeline, state: BarState & { readonly showing: b
282
210
  }
283
211
 
284
212
  /**
285
- * A grouped or stacked bar chart, the sibling of the line chart and built the
286
- * same way, out of several of the same pieces:
213
+ * A grouped or stacked bar chart, built like the line chart from `ChartCanvas`,
214
+ * `VerticalAxis`, {@link CategoryAxis}, one {@link ChartBars} per series and
215
+ * `ChartLegend`. Zero is always on the scale and columns grow about it, so a
216
+ * negative value draws downward from the same `growth` signal.
287
217
  *
288
- * - `ChartCanvas` — the plot panel with a horizontal grid, under everything.
289
- * - `VerticalAxis` — the numeric value markers, staggering in bottom-to-top.
290
- * - {@link CategoryAxis} — the row of names beneath the baseline, one per row.
291
- * - {@link ChartBars} (one per series) — that series' column in every category,
292
- * plus the figures printed on them.
293
- * - `ChartLegend` — a row of entries below the chart, one per series.
294
- *
295
- * Every part reads the chart's entries through bindings, and the columns map
296
- * across the plot panel's own laid-out box, so a column's end lands exactly on
297
- * its marker. **Zero is always on the scale**, and the columns grow about it, so
298
- * a negative value draws downward out of the same `growth` signal.
299
- *
300
- * **Resting state is the finished chart**; `enter` arms every part on its first
301
- * frame. `showSeries`/`hideSeries`/`spotlightSeries` pick out a *series* (hiding
302
- * one reflows the group), `spotlightCategory`/`clearCategory` a *column of the
303
- * table*. A `to` of the value axis's `range` or `ticks` tweens from the domain
304
- * as drawn, so an auto axis rescales the columns continuously.
218
+ * Resting state is the finished chart; `enter` arms every part on its first
219
+ * frame. A `to` of the value axis's `range` or `ticks` tweens from the domain as
220
+ * drawn, so an auto axis rescales the columns continuously.
305
221
  */
306
222
  export const BarChart = component2d({
307
223
  key: "barChart",
308
224
  state: {
309
225
  data: fresh<DataRecord[]>(() => []),
226
+ // Names a column, not a value, like the line chart's `xField`.
310
227
  categoryField: "",
311
228
  series: fresh<BarSeries[]>(() => []),
312
229
  stacked: false,
313
230
  valueAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
314
231
  categoryAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisDomain }),
315
232
  theme: Types.custom<BarChartTheme, BarChartConfig>(DEFAULT_BAR_CHART_THEME, { resolve: resolveBarChartTheme, lerp: lerpBarChartTheme }),
316
- /**
317
- * Global rise of every column in `[0, 1]`, measured about the baseline: 0
318
- * collapses them all onto it, 1 draws them at full length.
319
- */
233
+ /** Rise of every column about the baseline, `[0, 1]`: 0 collapses them onto it. */
320
234
  growth: 1,
321
- /**
322
- * How much of the slot each series claims, one entry per series: a weight
323
- * rather than a switch, so hiding one is the others widening into the space.
324
- * Seeded from each series' `enabled`.
325
- */
235
+ /** Each series' share of the slot, seeded from `enabled`: a weight rather than a switch, so hiding one widens the others. */
326
236
  weights: fresh<number[]>(() => []),
327
- /** The category spotlit, or `null` for none. The choice snaps; `spotlightReveal` is the animation. */
237
+ /** The category spotlit, or `null`. The choice snaps; `spotlightReveal` animates it. */
328
238
  spotlight: Types.custom<number | null>(null),
329
- /** How far the category spotlight is drawn in, `[0, 1]`. */
330
239
  spotlightReveal: 0,
331
240
  /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
332
241
  showLegend: Types.boolean(true, { animatable: false }),
@@ -336,7 +245,7 @@ export const BarChart = component2d({
336
245
  showing: Types.custom<boolean[]>([], { private: true }),
337
246
  },
338
247
  defaults: {
339
- // Its parts fill it; a node made of them would hug them.
248
+ // A node made of parts that fill would otherwise hug them.
340
249
  width: "fill",
341
250
  height: "fill",
342
251
  weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
@@ -347,7 +256,6 @@ export const BarChart = component2d({
347
256
  const theme = () => state.theme
348
257
  const domain = () => barChartDomain(state)
349
258
  let plot: Node2D | undefined
350
- // Before the panel is built or laid out it has no size, and the columns map onto nothing.
351
259
  const plotSize = () => laidOutSize(plot)
352
260
  const captioned = state.showValues === true
353
261
 
@@ -397,8 +305,7 @@ export const BarChart = component2d({
397
305
  series: state.series,
398
306
  swatches: () => state.series.map((s, i) => resolveBarFill(theme().seriesFills, s, i)),
399
307
  legend: () => theme().legend,
400
- // The row's spacing is the theme's as the chart was built, like the
401
- // entries' type and chips: a theme tween re-paints the swatches only.
308
+ // Spacing is fixed as built; a theme tween re-paints the swatches only.
402
309
  gap: state.theme.legend.itemGap,
403
310
  padding: state.theme.legend.padding,
404
311
  }) as unknown as Node2D
@@ -413,8 +320,7 @@ export const BarChart = component2d({
413
320
  shadow: () => theme().plotArea.shadow,
414
321
  xGridStroke: () => theme().xAxis.lineStyle,
415
322
  yGridStroke: () => theme().yAxis.lineStyle,
416
- // No vertical rules: the horizontal axis is categorical, so there is no
417
- // numeric position along it for a grid line to mark.
323
+ // Categorical x axis: no numeric position for a vertical rule to mark.
418
324
  xMarkers: [],
419
325
  yMarkers: () => domain().ticks,
420
326
  xExtent: () => [0, 1] as Range,
@@ -438,27 +344,20 @@ export const BarChart = component2d({
438
344
  return asParts([cartesianFrame({ axis: vAxis as unknown as Node2D, plot, gutter, below })])
439
345
  },
440
346
  commands: {
441
- /**
442
- * Animate the whole chart in: the two axes stagger their markers and
443
- * captions in, the columns rise from the baseline, and the legend fades up
444
- * last. Its first frame arms every part: markers and captions shifted and
445
- * transparent, columns collapsed, the legend dropped and transparent.
446
- * `duration` is the **whole** entrance; the beats are shares of it.
447
- */
347
+ /** Axes stagger in, columns rise from the baseline, the legend fades up last. `duration` is the whole entrance. */
448
348
  enter: {
449
349
  args: {},
450
- // Held from the chart's arrival, so it never shows finished ahead of its draw.
350
+ // Held from arrival, so it never shows finished ahead of its draw.
451
351
  entrance: true,
452
352
  easing: easeOut(),
453
353
  run: (timeline, _state, children) => {
454
354
  const parts = partsOf(children)
455
355
  if (!parts) return timeline.wait(timeline.duration)
456
356
  const duration = timeline.duration
457
- const axes = duration * AXES_SHARE
458
- const rise = duration * BARS_SHARE
357
+ const axes = duration * ENTER_SHARE.axes
358
+ const rise = duration * ENTER_SHARE.marks
459
359
  const { legend } = parts
460
360
  const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
461
- // Without a legend the beat is still its share of the whole.
462
361
  return beats(timeline, [
463
362
  at0(timeline.animate(() => ({ growth: 0 }))),
464
363
  ...(legend ? [at0(legend.to({ opacity: 0, y: -LEGEND_SLIDE }))] : []),
@@ -466,31 +365,23 @@ export const BarChart = component2d({
466
365
  { at: 0, duration: axes, piece: parts.cAxis.run("enter", { slide: 24 }) },
467
366
  // No priming to 0 first: the value at `t` is a function of `t`.
468
367
  { at: axes, duration: rise, piece: timeline.animate((t) => ({ growth: t })) },
469
- ...(legend ? [{ at: axes + rise, duration: duration * LEGEND_SHARE, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
368
+ ...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
470
369
  ])
471
370
  },
472
371
  },
473
- /** Show series `index`: its columns come back, the group reflowing to make room, with its legend entry. */
372
+ /** Bring series `index` back, the group reflowing to make room, with its legend entry. */
474
373
  showSeries: {
475
374
  args: { seriesIndex: SERIES_ARG },
476
375
  easing: easeOut(),
477
376
  run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 1),
478
377
  },
479
- /**
480
- * Hide series `index`: its columns narrow away (or, stacked, sink out of the
481
- * stack) while the rest of the group widens into the space — the mirror of
482
- * `showSeries`.
483
- */
378
+ /** The mirror of `showSeries`: columns narrow away (stacked, sink out) while the rest widen into the space. */
484
379
  hideSeries: {
485
380
  args: { seriesIndex: SERIES_ARG },
486
381
  easing: easeOut(),
487
382
  run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 0),
488
383
  },
489
- /**
490
- * Fade every other series — its columns **and its legend entry** — to focus
491
- * on `index`, which comes back to full. Nothing moves, so the group doesn't
492
- * reflow.
493
- */
384
+ /** Fade every other series, columns and legend entry, to focus on `index`. Nothing moves, so the group does not reflow. */
494
385
  spotlightSeries: {
495
386
  args: { seriesIndex: SERIES_ARG },
496
387
  easing: easeOut(),
@@ -506,10 +397,9 @@ export const BarChart = component2d({
506
397
  },
507
398
  },
508
399
  /**
509
- * Spotlight one **category**: every other category's columns and caption
510
- * fade to the theme's `dimOpacity`. Called while another is lit, the focus
511
- * moves directly: only the dimming changes, so nothing retracts first. A
512
- * category that is not there takes its slot and changes nothing.
400
+ * Fade every other category's columns and caption to `dimOpacity`. While
401
+ * another is lit the focus moves directly. A missing category takes its slot
402
+ * and changes nothing.
513
403
  */
514
404
  spotlightCategory: {
515
405
  args: { categoryIndex: Types.number(0, { min: 0, step: 1 }) },
@@ -521,11 +411,7 @@ export const BarChart = component2d({
521
411
  return timeline.animate((t) => ({ spotlight: index, spotlightReveal: from + (1 - from) * t }))
522
412
  },
523
413
  },
524
- /**
525
- * Drop the category spotlight, every category back to full. The lit
526
- * category is held until the fade lands; a no-op when nothing is lit, which
527
- * still takes its slot.
528
- */
414
+ /** Every category back to full; the lit one is held until the fade lands. A no-op when nothing is lit, which still takes its slot. */
529
415
  clearCategory: {
530
416
  args: {},
531
417
  easing: easeOut(),
@@ -2,63 +2,32 @@ import {
2
2
  Rect,
3
3
  Text,
4
4
  Types,
5
+ clamp,
5
6
  component2d,
6
7
  insetsOps,
7
8
  textOps,
8
9
  type NodeOf,
10
+ // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
9
11
  type Node2D,
10
- type Node2DProps,
12
+ type Node2DProps as _Node2DProps,
11
13
  } from "@motionscript/sdk"
14
+ import { enterBudget } from "@motionscript/sdk/component"
12
15
 
13
16
  import { asParts, fresh } from "../kit/entries"
14
17
  import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
15
18
  import {
16
19
  DEFAULT_X_AXIS,
17
- clampUnit,
18
- enterBudget,
19
20
  type AxisConfig,
20
21
  } from "../kit/shared"
21
22
 
22
- export interface CategoryAxisProps extends Node2DProps {
23
- /** One caption per category slot, left to right — the chart's rows, in order. */
24
- categories: string[]
25
- /** Caption/title text style and gutters — see {@link AxisConfig}. */
26
- axis: AxisConfig
27
- /**
28
- * Caption drawn centred in the gutter beneath the category row. Empty string
29
- * (the default) hides it.
30
- */
31
- label: string
32
- /**
33
- * Per-category opacity multiplier — how the chart's category spotlight reaches
34
- * the captions, so the name under a dimmed column dims with it. Missing
35
- * entries read as 1.
36
- */
37
- dims: number[]
38
- }
39
-
40
- /** How far a caption starts above its slot when it enters, in px. */
41
23
  const SLIDE = 24
42
24
 
43
25
  /**
44
- * The **categorical** horizontal axis of a {@link BarChart}: a row of
45
- * {@link Text} captions, one per row of the data, each centred on its slot
46
- * beneath the baseline.
47
- *
48
- * Not a variant of the line chart's `HorizontalAxis` and deliberately not
49
- * folded into it: that axis places markers by *value* across a numeric range,
50
- * this one divides its width into equal slots and centres a name in each. The
51
- * two look alike on screen and share nothing underneath — one reads a scale,
52
- * the other counts rows.
53
- *
54
- * The slot arithmetic is the same as the plot's, because this node is laid out
55
- * to exactly the plot's width (the chart reserves the axis gutter with an
56
- * invisible twin of the value axis, as the line chart does), so a caption lands
57
- * under the columns it names without either side being told about the other.
58
- *
59
- * `enter` staggers the captions in left-to-right, each lifted a little above
60
- * its slot and transparent; the resting state is the landed one, so a chart
61
- * that is never told to enter still reads as finished.
26
+ * The categorical axis of a {@link BarChart}: one {@link Text} caption per row,
27
+ * centred on its slot. Not folded into `HorizontalAxis`, which places markers by
28
+ * value on a scale; this divides its width into equal slots. It is laid out to
29
+ * exactly the plot's width, so each caption lands under its columns with neither
30
+ * side told about the other. The resting state is landed.
62
31
  */
63
32
  export const CategoryAxis = component2d({
64
33
  key: "categoryAxis",
@@ -66,10 +35,11 @@ export const CategoryAxis = component2d({
66
35
  categories: fresh<string[]>(() => []),
67
36
  axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
68
37
  label: "",
38
+ /** Per-category opacity multiplier, so the caption under a dimmed column dims with it. Missing reads as 1. */
69
39
  dims: fresh<number[]>(() => []),
70
40
  arrivals: arrivalsEntry(),
71
41
  },
72
- // A plain node filled its parent; one made of its captions would hug them.
42
+ // A node made of captions would otherwise hug them.
73
43
  defaults: { width: "fill", height: "fill" },
74
44
  children: (state) => {
75
45
  const tickPad = insetsOps.resolve(state.axis.tickPadding).top
@@ -78,23 +48,19 @@ export const CategoryAxis = component2d({
78
48
  const column: Node2D[] = []
79
49
  // The column's parent is the axis: its box is what the slots divide.
80
50
  const width = () => (column[0]?.parent as Node2D | null | undefined)?.layoutBounds.width ?? 0
81
- /** Centre x of slot `i`, in the node's centred space. */
82
51
  const slotCenter = (index: number) => {
83
52
  const n = state.categories.length
84
53
  if (n <= 0) return 0
85
54
  return -width() / 2 + (width() / n) * (index + 0.5)
86
55
  }
87
56
 
88
- // Each caption sits in a hug box carrying the spotlight dim, so the
89
- // entrance can animate the text's own opacity without the two fighting over
90
- // one value — opacity multiplies down the tree, which is exactly the
91
- // composition wanted here.
57
+ // The spotlight dim sits on a hug box around each caption, so the entrance animates the text's own opacity without the two fighting over one value.
92
58
  const slots = state.categories.map((caption, i) =>
93
59
  new Rect({
94
60
  width: "hug",
95
61
  height: "hug",
96
62
  flow: "freeform",
97
- opacity: () => clampUnit(state.dims[i] ?? 1),
63
+ opacity: () => clamp(state.dims[i] ?? 1, 0, 1),
98
64
  x: () => slotCenter(i),
99
65
  children: [
100
66
  new Text({
@@ -124,16 +90,9 @@ export const CategoryAxis = component2d({
124
90
  },
125
91
  commands: {
126
92
  /**
127
- * Slide + fade every caption in from above, staggered left-to-right, then
128
- * bring the title down behind them. Its first frame arms them all: lifted
129
- * by `slide` and transparent.
130
- *
131
- * The timeline's duration is the **whole** entrance — the stagger and the
132
- * title are carved out of it rather than added on top. See {@link enterBudget}.
133
- *
134
- * `stagger` is seconds between successive captions (derived when omitted);
135
- * `slide` is the starting upward offset in px (node space is y-up, so a
136
- * positive start slides *down* into place).
93
+ * Slide and fade captions in left to right, then the title. The duration is
94
+ * the whole entrance ({@link enterBudget}). `stagger` is seconds between
95
+ * captions (derived when omitted); `slide` is the starting offset in px.
137
96
  */
138
97
  enter: {
139
98
  args: {