@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
@@ -5,13 +5,11 @@ import {
5
5
  easeOut,
6
6
  type ChildView,
7
7
  type DataRecord,
8
- type EntryOptions,
9
8
  type NodeOf,
10
- // Named so this component's emitted type reaches `Node2D` through the sdk, not core.
9
+ // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
11
10
  type Node2D,
12
- type Node2DProps,
11
+ type Node2DProps as _Node2DProps,
13
12
  type Size2D,
14
- type Timeline,
15
13
  type Vector2,
16
14
  } from "@motionscript/sdk"
17
15
  import { formatNumber } from "@motionscript/sdk/component"
@@ -20,8 +18,8 @@ import { cartesianFrame, cartesianViews, laidOutSize } from "../kit/cartesian"
20
18
  import { ChartBorder } from "../kit/chart-border"
21
19
  import { ChartCanvas } from "../kit/chart-canvas"
22
20
  import { ChartLegend } from "../kit/chart-legend"
23
- import { asParts, fresh } from "../kit/entries"
24
- 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"
25
23
  import { VerticalAxis } from "../kit/vertical-axis"
26
24
  import {
27
25
  SPOTLIGHT_DIM,
@@ -36,6 +34,7 @@ import {
36
34
  settleAxisDomain,
37
35
  type AxisDomainConfig,
38
36
  type Range,
37
+ type ResolvedAxisDomain,
39
38
  } from "../kit/shared"
40
39
  import { ChartLine } from "./chart-line"
41
40
  import { ChartRegion } from "./chart-region"
@@ -55,41 +54,10 @@ import {
55
54
  } from "./shared"
56
55
  import { resolveXScale, type XScale } from "./x-scale"
57
56
 
58
- export interface LineChartProps extends Node2DProps {
59
- /** One row per sample — a plain record of field name to value. */
60
- data: DataRecord[]
61
- /**
62
- * Key into each row for its x value, shared by every series. Named `xField`
63
- * rather than `x` because every node already has an `x` — its position.
64
- */
65
- xField: string
66
- /** One polyline per entry: which field it plots, its legend `label`, its `fill`. */
67
- series: LineSeries[]
68
- /** X-axis data domain: caption, scale range, and tick placement. */
69
- xAxis?: AxisDomainConfig
70
- /** Y-axis data domain: caption, scale range, and tick placement. */
71
- yAxis?: AxisDomainConfig
72
- /**
73
- * A slice of the plot to spotlight, in **data units**. Dashed rules mark the
74
- * bounded edges and every series fades outside the box — see
75
- * `spotlightRegion`, which animates the same thing. `null` (the default) is no
76
- * region.
77
- */
78
- region?: PlotRegion | null
79
- /**
80
- * The chart's visual theme — series palette, axis text/grid/gutters, plot
81
- * panel paint, and legend styling. Every branch is optional and deep-merged
82
- * onto {@link DEFAULT_CHART_THEME}, so pass only what you want to override.
83
- */
84
- theme?: ChartTheme
85
- /** Whether the legend row below the plot is drawn at all. */
86
- showLegend?: boolean
87
- }
88
-
89
57
  /** Both axes' domains with their ranges and ticks resolved. */
90
58
  export interface LineChartDomains {
91
- readonly x: AxisDomainConfig & { readonly range: Range; readonly ticks: number[] }
92
- readonly y: AxisDomainConfig & { readonly range: Range; readonly ticks: number[] }
59
+ readonly x: ResolvedAxisDomain
60
+ readonly y: ResolvedAxisDomain
93
61
  }
94
62
 
95
63
  /** What a line chart plots, as its entries hold it. */
@@ -101,12 +69,6 @@ export interface LineChartValues {
101
69
  readonly yAxis: AxisDomainConfig
102
70
  }
103
71
 
104
- /**
105
- * The argument every per-series command takes: an index into `series`. Its own
106
- * kind rather than `number` so a host can offer the series by name.
107
- */
108
- const SERIES_ARG = Types.custom<number, number, EntryOptions & { readonly min: number; readonly step: number }>(0, { kind: "series", min: 0, step: 1 })
109
-
110
72
  /** One argument, two controls: the commands take a `PlotRegion`, so the x and y spans a host draws are declared inside it. */
111
73
  const REGION_ARG = Types.custom<PlotRegion | undefined>(undefined, {
112
74
  kind: "region",
@@ -116,21 +78,6 @@ const REGION_ARG = Types.custom<PlotRegion | undefined>(undefined, {
116
78
  },
117
79
  })
118
80
 
119
- /** How far the legend starts below its slot when it enters, in px. */
120
- const LEGEND_SLIDE = 20
121
-
122
- /** Fallback marker count when neither axis's ticks are given. */
123
- const DEFAULT_MARKERS = 5
124
-
125
- /**
126
- * How `enter` divides its duration between its three beats: the axes arriving,
127
- * the lines tracing, and the legend coming up. Shares of the whole, so they must
128
- * sum to 1 — the entrance takes exactly the time it was given.
129
- */
130
- const AXES_SHARE = 0.35
131
- const LINES_SHARE = 0.5
132
- const LEGEND_SHARE = 0.15
133
-
134
81
  /** What the chart's own commands read: its entries, and the private `home` a zoom remembers. */
135
82
  interface LineState extends LineChartValues {
136
83
  readonly theme: ChartConfig
@@ -139,30 +86,19 @@ interface LineState extends LineChartValues {
139
86
  readonly home: LineChartDomains | null
140
87
  }
141
88
 
142
- /**
143
- * What the x column *is* — a number, an instant, or a place in the file — and
144
- * with it how each row is positioned, ticked and captioned. See `x-scale.ts`.
145
- */
89
+ /** What the x column is (a number, an instant, or a place in the file), and so how rows are positioned, ticked and captioned. */
146
90
  function columnOf(s: LineChartValues, format = s.xAxis.format): XScale {
147
91
  return resolveXScale(s.data, s.xField, format)
148
92
  }
149
93
 
150
- /**
151
- * The horizontal axis with its range and ticks resolved: the explicit ones, else
152
- * the column's extent and five ticks across it, asked of the column because only
153
- * a numeric one has round numbers between its samples.
154
- */
94
+ /** The explicit range and ticks, else the column's extent and ticks: only a numeric column has round numbers between its samples. */
155
95
  function drawnX(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
156
96
  const column = columnOf(s, domain.format)
157
97
  const range = domain.range ?? column.extent
158
98
  return { ...domain, range, ticks: domain.ticks ?? column.ticks(range, DEFAULT_MARKERS - 1) }
159
99
  }
160
100
 
161
- /**
162
- * The vertical axis with its range and ticks resolved. Auto-ranging steps into
163
- * one fewer segment than the intended marker count, so the tidy bounds frame the
164
- * ticks the axis draws.
165
- */
101
+ /** Auto-ranging steps into one fewer segment than the marker count, so the tidy bounds frame the ticks drawn. */
166
102
  function drawnY(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
167
103
  const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
168
104
  const range = resolveYScale(s.data, s.series, domain.range, divisions)
@@ -175,7 +111,7 @@ const domainsDrawn = new WeakMap<
175
111
  Pick<LineChartValues, "xField" | "xAxis" | "yAxis" | "series"> & { readonly domains: LineChartDomains }
176
112
  >()
177
113
 
178
- /** Both axes as a line chart draws them now, an auto range or tick row read back as itself. */
114
+ /** Both axes as drawn now. */
179
115
  export function lineChartDomains(s: LineChartValues): LineChartDomains {
180
116
  const { data, xField, xAxis, yAxis, series } = s
181
117
  const kept = domainsDrawn.get(data)
@@ -190,10 +126,7 @@ export function lineChartDomains(s: LineChartValues): LineChartDomains {
190
126
  return domains
191
127
  }
192
128
 
193
- /**
194
- * Map one series' data points into the plot's **centred** pixel space (origin at
195
- * the plot centre, y-up): x runs `[-w/2, +w/2]`, y runs `[-h/2, +h/2]`.
196
- */
129
+ /** One series' points in the plot's centred pixel space (origin at its centre, y-up). */
197
130
  function seriesPixels(s: LineChartValues, domains: LineChartDomains, index: number, plot: Size2D): Vector2[] {
198
131
  const series = s.series[index]
199
132
  if (!series) return []
@@ -204,46 +137,37 @@ function seriesPixels(s: LineChartValues, domains: LineChartDomains, index: numb
204
137
  const [yMin, yMax] = domains.y.range
205
138
  const xToPx = (x: number) => (xMax <= xMin ? 0 : ((x - xMin) / (xMax - xMin) - 0.5) * w)
206
139
  const yToPx = (y: number) => (yMax <= yMin ? 0 : ((y - yMin) / (yMax - yMin) - 0.5) * h)
207
- // `seriesPoints` already sorted them along x.
208
140
  return points.map(([x, y]) => ({ x: xToPx(x), y: yToPx(y) }))
209
141
  }
210
142
 
211
- /** `region` mapped into the plot's centred pixel space, against the live scales, so it travels with a zoom. */
143
+ /** `region` in centred pixel space against the live scales, so it travels with a zoom. */
212
144
  function regionBox(s: LineChartValues & { readonly region: PlotRegion | null }, domains: LineChartDomains, plot: Size2D): RegionBox {
213
145
  const { width: w, height: h } = plot
214
146
  return resolveRegionBox(s.region, { left: -w / 2, right: w / 2, bottom: -h / 2, top: h / 2 }, domains.x.range, domains.y.range)
215
147
  }
216
148
 
217
- /**
218
- * Whether a call-out is authored at all. Not whether it resolves to pixels: a
219
- * command is built before the first layout, and a region on a chart not yet
220
- * measured is a real one. A region bounding neither axis is none, as `null` is.
221
- */
149
+ /** Whether a call-out is authored, not whether it resolves to pixels: a command is built before the first layout. A region bounding neither axis is none. */
222
150
  function hasCallout(s: { readonly region: PlotRegion | null }): boolean {
223
151
  const region = s.region
224
152
  return region != null && (region.x !== undefined || region.y !== undefined)
225
153
  }
226
154
 
227
- /** The `region` a document handed a command, or `{}`: a command is added before its arguments are filled in. */
155
+ /** The `region` argument, or `{}`: a command is added before its arguments are filled in. */
228
156
  function regionArg(region: PlotRegion | null | undefined): PlotRegion {
229
157
  return region != null && typeof region === "object" ? region : {}
230
158
  }
231
159
 
232
- /** A range read as a span, whichever way round the author wrote it. */
160
+ /** A range ordered low to high. */
233
161
  function span([a, b]: Range): Range {
234
162
  return a <= b ? [a, b] : [b, a]
235
163
  }
236
164
 
237
- /**
238
- * Rescale an axis to `range`, keeping the tick *count* it already has, so each
239
- * marker slides from its old value to its new one instead of the row popping.
240
- */
165
+ /** Rescale to `range` keeping the tick count, so each marker slides rather than the row popping. */
241
166
  function rescale(range: Range, ticks: readonly number[]): AxisDomainConfig {
242
167
  const to = span(range)
243
168
  return { range: to, ticks: markerValues(to, Math.max(2, ticks.length) - 1) }
244
169
  }
245
170
 
246
- /** The axis writes of a move from `from` toward `to` at `t`. */
247
171
  function axesAt(from: LineChartDomains, to: Partial<Record<"x" | "y", AxisDomainConfig>>, t: number) {
248
172
  return {
249
173
  ...(to.x && { xAxis: lerpAxisDomain(from.x, to.x, t) }),
@@ -251,8 +175,6 @@ function axesAt(from: LineChartDomains, to: Partial<Record<"x" | "y", AxisDomain
251
175
  }
252
176
  }
253
177
 
254
- type AnyTimeline = Timeline<any, any>
255
-
256
178
  /** The parts a line chart's commands drive, found through its children. */
257
179
  function partsOf(children: readonly ChildView[]) {
258
180
  const frame = cartesianViews(children)
@@ -267,17 +189,15 @@ function partsOf(children: readonly ChildView[]) {
267
189
  }
268
190
  }
269
191
 
270
- /** Drop the call-out: the rules retract, the wash fades, and every series comes back to full. */
271
192
  function clearing(timeline: AnyTimeline, state: LineState) {
272
193
  const from = state.regionReveal
273
194
  const region = state.region
274
- // Nothing to retract, but the call still takes its slot on the timeline.
195
+ // Nothing to retract, but the call still takes its slot.
275
196
  if (from === 0) return timeline.animate(() => ({ region: null, regionReveal: 0 }), { eased: false })
276
197
  // `region` is held until the retraction lands, or there would be nothing left to retract.
277
198
  return timeline.animate((t) => (t >= 1 ? { region: null, regionReveal: 0 } : { region, regionReveal: from * (1 - t) }))
278
199
  }
279
200
 
280
- /** Trace series `index`'s line in (`show`) or back out, in lockstep with its legend entry. */
281
201
  function toggling(timeline: AnyTimeline, children: readonly ChildView[], index: number, show: boolean) {
282
202
  const parts = partsOf(children)
283
203
  const line = parts?.lines[index]
@@ -289,62 +209,35 @@ function toggling(timeline: AnyTimeline, children: readonly ChildView[], index:
289
209
  }
290
210
 
291
211
  /**
292
- * A multi-series line chart, composed from five independent parts rather than
293
- * one monolithic draw:
294
- *
295
- * - {@link ChartCanvas} — the plot panel with a two-way grid, under everything.
296
- * - {@link VerticalAxis} / {@link HorizontalAxis} — columns/rows of real text
297
- * markers that stagger in, each carrying its own axis caption.
298
- * - {@link ChartRegion} — the optional call-out marking a spotlit slice of the
299
- * plot: dashed rules on its bounded edges, and every series faded outside it.
300
- * - {@link ChartLine} (one per series) — the traced polylines over the canvas.
301
- * - {@link ChartLegend} — a row of entries below the chart, one per series.
212
+ * A multi-series line chart composed of {@link ChartCanvas}, {@link VerticalAxis} /
213
+ * {@link HorizontalAxis}, {@link ChartRegion}, one {@link ChartLine} per series and
214
+ * {@link ChartLegend}. Every part reads the chart's entries through bindings and
215
+ * maps against the plot panel's own laid-out box.
302
216
  *
303
- * Every part reads the chart's entries through bindings, and the lines, the
304
- * region and the grid all map against the plot panel's own laid-out box, so
305
- * points land exactly on their markers and the chart re-flows if its box
306
- * changes.
307
- *
308
- * **Resting state is the finished chart**: an editor paints a node's Initial
309
- * state on a paused canvas. `enter` arms every part on its first frame.
310
- *
311
- * Past the entrance, `showSeries`/`hideSeries`/`spotlightSeries` pick out a
312
- * *series*, `spotlightRegion` marks a *slice of the plot* without changing the
313
- * scale, and `zoomTo` rescales the axes so a slice fills the plot. A spotlit
314
- * region resolves against the live scales, so it travels with a zoom. A `to` of
315
- * an axis `range` or `ticks` tweens from the domain as drawn (see
316
- * {@link lerpAxisEntry}), so an auto axis rescales continuously.
217
+ * Resting state is the finished chart; `enter` arms every part on its first
218
+ * frame. A `to` of an axis `range` or `ticks` tweens from the domain as drawn
219
+ * (see {@link lerpAxisEntry}), so an auto axis rescales continuously.
317
220
  */
318
221
  export const LineChart = component2d({
319
222
  key: "lineChart",
320
223
  state: {
321
224
  data: fresh<DataRecord[]>(() => [], { kind: "dataset" }),
225
+ // Not `x`: every node already has an `x`, its position.
322
226
  xField: "",
323
227
  series: fresh<LineSeries[]>(() => [], { kind: "series" }),
324
228
  xAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
325
229
  yAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
326
230
  theme: Types.custom<ChartTheme, ChartConfig>(DEFAULT_CHART_THEME, { resolve: resolveChartTheme, lerp: lerpChartTheme }),
327
- /**
328
- * The spotlit slice of the plot in data units, or `null` for none. The box
329
- * snaps; the animation is {@link regionReveal} drawing the rules on.
330
- */
231
+ /** The spotlit slice in data units, or `null`. The box snaps; {@link regionReveal} animates it. */
331
232
  region: Types.custom<PlotRegion | null>(null),
332
- /**
333
- * How far the `region` call-out is drawn in, `[0, 1]`: the rules grow out and
334
- * the wash fades up as it climbs, while every line's out-of-region stretch
335
- * fades to the theme's `region.dimOpacity`.
336
- */
233
+ /** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade to `region.dimOpacity`. */
337
234
  regionReveal: 0,
338
235
  /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
339
236
  showLegend: Types.boolean(true, { animatable: false }),
340
- /**
341
- * The domain `zoomTo` first left, so `resetZoom` restores the chart as it
342
- * stood, auto-derived ticks included. Cleared by `resetZoom`, so the next zoom
343
- * remembers wherever the chart rests then.
344
- */
237
+ /** The domain `zoomTo` first left, so `resetZoom` restores it, auto-derived ticks included. */
345
238
  home: Types.custom<LineChartDomains | null>(null, { private: true }),
346
239
  },
347
- // Its parts fill it; a node made of them would hug them.
240
+ // A node made of parts that fill would otherwise hug them.
348
241
  defaults: { width: "fill", height: "fill" },
349
242
  allowedChildren: [],
350
243
  children: (state) => {
@@ -359,7 +252,6 @@ export const LineChart = component2d({
359
252
  const vAxis = new VerticalAxis({
360
253
  range: () => domains().y.range,
361
254
  markers: () => domains().y.ticks,
362
- // The y axis is always numbers, so its format is simply the pattern.
363
255
  captions: () => {
364
256
  const markers = domains().y.ticks
365
257
  const format = state.yAxis.format
@@ -374,8 +266,7 @@ export const LineChart = component2d({
374
266
  const hAxis = new HorizontalAxis({
375
267
  range: () => domains().x.range,
376
268
  markers: () => domains().x.ticks,
377
- // A tick on a month or category column is captioned with the cell it came
378
- // out of — `Jan`, not the instant it parsed to.
269
+ // A month or category tick is captioned with its cell (`Jan`), not the instant it parsed to.
379
270
  captions: () => {
380
271
  const column = columnOf(state)
381
272
  return domains().x.ticks.map((value) => column.label(value))
@@ -392,10 +283,8 @@ export const LineChart = component2d({
392
283
  points: () => seriesPixels(state, domains(), i, plotSize()),
393
284
  stroke: () => resolveSeriesStroke(theme().seriesStyles, s, i),
394
285
  enabled: enabled[i],
395
- // Each line clips itself against the box the rules are drawn from, so
396
- // the dim/bright boundary can't drift from the dashes.
286
+ // The box the rules are drawn from, so the dim boundary cannot drift from the dashes.
397
287
  spotlight: () => regionBox(state, domains(), plotSize()),
398
- // Ramped by the reveal that draws the rules: one signal, both halves.
399
288
  dimOpacity: () => 1 - (1 - theme().region.dimOpacity) * state.regionReveal,
400
289
  }) as unknown as Node2D
401
290
  )
@@ -405,12 +294,9 @@ export const LineChart = component2d({
405
294
  below.push(
406
295
  new ChartLegend({
407
296
  series: state.series,
408
- // The legend paints a swatch, not a line, so it is handed the colour each
409
- // stroke resolves to rather than the stroke itself.
410
297
  swatches: () => state.series.map((s, i) => resolveSeriesFill(theme().seriesStyles, s, i)),
411
298
  legend: () => theme().legend,
412
- // The row's spacing is the theme's as the chart was built, like the
413
- // entries' type and chips: a theme tween re-paints the swatches only.
299
+ // Spacing is fixed as built; a theme tween re-paints the swatches only.
414
300
  gap: state.theme.legend.itemGap,
415
301
  padding: state.theme.legend.padding,
416
302
  }) as unknown as Node2D
@@ -425,14 +311,12 @@ export const LineChart = component2d({
425
311
  shadow: () => theme().plotArea.shadow,
426
312
  xGridStroke: () => theme().xAxis.lineStyle,
427
313
  yGridStroke: () => theme().yAxis.lineStyle,
428
- // A grid line under every axis marker; the canvas skips the edges.
429
314
  xMarkers: () => domains().x.ticks,
430
315
  yMarkers: () => domains().y.ticks,
431
316
  xExtent: () => domains().x.range,
432
317
  yExtent: () => domains().y.range,
433
318
  }),
434
- // Over the grid, under the lines, so a spotlit stretch of a series isn't
435
- // tinted by the highlight meant to pick it out.
319
+ // Under the lines, so the highlight does not tint the stretch it picks out.
436
320
  new ChartRegion({
437
321
  box: () => regionBox(state, domains(), plotSize()),
438
322
  stroke: () => theme().region.stroke,
@@ -458,17 +342,13 @@ export const LineChart = component2d({
458
342
  },
459
343
  commands: {
460
344
  /**
461
- * Animate the whole chart in: the two axes stagger their markers in, the
462
- * series lines trace left to right, and the legend fades up last. Its first
463
- * frame arms every part: markers shifted and transparent, lines untraced,
464
- * the legend dropped and transparent.
465
- *
466
- * `duration` is the **whole** entrance; the beats are shares of it. A chart
467
- * authored with a `region` lands its call-out last, in the final quarter.
345
+ * Axes stagger in, lines trace left to right, the legend fades up last.
346
+ * `duration` is the whole entrance; a chart with a `region` lands its
347
+ * call-out in the final quarter.
468
348
  */
469
349
  enter: {
470
350
  args: {},
471
- // Held from the chart's arrival, so it never shows finished ahead of its draw.
351
+ // Held from arrival, so it never shows finished ahead of its draw.
472
352
  entrance: true,
473
353
  easing: easeOut(),
474
354
  run: (timeline, state, children) => {
@@ -477,8 +357,8 @@ export const LineChart = component2d({
477
357
  const duration = timeline.duration
478
358
  const revealing = hasCallout(state) && state.regionReveal === 0
479
359
  const body = revealing ? duration * 0.75 : duration
480
- const axes = body * AXES_SHARE
481
- const traced = body * LINES_SHARE
360
+ const axes = body * ENTER_SHARE.axes
361
+ const traced = body * ENTER_SHARE.marks
482
362
  const { legend } = parts
483
363
  const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
484
364
  // The first frame arms what every later beat moves: a beat ahead of the
@@ -490,17 +370,15 @@ export const LineChart = component2d({
490
370
  { at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
491
371
  { at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) },
492
372
  ...parts.lines.map((line) => ({ at: axes, duration: traced, piece: line.run("enter") })),
493
- ...(legend ? [{ at: axes + traced, duration: body * LEGEND_SHARE, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
373
+ ...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
494
374
  ...(revealing ? [{ at: body, duration: duration * 0.25, piece: timeline.animate((t) => ({ regionReveal: t })) }] : []),
495
375
  ])
496
376
  },
497
377
  },
498
378
  /**
499
- * Spotlight a rectangular slice of the plot given in **data units**: dashed
500
- * rules draw onto each bounded edge while every series fades outside the box.
501
- * Either axis alone is a band; both are a box. Called while another region is
502
- * up, the old call-out retracts over the first half and the new one draws in
503
- * over the second, each half eased on its own.
379
+ * Spotlight a slice of the plot in data units; either axis alone is a band.
380
+ * Over a region already up, the old one retracts in the first half and the
381
+ * new one draws in over the second, each half eased on its own.
504
382
  */
505
383
  spotlightRegion: {
506
384
  args: { region: REGION_ARG },
@@ -521,18 +399,13 @@ export const LineChart = component2d({
521
399
  )
522
400
  },
523
401
  },
524
- /** Drop the region call-out. A no-op when nothing is spotlit, which still takes its slot. */
402
+ /** Drop the call-out. A no-op when nothing is spotlit, which still takes its slot. */
525
403
  clearRegion: {
526
404
  args: {},
527
405
  easing: easeOut(),
528
406
  run: (timeline, state) => clearing(timeline, state),
529
407
  },
530
- /**
531
- * Zoom the plot to a slice of itself, given in **data units**: the named axes
532
- * rescale so that slice fills the plot, the grid, both marker rows and every
533
- * series following. An omitted axis is left as it is. The first zoom
534
- * remembers the domain it left, for `resetZoom`.
535
- */
408
+ /** Rescale the named axes so a slice (data units) fills the plot; an omitted axis is left. The first zoom remembers the domain for `resetZoom`. */
536
409
  zoomTo: {
537
410
  args: { region: REGION_ARG },
538
411
  easing: easeOut(),
@@ -548,7 +421,7 @@ export const LineChart = component2d({
548
421
  return timeline.animate((t) => ({ home, ...axesAt(from, to, t) }))
549
422
  },
550
423
  },
551
- /** Pull back out to the domain the first zoom left — the mirror of `zoomTo`, and a hold if it never zoomed. */
424
+ /** Back to the domain the first zoom left; a hold if it never zoomed. */
552
425
  resetZoom: {
553
426
  args: {},
554
427
  easing: easeOut(),
@@ -559,26 +432,19 @@ export const LineChart = component2d({
559
432
  return timeline.animate((t) => ({ home: null, ...axesAt(from, home, t) }))
560
433
  },
561
434
  },
562
- /**
563
- * Show series `index`: **trace** its line in left to right, in lockstep with
564
- * its legend entry growing in.
565
- */
435
+ /** Trace series `index` in, with its legend entry growing in. */
566
436
  showSeries: {
567
437
  args: { seriesIndex: SERIES_ARG },
568
438
  easing: easeOut(),
569
439
  run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, true),
570
440
  },
571
- /** Hide series `index`: **retrace** its line back out as its legend entry shrinks away. */
441
+ /** Retrace series `index` out as its legend entry shrinks away. */
572
442
  hideSeries: {
573
443
  args: { seriesIndex: SERIES_ARG },
574
444
  easing: easeOut(),
575
445
  run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, false),
576
446
  },
577
- /**
578
- * Fade every other series — each line **and its legend entry** — to focus on
579
- * `index`, which comes back to full. The legend dims in place, so the row
580
- * doesn't reflow.
581
- */
447
+ /** Fade every other series, line and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
582
448
  spotlightSeries: {
583
449
  args: { seriesIndex: SERIES_ARG },
584
450
  easing: easeOut(),