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