@motionscript/charts 0.1.1-alpha.0 → 0.1.1-alpha.2

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 (207) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +109 -28
  3. package/dist/bar-chart/bar-chart.d.ts +265 -39
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +219 -171
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +12 -7
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/category-axis.js +56 -42
  10. package/dist/bar-chart/category-axis.js.map +1 -1
  11. package/dist/bar-chart/chart-bars.d.ts +76 -52
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +105 -89
  14. package/dist/bar-chart/chart-bars.js.map +1 -1
  15. package/dist/bar-chart/index.d.ts +4 -4
  16. package/dist/bar-chart/index.d.ts.map +1 -1
  17. package/dist/bar-chart/index.js +2 -2
  18. package/dist/bar-chart/index.js.map +1 -1
  19. package/dist/bar-chart/shared.d.ts +41 -65
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +73 -117
  22. package/dist/bar-chart/shared.js.map +1 -1
  23. package/dist/browser/chunks/chunk-7XCRX6GE.js +2 -0
  24. package/dist/browser/chunks/chunk-7XCRX6GE.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 +5 -2
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +5 -2
  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 +51 -15
  38. package/dist/kit/cartesian.d.ts.map +1 -1
  39. package/dist/kit/cartesian.js +81 -29
  40. package/dist/kit/cartesian.js.map +1 -1
  41. package/dist/kit/chart-canvas.d.ts +32 -24
  42. package/dist/kit/chart-canvas.d.ts.map +1 -1
  43. package/dist/kit/chart-canvas.js +52 -46
  44. package/dist/kit/chart-canvas.js.map +1 -1
  45. package/dist/kit/chart-legend.d.ts +8 -4
  46. package/dist/kit/chart-legend.d.ts.map +1 -1
  47. package/dist/kit/chart-legend.js +30 -20
  48. package/dist/kit/chart-legend.js.map +1 -1
  49. package/dist/{line-chart → kit}/horizontal-axis.d.ts +31 -6
  50. package/dist/kit/horizontal-axis.d.ts.map +1 -0
  51. package/dist/{line-chart → kit}/horizontal-axis.js +23 -24
  52. package/dist/kit/horizontal-axis.js.map +1 -0
  53. package/dist/kit/motion.d.ts +5 -0
  54. package/dist/kit/motion.d.ts.map +1 -1
  55. package/dist/kit/motion.js +8 -0
  56. package/dist/kit/motion.js.map +1 -1
  57. package/dist/{pie-chart → kit}/pie-legend.d.ts +3 -3
  58. package/dist/kit/pie-legend.d.ts.map +1 -0
  59. package/dist/{pie-chart → kit}/pie-legend.js +11 -10
  60. package/dist/kit/pie-legend.js.map +1 -0
  61. package/dist/kit/polar.d.ts +31 -0
  62. package/dist/kit/polar.d.ts.map +1 -0
  63. package/dist/kit/polar.js +33 -0
  64. package/dist/kit/polar.js.map +1 -0
  65. package/dist/kit/shared.d.ts +11 -50
  66. package/dist/kit/shared.d.ts.map +1 -1
  67. package/dist/kit/shared.js +58 -141
  68. package/dist/kit/shared.js.map +1 -1
  69. package/dist/kit/style.d.ts +517 -0
  70. package/dist/kit/style.d.ts.map +1 -0
  71. package/dist/kit/style.js +292 -0
  72. package/dist/kit/style.js.map +1 -0
  73. package/dist/kit/vertical-axis.d.ts +22 -2
  74. package/dist/kit/vertical-axis.d.ts.map +1 -1
  75. package/dist/kit/vertical-axis.js +56 -43
  76. package/dist/kit/vertical-axis.js.map +1 -1
  77. package/dist/line-chart/chart-line.d.ts +95 -3
  78. package/dist/line-chart/chart-line.d.ts.map +1 -1
  79. package/dist/line-chart/chart-line.js +165 -53
  80. package/dist/line-chart/chart-line.js.map +1 -1
  81. package/dist/line-chart/chart-region.d.ts +1 -1
  82. package/dist/line-chart/chart-region.d.ts.map +1 -1
  83. package/dist/line-chart/chart-region.js +7 -8
  84. package/dist/line-chart/chart-region.js.map +1 -1
  85. package/dist/line-chart/index.d.ts +5 -6
  86. package/dist/line-chart/index.d.ts.map +1 -1
  87. package/dist/line-chart/index.js +3 -4
  88. package/dist/line-chart/index.js.map +1 -1
  89. package/dist/line-chart/line-chart.d.ts +281 -18
  90. package/dist/line-chart/line-chart.d.ts.map +1 -1
  91. package/dist/line-chart/line-chart.js +284 -134
  92. package/dist/line-chart/line-chart.js.map +1 -1
  93. package/dist/line-chart/shared.d.ts +51 -34
  94. package/dist/line-chart/shared.d.ts.map +1 -1
  95. package/dist/line-chart/shared.js +175 -83
  96. package/dist/line-chart/shared.js.map +1 -1
  97. package/dist/line-chart/x-scale.d.ts +2 -0
  98. package/dist/line-chart/x-scale.d.ts.map +1 -1
  99. package/dist/line-chart/x-scale.js +15 -0
  100. package/dist/line-chart/x-scale.js.map +1 -1
  101. package/dist/nodes.d.ts +3 -1
  102. package/dist/nodes.d.ts.map +1 -1
  103. package/dist/nodes.js +9 -1
  104. package/dist/nodes.js.map +1 -1
  105. package/dist/pie-chart/index.d.ts +2 -3
  106. package/dist/pie-chart/index.d.ts.map +1 -1
  107. package/dist/pie-chart/index.js +1 -2
  108. package/dist/pie-chart/index.js.map +1 -1
  109. package/dist/pie-chart/pie-chart.d.ts +114 -18
  110. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  111. package/dist/pie-chart/pie-chart.js +38 -24
  112. package/dist/pie-chart/pie-chart.js.map +1 -1
  113. package/dist/pie-chart/pie-ring.d.ts +16 -5
  114. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  115. package/dist/pie-chart/pie-ring.js +89 -55
  116. package/dist/pie-chart/pie-ring.js.map +1 -1
  117. package/dist/pie-chart/shared.d.ts +61 -62
  118. package/dist/pie-chart/shared.d.ts.map +1 -1
  119. package/dist/pie-chart/shared.js +44 -123
  120. package/dist/pie-chart/shared.js.map +1 -1
  121. package/dist/radar-chart/index.d.ts +5 -0
  122. package/dist/radar-chart/index.d.ts.map +1 -0
  123. package/dist/radar-chart/index.js +5 -0
  124. package/dist/radar-chart/index.js.map +1 -0
  125. package/dist/radar-chart/radar-chart.d.ts +212 -0
  126. package/dist/radar-chart/radar-chart.d.ts.map +1 -0
  127. package/dist/radar-chart/radar-chart.js +224 -0
  128. package/dist/radar-chart/radar-chart.js.map +1 -0
  129. package/dist/radar-chart/radar-grid.d.ts +49 -0
  130. package/dist/radar-chart/radar-grid.d.ts.map +1 -0
  131. package/dist/radar-chart/radar-grid.js +63 -0
  132. package/dist/radar-chart/radar-grid.js.map +1 -0
  133. package/dist/radar-chart/radar-series.d.ts +59 -0
  134. package/dist/radar-chart/radar-series.d.ts.map +1 -0
  135. package/dist/radar-chart/radar-series.js +71 -0
  136. package/dist/radar-chart/radar-series.js.map +1 -0
  137. package/dist/radar-chart/shared.d.ts +78 -0
  138. package/dist/radar-chart/shared.d.ts.map +1 -0
  139. package/dist/radar-chart/shared.js +129 -0
  140. package/dist/radar-chart/shared.js.map +1 -0
  141. package/dist/radial-chart/index.d.ts +6 -0
  142. package/dist/radial-chart/index.d.ts.map +1 -0
  143. package/dist/radial-chart/index.js +4 -0
  144. package/dist/radial-chart/index.js.map +1 -0
  145. package/dist/radial-chart/radial-chart.d.ts +210 -0
  146. package/dist/radial-chart/radial-chart.d.ts.map +1 -0
  147. package/dist/radial-chart/radial-chart.js +175 -0
  148. package/dist/radial-chart/radial-chart.js.map +1 -0
  149. package/dist/radial-chart/radial-rings.d.ts +121 -0
  150. package/dist/radial-chart/radial-rings.d.ts.map +1 -0
  151. package/dist/radial-chart/radial-rings.js +273 -0
  152. package/dist/radial-chart/radial-rings.js.map +1 -0
  153. package/dist/radial-chart/shared.d.ts +77 -0
  154. package/dist/radial-chart/shared.d.ts.map +1 -0
  155. package/dist/radial-chart/shared.js +78 -0
  156. package/dist/radial-chart/shared.js.map +1 -0
  157. package/package.json +4 -4
  158. package/registry.json +21 -6
  159. package/src/bar-chart/bar-chart.ts +263 -179
  160. package/src/bar-chart/category-axis.ts +46 -40
  161. package/src/bar-chart/chart-bars.ts +136 -117
  162. package/src/bar-chart/index.ts +7 -10
  163. package/src/bar-chart/shared.ts +88 -233
  164. package/src/engine.ts +5 -2
  165. package/src/index.ts +2 -0
  166. package/src/kit/cartesian.ts +121 -40
  167. package/src/kit/chart-canvas.ts +49 -54
  168. package/src/kit/chart-legend.ts +28 -20
  169. package/src/kit/horizontal-axis.ts +114 -0
  170. package/src/kit/motion.ts +7 -0
  171. package/src/{pie-chart → kit}/pie-legend.ts +12 -11
  172. package/src/kit/polar.ts +54 -0
  173. package/src/kit/shared.ts +55 -277
  174. package/src/kit/style.ts +353 -0
  175. package/src/kit/vertical-axis.ts +67 -48
  176. package/src/line-chart/chart-line.ts +176 -54
  177. package/src/line-chart/chart-region.ts +6 -10
  178. package/src/line-chart/index.ts +14 -10
  179. package/src/line-chart/line-chart.ts +353 -151
  180. package/src/line-chart/shared.ts +193 -174
  181. package/src/line-chart/x-scale.ts +13 -0
  182. package/src/nodes.ts +9 -1
  183. package/src/pie-chart/index.ts +2 -23
  184. package/src/pie-chart/pie-chart.ts +54 -35
  185. package/src/pie-chart/pie-ring.ts +92 -71
  186. package/src/pie-chart/shared.ts +80 -270
  187. package/src/radar-chart/index.ts +4 -0
  188. package/src/radar-chart/radar-chart.ts +290 -0
  189. package/src/radar-chart/radar-grid.ts +83 -0
  190. package/src/radar-chart/radar-series.ts +88 -0
  191. package/src/radar-chart/shared.ts +153 -0
  192. package/src/radial-chart/index.ts +5 -0
  193. package/src/radial-chart/radial-chart.ts +212 -0
  194. package/src/radial-chart/radial-rings.ts +328 -0
  195. package/src/radial-chart/shared.ts +126 -0
  196. package/dist/browser/chunks/chunk-4SMRCU24.js +0 -2
  197. package/dist/browser/chunks/chunk-4SMRCU24.js.map +0 -7
  198. package/dist/kit/chart-border.d.ts +0 -18
  199. package/dist/kit/chart-border.d.ts.map +0 -1
  200. package/dist/kit/chart-border.js +0 -23
  201. package/dist/kit/chart-border.js.map +0 -1
  202. package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
  203. package/dist/line-chart/horizontal-axis.js.map +0 -1
  204. package/dist/pie-chart/pie-legend.d.ts.map +0 -1
  205. package/dist/pie-chart/pie-legend.js.map +0 -1
  206. package/src/kit/chart-border.ts +0 -36
  207. package/src/line-chart/horizontal-axis.ts +0 -122
@@ -1,26 +1,35 @@
1
1
  import {
2
2
  Rect,
3
+ Text,
3
4
  Types,
5
+ clamp,
4
6
  component2d,
5
7
  easeOut,
6
8
  type ChildView,
7
9
  type DataRecord,
8
10
  type NodeOf,
11
+ type OutOf,
9
12
  // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
10
13
  type Node2D,
11
14
  type Node2DProps as _Node2DProps,
15
+ type TextStyle as _TextStyle,
16
+ type Fill as _Fill,
17
+ type FillResolved as _FillResolved,
18
+ type Stroke as _Stroke,
19
+ type StrokeResolved as _StrokeResolved,
12
20
  type Size2D,
21
+ type StyleInput as _StyleInput,
22
+ type StyleValues as _StyleValues,
13
23
  type Vector2,
14
24
  } from "@motionscript/sdk"
15
25
  import { formatNumber } from "@motionscript/sdk/component"
16
26
 
17
- import { cartesianFrame, cartesianViews, laidOutSize } from "../kit/cartesian"
18
- import { ChartBorder } from "../kit/chart-border"
27
+ import { boundText, cartesianFrame, chartViews, columnGhost, fade, frameGap, headroom, laidOutSize, overhang } from "../kit/cartesian"
19
28
  import { ChartCanvas } from "../kit/chart-canvas"
20
29
  import { ChartLegend } from "../kit/chart-legend"
21
30
  import { SERIES_ARG, asParts, fresh } from "../kit/entries"
22
- import { DEFAULT_MARKERS, ENTER_SHARE, LEGEND_SLIDE, beats, type AnyTimeline, type Beat } from "../kit/motion"
23
- import { VerticalAxis } from "../kit/vertical-axis"
31
+ import { HorizontalAxis } from "../kit/horizontal-axis"
32
+ import { DEFAULT_MARKERS, ENTER_SHARE, beats, legendAway, type AnyTimeline, type Beat } from "../kit/motion"
24
33
  import {
25
34
  SPOTLIGHT_DIM,
26
35
  formatMarker,
@@ -28,32 +37,55 @@ import {
28
37
  lerpAxisEntry,
29
38
  markerStep,
30
39
  markerValues,
40
+ niceRange,
41
+ niceTicks,
31
42
  resolveAxisDomain,
32
- resolveSeriesFill,
33
- resolveSeriesStroke,
34
43
  settleAxisDomain,
35
44
  type AxisDomainConfig,
36
45
  type Range,
37
46
  type ResolvedAxisDomain,
38
47
  } from "../kit/shared"
39
- import { ChartLine } from "./chart-line"
48
+ import {
49
+ CARTESIAN_GRID,
50
+ CARTESIAN_TEXT,
51
+ CHART_LEGEND,
52
+ CHART_SERIES,
53
+ CHART_STYLE_FAMILY,
54
+ CHART_STYLE_PRESETS,
55
+ GRID_BACKGROUND,
56
+ axisConfigOf,
57
+ legendConfigOf,
58
+ migrateChartStyle,
59
+ seriesFill,
60
+ sizeOf,
61
+ valueText,
62
+ } from "../kit/style"
63
+ import { VerticalAxis, axisPads } from "../kit/vertical-axis"
64
+ import { ChartArea, ChartLine, traceX } from "./chart-line"
40
65
  import { ChartRegion } from "./chart-region"
41
- import { HorizontalAxis } from "./horizontal-axis"
42
66
  import {
43
- DEFAULT_CHART_THEME,
44
- lerpChartTheme,
45
- resolveChartTheme,
67
+ LINE_LABELS,
68
+ LINE_MARKS,
46
69
  resolveRegionBox,
47
- resolveYScale,
48
- seriesPoints,
49
- type ChartConfig,
50
- type ChartTheme,
70
+ stackExtent,
71
+ stackSeries,
51
72
  type LineSeries,
73
+ type LineStack,
52
74
  type PlotRegion,
53
75
  type RegionBox,
76
+ type StackedSeries,
54
77
  } from "./shared"
55
78
  import { resolveXScale, type XScale } from "./x-scale"
56
79
 
80
+ const LINE_STYLE = Types.style(
81
+ CHART_STYLE_FAMILY,
82
+ { series: CHART_SERIES, background: GRID_BACKGROUND, text: CARTESIAN_TEXT, grid: CARTESIAN_GRID, marks: LINE_MARKS, labels: LINE_LABELS, legend: CHART_LEGEND },
83
+ { presets: CHART_STYLE_PRESETS }
84
+ )
85
+
86
+ /** A line chart's resolved style. */
87
+ export type LineChartStyle = OutOf<typeof LINE_STYLE>
88
+
57
89
  /** Both axes' domains with their ranges and ticks resolved. */
58
90
  export interface LineChartDomains {
59
91
  readonly x: ResolvedAxisDomain
@@ -65,8 +97,13 @@ export interface LineChartValues {
65
97
  readonly data: DataRecord[]
66
98
  readonly xField: string
67
99
  readonly series: LineSeries[]
100
+ readonly stack: LineStack
68
101
  readonly xAxis: AxisDomainConfig
69
102
  readonly yAxis: AxisDomainConfig
103
+ /** Each series' share of a stack, `1` when missing. */
104
+ readonly weights?: readonly number[]
105
+ /** An area must reach its baseline, so a chart with areas keeps zero on the scale. */
106
+ readonly style?: { readonly area: string }
70
107
  }
71
108
 
72
109
  /** One argument, two controls: the commands take a `PlotRegion`, so the x and y spans a host draws are declared inside it. */
@@ -78,19 +115,28 @@ const REGION_ARG = Types.custom<PlotRegion | undefined>(undefined, {
78
115
  },
79
116
  })
80
117
 
81
- /** What the chart's own commands read: its entries, and the private `home` a zoom remembers. */
118
+ /** What the chart's own commands read: its entries, and the private state they write. */
82
119
  interface LineState extends LineChartValues {
83
- readonly theme: ChartConfig
120
+ readonly style: LineChartStyle
84
121
  readonly region: PlotRegion | null
85
122
  readonly regionReveal: number
86
123
  readonly home: LineChartDomains | null
124
+ readonly weights: number[]
87
125
  }
88
126
 
127
+ /** The share of the y axis a percent stack reads to. */
128
+ const PERCENT: Range = [0, 100]
129
+
89
130
  /** What the x column is (a number, an instant, or a place in the file), and so how rows are positioned, ticked and captioned. */
90
131
  function columnOf(s: LineChartValues, format = s.xAxis.format): XScale {
91
132
  return resolveXScale(s.data, s.xField, format)
92
133
  }
93
134
 
135
+ /** Every series as drawn; `weighted` false reads each at full share, as the scale does so the ticks hold still while a series sinks out. */
136
+ function stacksOf(s: LineChartValues, weighted = true): StackedSeries[] {
137
+ return stackSeries(s.data, columnOf(s), s.series, s.stack, weighted ? s.weights : undefined)
138
+ }
139
+
94
140
  /** The explicit range and ticks, else the column's extent and ticks: only a numeric column has round numbers between its samples. */
95
141
  function drawnX(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
96
142
  const column = columnOf(s, domain.format)
@@ -98,46 +144,83 @@ function drawnX(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig
98
144
  return { ...domain, range, ticks: domain.ticks ?? column.ticks(range, DEFAULT_MARKERS - 1) }
99
145
  }
100
146
 
147
+ /** The y range the data asks for: a percent stack reads to 100; areas and stacks keep zero on the scale. */
148
+ function autoY(s: LineChartValues, divisions: number): Range {
149
+ const [lo, hi] = stackExtent(stacksOf(s, false))
150
+ if (s.stack === "percent" && lo >= 0) return PERCENT
151
+ const zero = s.stack !== "none" || (s.style?.area ?? "none") !== "none"
152
+ return niceRange(zero ? [Math.min(0, lo), Math.max(0, hi)] : [lo, hi], divisions)
153
+ }
154
+
101
155
  /** Auto-ranging steps into one fewer segment than the marker count, so the tidy bounds frame the ticks drawn. */
102
156
  function drawnY(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
103
157
  const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
104
- const range = resolveYScale(s.data, s.series, domain.range, divisions)
105
- return { ...domain, range, ticks: domain.ticks ?? markerValues(range, DEFAULT_MARKERS - 1) }
158
+ const range = domain.range ?? autoY(s, divisions)
159
+ const even = domain.range !== undefined || (s.stack === "percent" && range === PERCENT)
160
+ return { ...domain, range, ticks: domain.ticks ?? (even ? markerValues(range, DEFAULT_MARKERS - 1) : niceTicks(range, DEFAULT_MARKERS - 1)) }
106
161
  }
107
162
 
108
163
  /** The last domains worked out from each data array, and what else they were read from. */
109
- const domainsDrawn = new WeakMap<
110
- DataRecord[],
111
- Pick<LineChartValues, "xField" | "xAxis" | "yAxis" | "series"> & { readonly domains: LineChartDomains }
112
- >()
164
+ const domainsDrawn = new WeakMap<DataRecord[], { readonly key: readonly unknown[]; readonly domains: LineChartDomains }>()
113
165
 
114
166
  /** Both axes as drawn now. */
115
167
  export function lineChartDomains(s: LineChartValues): LineChartDomains {
116
- const { data, xField, xAxis, yAxis, series } = s
117
- const kept = domainsDrawn.get(data)
118
- if (kept && kept.xField === xField && kept.xAxis === xAxis && kept.yAxis === yAxis && kept.series === series) {
119
- return kept.domains
120
- }
168
+ const key = [s.xField, s.xAxis, s.yAxis, s.series, s.stack, s.style?.area]
169
+ const kept = domainsDrawn.get(s.data)
170
+ if (kept && kept.key.every((value, i) => value === key[i])) return kept.domains
121
171
  const domains = {
122
- x: settleAxisDomain(xAxis, (d) => drawnX(s, d)) as LineChartDomains["x"],
123
- y: settleAxisDomain(yAxis, (d) => drawnY(s, d)) as LineChartDomains["y"],
172
+ x: settleAxisDomain(s.xAxis, (d) => drawnX(s, d)) as LineChartDomains["x"],
173
+ y: settleAxisDomain(s.yAxis, (d) => drawnY(s, d)) as LineChartDomains["y"],
124
174
  }
125
- domainsDrawn.set(data, { xField, xAxis, yAxis, series, domains })
175
+ domainsDrawn.set(s.data, { key, domains })
126
176
  return domains
127
177
  }
128
178
 
129
- /** One series' points in the plot's centred pixel space (origin at its centre, y-up). */
130
- function seriesPixels(s: LineChartValues, domains: LineChartDomains, index: number, plot: Size2D): Vector2[] {
131
- const series = s.series[index]
132
- if (!series) return []
133
- const points = seriesPoints(s.data, columnOf(s), series)
134
- if (points.length === 0) return []
135
- const { width: w, height: h } = plot
179
+ /** Data to the plot's centred pixel space (origin at its centre, y-up). */
180
+ function mapper(domains: LineChartDomains, plot: Size2D) {
136
181
  const [xMin, xMax] = domains.x.range
137
182
  const [yMin, yMax] = domains.y.range
138
- const xToPx = (x: number) => (xMax <= xMin ? 0 : ((x - xMin) / (xMax - xMin) - 0.5) * w)
139
- const yToPx = (y: number) => (yMax <= yMin ? 0 : ((y - yMin) / (yMax - yMin) - 0.5) * h)
140
- return points.map(([x, y]) => ({ x: xToPx(x), y: yToPx(y) }))
183
+ return {
184
+ x: (x: number) => (xMax <= xMin ? 0 : ((x - xMin) / (xMax - xMin) - 0.5) * plot.width),
185
+ y: (y: number) => (yMax === yMin ? 0 : ((y - yMin) / (yMax - yMin) - 0.5) * plot.height),
186
+ }
187
+ }
188
+
189
+ interface SeriesPixels {
190
+ readonly points: Vector2[]
191
+ /** The line under the series in a stack; empty when it fills to the baseline. */
192
+ readonly base: Vector2[]
193
+ /** What each point reads: its own value, or its share in a percent stack. */
194
+ readonly values: number[]
195
+ }
196
+
197
+ const NO_PIXELS: SeriesPixels = { points: [], base: [], values: [] }
198
+
199
+ /** Series `index` in the plot's centred pixel space. */
200
+ function seriesPixels(s: LineChartValues, domains: LineChartDomains, index: number, plot: Size2D): SeriesPixels {
201
+ const stack = stacksOf(s)[index]
202
+ if (!stack) return NO_PIXELS
203
+ const map = mapper(domains, plot)
204
+ const toPx = ([x, y]: [number, number]): Vector2 => ({ x: map.x(x), y: map.y(y) })
205
+ return {
206
+ points: stack.points.map(toPx),
207
+ base: stack.base ? stack.base.map(toPx) : [],
208
+ values: stack.points.map(([, y], k) => y - (stack.base?.[k]?.[1] ?? 0)),
209
+ }
210
+ }
211
+
212
+ /** Where the areas fill down to: zero, held inside the scale. */
213
+ function baselineValue(domains: LineChartDomains): number {
214
+ const [lo, hi] = domains.y.range
215
+ return clamp(0, Math.min(lo, hi), Math.max(lo, hi))
216
+ }
217
+
218
+ /** Whether anything plotted runs past the scale, so the plot clips it: a zoom, or an author's range narrower than the data. */
219
+ function overflows(s: LineChartValues, domains: LineChartDomains): boolean {
220
+ const [ylo, yhi] = stackExtent(stacksOf(s, false))
221
+ const [xlo, xhi] = columnOf(s).extent
222
+ const within = (v: number, [a, b]: Range) => v >= Math.min(a, b) - 1e-9 && v <= Math.max(a, b) + 1e-9
223
+ return !(within(ylo, domains.y.range) && within(yhi, domains.y.range) && within(xlo, domains.x.range) && within(xhi, domains.x.range))
141
224
  }
142
225
 
143
226
  /** `region` in centred pixel space against the live scales, so it travels with a zoom. */
@@ -175,17 +258,26 @@ function axesAt(from: LineChartDomains, to: Partial<Record<"x" | "y", AxisDomain
175
258
  }
176
259
  }
177
260
 
261
+ /** A value as the y axis writes it: the author's pattern, a share in a percent stack, else the bare reading at the ticks' precision. */
262
+ function yLabel(s: LineChartValues, value: number, ticks: readonly number[]): string {
263
+ const format = s.yAxis.format
264
+ if (format !== undefined) return formatNumber(value, format)
265
+ const text = formatMarker(value, markerStep(ticks))
266
+ return s.stack === "percent" ? `${text}%` : text
267
+ }
268
+
269
+ function weightOf(s: { readonly weights: readonly number[] }, index: number): number {
270
+ return Math.max(0, s.weights[index] ?? 1)
271
+ }
272
+
178
273
  /** The parts a line chart's commands drive, found through its children. */
179
274
  function partsOf(children: readonly ChildView[]) {
180
- const frame = cartesianViews(children)
181
- if (!frame) return null
182
- const hAxis = frame.below.find(HorizontalAxis.is)
183
- if (!VerticalAxis.is(frame.axis) || !hAxis) return null
275
+ const views = chartViews(children)
184
276
  return {
185
- vAxis: frame.axis,
186
- hAxis,
187
- lines: frame.plot.filter(ChartLine.is),
188
- legend: frame.below.find(ChartLegend.is) ?? null,
277
+ vAxis: views.find(VerticalAxis.is) ?? null,
278
+ hAxis: views.find(HorizontalAxis.is) ?? null,
279
+ lines: views.filter(ChartLine.is),
280
+ legend: views.find(ChartLegend.is) ?? null,
189
281
  }
190
282
  }
191
283
 
@@ -198,21 +290,38 @@ function clearing(timeline: AnyTimeline, state: LineState) {
198
290
  return timeline.animate((t) => (t >= 1 ? { region: null, regionReveal: 0 } : { region, regionReveal: from * (1 - t) }))
199
291
  }
200
292
 
201
- function toggling(timeline: AnyTimeline, children: readonly ChildView[], index: number, show: boolean) {
293
+ /**
294
+ * Show or hide series `index` with its legend entry. Apart, the line retraces;
295
+ * in a stack it sinks into the series below as its share falls to nothing, so
296
+ * the ones above settle rather than leaving a gap.
297
+ */
298
+ function toggling(timeline: AnyTimeline, state: LineState, children: readonly ChildView[], index: number, show: boolean) {
202
299
  const parts = partsOf(children)
203
- const line = parts?.lines[index]
204
- if (!parts || !line) return timeline.wait(timeline.duration)
300
+ const line = parts.lines[index]
301
+ if (!line) return timeline.wait(timeline.duration)
302
+ const legend = parts.legend ? [parts.legend.run(show ? "showSeries" : "hideSeries", { index })] : []
303
+ if (state.stack === "none") {
304
+ return timeline.together([line.run("enter", show ? { from: 0, to: 1 } : { from: 1, to: 0 }), ...legend])
305
+ }
306
+ const from = state.series.map((_, i) => weightOf(state, i))
307
+ const start = from[index] ?? 1
308
+ const target = show ? 1 : 0
205
309
  return timeline.together([
206
- line.run("enter", show ? { from: 0, to: 1 } : { from: 1, to: 0 }),
207
- ...(parts.legend ? [parts.legend.run(show ? "showSeries" : "hideSeries", { index })] : []),
310
+ timeline.animate((t) => {
311
+ const weights = [...from]
312
+ weights[index] = start + (target - start) * timeline.ease(t)
313
+ return { weights }
314
+ }, { eased: false }),
315
+ line.run("hold", { shown: show }),
316
+ ...legend,
208
317
  ])
209
318
  }
210
319
 
211
320
  /**
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.
321
+ * A multi-series line or area chart composed of {@link ChartCanvas}, the value
322
+ * and x axes, {@link ChartRegion}, a {@link ChartArea} and a {@link ChartLine}
323
+ * per series, and {@link ChartLegend}. Every part reads the chart's entries
324
+ * through bindings and maps against the plot panel's own laid-out box.
216
325
  *
217
326
  * Resting state is the finished chart; `enter` arms every part on its first
218
327
  * frame. A `to` of an axis `range` or `ticks` tweens from the domain as drawn
@@ -225,126 +334,217 @@ export const LineChart = component2d({
225
334
  // Not `x`: every node already has an `x`, its position.
226
335
  xField: "",
227
336
  series: fresh<LineSeries[]>(() => [], { kind: "series" }),
337
+ /** `stacked`: each series sits on the sum of those before it; `percent`: every row reads as shares of 100. */
338
+ stack: Types.oneOf(["none", "stacked", "percent"] as const),
228
339
  xAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
229
340
  yAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
230
- theme: Types.custom<ChartTheme, ChartConfig>(DEFAULT_CHART_THEME, { resolve: resolveChartTheme, lerp: lerpChartTheme }),
341
+ style: LINE_STYLE,
231
342
  /** The spotlit slice in data units, or `null`. The box snaps; {@link regionReveal} animates it. */
232
343
  region: Types.custom<PlotRegion | null>(null),
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`. */
344
+ /** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade. */
234
345
  regionReveal: 0,
235
- /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
236
- showLegend: Types.boolean(true, { animatable: false }),
237
346
  /** The domain `zoomTo` first left, so `resetZoom` restores it, auto-derived ticks included. */
238
347
  home: Types.custom<LineChartDomains | null>(null, { private: true }),
348
+ /** Each series' share of a stack, seeded from `enabled`: a weight rather than a switch, so a hidden series sinks out. */
349
+ weights: Types.custom<number[]>([], { private: true }),
350
+ /** Whether the plot clips its marks, from a zoom's first frame until a reset lands. */
351
+ clipped: Types.boolean(false, { private: true }),
239
352
  },
240
353
  // A node made of parts that fill would otherwise hug them.
241
- defaults: { width: "fill", height: "fill" },
354
+ defaults: {
355
+ width: "fill",
356
+ height: "fill",
357
+ weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
358
+ },
359
+ migrate: { style: migrateChartStyle("cartesian") },
242
360
  allowedChildren: [],
243
361
  children: (state) => {
244
- const theme = () => state.theme
362
+ const style = () => state.style
245
363
  const domains = () => lineChartDomains(state)
246
364
  let plot: Node2D | undefined
247
365
  const plotSize = () => laidOutSize(plot)
366
+ const pixels = (i: number) => seriesPixels(state, domains(), i, plotSize())
248
367
  const enabled = state.series.map((s) => s.enabled ?? true)
368
+ const built = state.style
369
+ const yConfig = () => axisConfigOf(style(), "y")
370
+ const xConfig = () => axisConfigOf(style(), "x")
371
+ const yCaptions = () => {
372
+ const ticks = domains().y.ticks
373
+ return ticks.map((value) => yLabel(state, value, ticks))
374
+ }
249
375
 
250
376
  // Bound, not snapshotted, so a rescale re-labels and re-places the row as it
251
377
  // tweens. The marker *count* is read once, which is why `zoomTo` keeps it.
252
- const vAxis = new VerticalAxis({
253
- range: () => domains().y.range,
254
- markers: () => domains().y.ticks,
255
- captions: () => {
256
- const markers = domains().y.ticks
257
- const format = state.yAxis.format
258
- return markers.map((value) => (format === undefined ? formatMarker(value, markerStep(markers)) : formatNumber(value, format)))
259
- },
260
- axis: () => theme().yAxis,
261
- label: state.yAxis.label ?? "",
262
- width: "hug",
263
- height: "fill",
264
- })
265
-
266
- const hAxis = new HorizontalAxis({
267
- range: () => domains().x.range,
268
- markers: () => domains().x.ticks,
269
- // A month or category tick is captioned with its cell (`Jan`), not the instant it parsed to.
270
- captions: () => {
271
- const column = columnOf(state)
272
- return domains().x.ticks.map((value) => column.label(value))
273
- },
274
- axis: () => theme().xAxis,
275
- label: state.xAxis.label ?? "",
276
- width: "fill",
277
- height: "hug",
278
- })
279
-
280
- const lines = state.series.map(
281
- (s, i) =>
282
- new ChartLine({
283
- points: () => seriesPixels(state, domains(), i, plotSize()),
284
- stroke: () => resolveSeriesStroke(theme().seriesStyles, s, i),
285
- enabled: enabled[i],
286
- // The box the rules are drawn from, so the dim boundary cannot drift from the dashes.
287
- spotlight: () => regionBox(state, domains(), plotSize()),
288
- dimOpacity: () => 1 - (1 - theme().region.dimOpacity) * state.regionReveal,
289
- }) as unknown as Node2D
378
+ const vAxis = built.showYAxis
379
+ ? (new VerticalAxis({
380
+ range: () => domains().y.range,
381
+ markers: () => domains().y.ticks,
382
+ captions: yCaptions,
383
+ axis: yConfig,
384
+ label: state.yAxis.label ?? "",
385
+ width: "hug",
386
+ height: "fill",
387
+ }) as unknown as Node2D)
388
+ : undefined
389
+
390
+ const hAxis = built.showXAxis
391
+ ? (new HorizontalAxis({
392
+ range: () => domains().x.range,
393
+ markers: () => domains().x.ticks,
394
+ // A month or category tick is captioned with its cell (`Jan`), not the instant it parsed to.
395
+ captions: () => {
396
+ const column = columnOf(state)
397
+ return domains().x.ticks.map((value) => column.label(value))
398
+ },
399
+ axis: xConfig,
400
+ label: state.xAxis.label ?? "",
401
+ overhang: () => ({ left: overhang(style()), right: overhang(style()) }),
402
+ width: "fill",
403
+ height: "hug",
404
+ }) as unknown as Node2D)
405
+ : undefined
406
+
407
+ const ghost = vAxis && hAxis
408
+ ? columnGhost({
409
+ captions: yCaptions,
410
+ count: lineChartDomains(state).y.ticks.length,
411
+ text: () => yConfig().labelStyle,
412
+ title: state.yAxis.label ?? "",
413
+ titleText: () => yConfig().titleStyle,
414
+ tickPad: () => axisPads(yConfig(), "right").tick,
415
+ titlePad: () => axisPads(yConfig(), "right").title,
416
+ })
417
+ : undefined
418
+
419
+ const spotlight = () => regionBox(state, domains(), plotSize())
420
+ const dimOpacity = () => 1 - (1 - SPOTLIGHT_DIM) * state.regionReveal
421
+ const lines = state.series.map((s, i) =>
422
+ new ChartLine({
423
+ points: () => pixels(i).points,
424
+ curve: () => style().curve,
425
+ stroke: () => ({ weight: style().strokeWidth, fill: seriesFill(style(), s, i), align: "center", cap: "round", join: "round" }),
426
+ dotSize: () => (style().dots ? style().dotSize : 0),
427
+ dotFill: () => seriesFill(style(), s, i),
428
+ enabled: enabled[i],
429
+ spotlight,
430
+ dimOpacity,
431
+ })
432
+ )
433
+ const areas = state.series.map((s, i) =>
434
+ new ChartArea({
435
+ points: () => pixels(i).points,
436
+ base: () => pixels(i).base,
437
+ baseline: () => mapper(domains(), plotSize()).y(baselineValue(domains())),
438
+ curve: () => style().curve,
439
+ area: () => style().area,
440
+ fill: () => seriesFill(style(), s, i),
441
+ areaOpacity: () => style().areaOpacity,
442
+ // The line's own trace, switch and fade, so the two arrive, leave and dim as one.
443
+ progress: () => lines[i].progress,
444
+ enabled: () => lines[i].enabled,
445
+ opacity: () => lines[i].opacity,
446
+ spotlight,
447
+ dimOpacity,
448
+ })
290
449
  )
291
450
 
292
- const below: Node2D[] = [hAxis as unknown as Node2D]
293
- if (state.showLegend !== false) {
294
- below.push(
295
- new ChartLegend({
296
- series: state.series,
297
- swatches: () => state.series.map((s, i) => resolveSeriesFill(theme().seriesStyles, s, i)),
298
- legend: () => theme().legend,
299
- // Spacing is fixed as built; a theme tween re-paints the swatches only.
300
- gap: state.theme.legend.itemGap,
301
- padding: state.theme.legend.padding,
302
- }) as unknown as Node2D
303
- )
451
+ const labels: Node2D[] = []
452
+ if (built.values !== "none") {
453
+ const below = built.values === "below"
454
+ state.series.forEach((_, i) => {
455
+ state.data.forEach((_row, k) => {
456
+ const line = lines[i]
457
+ const label: Node2D = new Text({
458
+ ...boundText(() => valueText(style())),
459
+ text: () => {
460
+ const value = pixels(i).values[k]
461
+ return value === undefined ? "" : yLabel(state, value, domains().y.ticks)
462
+ },
463
+ // Held inside the plot, so a figure on an end point does not run into the axis beside it.
464
+ x: () => {
465
+ const half = (plotSize().width - label.layoutBounds.width) / 2
466
+ return clamp(pixels(i).points[k]?.x ?? 0, -Math.max(0, half), Math.max(0, half))
467
+ },
468
+ y: () => {
469
+ const point = pixels(i).points[k]
470
+ const clear = label.layoutBounds.height / 2 + (style().dots ? style().dotSize / 2 : style().strokeWidth / 2) + Math.round(sizeOf(style().valueText) * 0.25)
471
+ return (point?.y ?? 0) + (below ? -clear : clear)
472
+ },
473
+ // In once the trace has passed its point.
474
+ opacity: () => {
475
+ const point = pixels(i).points[k]
476
+ if (!line.enabled || !point) return 0
477
+ const reach = traceX(pixels(i).points, line.progress)
478
+ return line.opacity * (reach === Infinity ? 1 : clamp((reach - point.x) / 24, 0, 1))
479
+ },
480
+ }) as unknown as Node2D
481
+ labels.push(label)
482
+ })
483
+ })
304
484
  }
305
485
 
486
+ const legendAt = built.legend
487
+ const legend = legendAt !== "none"
488
+ ? (new ChartLegend({
489
+ series: state.series,
490
+ swatches: () => state.series.map((s, i) => seriesFill(style(), s, i)),
491
+ legend: () => legendConfigOf(style()),
492
+ flow: legendAt === "right" ? "vertical" : "horizontal",
493
+ }) as unknown as Node2D)
494
+ : undefined
495
+
306
496
  plot = new Rect({
307
- clip: true,
497
+ clip: () => state.clipped || overflows(state, domains()),
308
498
  children: [
309
499
  new ChartCanvas({
310
- fill: () => theme().plotArea.fill,
311
- shadow: () => theme().plotArea.shadow,
312
- xGridStroke: () => theme().xAxis.lineStyle,
313
- yGridStroke: () => theme().yAxis.lineStyle,
500
+ fill: () => style().gridBackground,
501
+ xGridStroke: () => style().verticalGrid,
502
+ yGridStroke: () => style().horizontalGrid,
314
503
  xMarkers: () => domains().x.ticks,
315
504
  yMarkers: () => domains().y.ticks,
316
505
  xExtent: () => domains().x.range,
317
506
  yExtent: () => domains().y.range,
507
+ axisStroke: () => style().axisStroke,
508
+ xAxisAt: () => baselineValue(domains()),
509
+ yAxisAt: () => domains().x.range[0],
318
510
  }),
319
- // Under the lines, so the highlight does not tint the stretch it picks out.
511
+ // Under the marks, so the highlight does not tint the stretch it picks out.
320
512
  new ChartRegion({
321
- box: () => regionBox(state, domains(), plotSize()),
322
- stroke: () => theme().region.stroke,
323
- fill: () => theme().region.fill,
513
+ box: spotlight,
514
+ stroke: () => ({ weight: 2, fill: fade(style().valueText.fill ?? "white", 0.7), dash: [14, 12], cap: "butt", align: "center" }),
515
+ fill: () => fade(style().valueText.fill ?? "white", 0.06),
324
516
  reveal: () => state.regionReveal,
325
517
  }),
518
+ ...areas,
326
519
  ...lines,
327
- new ChartBorder({ stroke: () => theme().plotArea.stroke }),
520
+ ...labels,
328
521
  ],
329
522
  }) as unknown as Node2D
330
523
 
331
- const gutter = new VerticalAxis({
332
- opacity: 0,
333
- range: () => domains().y.range,
334
- markers: () => domains().y.ticks,
335
- axis: () => theme().yAxis,
336
- label: state.yAxis.label ?? "",
337
- width: "hug",
338
- height: 0,
339
- }) as unknown as Node2D
340
-
341
- return asParts([cartesianFrame({ axis: vAxis as unknown as Node2D, plot, gutter, below })])
524
+ return asParts([
525
+ cartesianFrame({
526
+ background: () => style().background,
527
+ left: vAxis,
528
+ ghost,
529
+ plot,
530
+ bottom: hAxis,
531
+ legend,
532
+ legendAt,
533
+ gap: () => frameGap(style()),
534
+ padding: () => ({
535
+ top: headroom(style()),
536
+ right: hAxis ? overhang(style()) : 0,
537
+ left: hAxis && !vAxis ? overhang(style()) : 0,
538
+ bottom: 0,
539
+ }),
540
+ }),
541
+ ])
342
542
  },
343
543
  commands: {
344
544
  /**
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.
545
+ * Axes stagger in, lines (and their areas) trace left to right, the legend
546
+ * fades up last. `duration` is the whole entrance; a chart with a `region`
547
+ * lands its call-out in the final quarter.
348
548
  */
349
549
  enter: {
350
550
  args: {},
@@ -353,7 +553,6 @@ export const LineChart = component2d({
353
553
  easing: easeOut(),
354
554
  run: (timeline, state, children) => {
355
555
  const parts = partsOf(children)
356
- if (!parts) return timeline.wait(timeline.duration)
357
556
  const duration = timeline.duration
358
557
  const revealing = hasCallout(state) && state.regionReveal === 0
359
558
  const body = revealing ? duration * 0.75 : duration
@@ -361,17 +560,20 @@ export const LineChart = component2d({
361
560
  const traced = body * ENTER_SHARE.marks
362
561
  const { legend } = parts
363
562
  const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
563
+ const away = legendAway(state.style.legend)
364
564
  // The first frame arms what every later beat moves: a beat ahead of the
365
565
  // playhead writes nothing. A beat with nothing to do still takes its share.
366
566
  return beats(timeline, [
367
567
  ...(revealing ? [at0(timeline.animate(() => ({ regionReveal: 0 })))] : []),
368
568
  ...parts.lines.map((line) => at0(line.to({ progress: 0 }))),
369
- ...(legend ? [at0(legend.to({ opacity: 0, y: -LEGEND_SLIDE }))] : []),
370
- { at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
371
- { at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) },
569
+ ...(legend ? [at0(legend.to({ opacity: 0, ...away }))] : []),
570
+ ...(parts.vAxis ? [{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) }] : []),
571
+ ...(parts.hAxis ? [{ at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) }] : []),
372
572
  ...parts.lines.map((line) => ({ at: axes, duration: traced, piece: line.run("enter") })),
373
- ...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
573
+ ...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
374
574
  ...(revealing ? [{ at: body, duration: duration * 0.25, piece: timeline.animate((t) => ({ regionReveal: t })) }] : []),
575
+ // Takes the whole slot even with no parts to move.
576
+ { at: 0, duration, piece: timeline.wait(duration) },
375
577
  ])
376
578
  },
377
579
  },
@@ -418,7 +620,7 @@ export const LineChart = component2d({
418
620
  ...(region.x && { x: { ...from.x, ...rescale(region.x, from.x.ticks) } }),
419
621
  ...(region.y && { y: { ...from.y, ...rescale(region.y, from.y.ticks) } }),
420
622
  }
421
- return timeline.animate((t) => ({ home, ...axesAt(from, to, t) }))
623
+ return timeline.animate((t) => ({ home, clipped: true, ...axesAt(from, to, t) }))
422
624
  },
423
625
  },
424
626
  /** Back to the domain the first zoom left; a hold if it never zoomed. */
@@ -429,32 +631,32 @@ export const LineChart = component2d({
429
631
  const home = state.home
430
632
  if (!home) return timeline.wait(timeline.duration)
431
633
  const from = lineChartDomains(state)
432
- return timeline.animate((t) => ({ home: null, ...axesAt(from, home, t) }))
634
+ return timeline.animate((t) => ({ home: null, clipped: t < 1, ...axesAt(from, home, t) }))
433
635
  },
434
636
  },
435
- /** Trace series `index` in, with its legend entry growing in. */
637
+ /** Bring series `index` in, with its legend entry growing in: traced, or in a stack risen out of the series below. */
436
638
  showSeries: {
437
639
  args: { seriesIndex: SERIES_ARG },
438
640
  easing: easeOut(),
439
- run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, true),
641
+ run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, true),
440
642
  },
441
- /** Retrace series `index` out as its legend entry shrinks away. */
643
+ /** The mirror of `showSeries`. */
442
644
  hideSeries: {
443
645
  args: { seriesIndex: SERIES_ARG },
444
646
  easing: easeOut(),
445
- run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, false),
647
+ run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, false),
446
648
  },
447
- /** Fade every other series, line and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
649
+ /** Fade every other series, line, area and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
448
650
  spotlightSeries: {
449
651
  args: { seriesIndex: SERIES_ARG },
450
652
  easing: easeOut(),
451
653
  run: (timeline, _state, children) => {
452
654
  const parts = partsOf(children)
453
- if (!parts) return timeline.wait(timeline.duration)
454
655
  const index = timeline.args.seriesIndex
455
656
  return timeline.together([
456
657
  ...parts.lines.map((line, i) => line.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
457
658
  ...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
659
+ timeline.wait(timeline.duration),
458
660
  ])
459
661
  },
460
662
  },