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

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