@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,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, CARTESIAN_TEXT, CHART_LEGEND, CHART_SERIES, CHART_STYLE_FAMILY, CHART_STYLE_PRESETS, GRID_BACKGROUND, axisConfigOf, legendConfigOf, migrateChartStyle, seriesFill, sizeOf, 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, { series: CHART_SERIES, background: GRID_BACKGROUND, text: CARTESIAN_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,206 @@ 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
+ },
231
+ migrate: { style: migrateChartStyle("cartesian") },
165
232
  allowedChildren: [],
166
233
  children: (state) => {
167
- const theme = () => state.theme;
234
+ const style = () => state.style;
168
235
  const domains = () => lineChartDomains(state);
169
236
  let plot;
170
237
  const plotSize = () => laidOutSize(plot);
238
+ const pixels = (i) => seriesPixels(state, domains(), i, plotSize());
171
239
  const enabled = state.series.map((s) => s.enabled ?? true);
240
+ const built = state.style;
241
+ const yConfig = () => axisConfigOf(style(), "y");
242
+ const xConfig = () => axisConfigOf(style(), "x");
243
+ const yCaptions = () => {
244
+ const ticks = domains().y.ticks;
245
+ return ticks.map((value) => yLabel(state, value, ticks));
246
+ };
172
247
  // Bound, not snapshotted, so a rescale re-labels and re-places the row as it
173
248
  // 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
- });
249
+ const vAxis = built.showYAxis
250
+ ? new VerticalAxis({
251
+ range: () => domains().y.range,
252
+ markers: () => domains().y.ticks,
253
+ captions: yCaptions,
254
+ axis: yConfig,
255
+ label: state.yAxis.label ?? "",
256
+ width: "hug",
257
+ height: "fill",
258
+ })
259
+ : undefined;
260
+ const hAxis = built.showXAxis
261
+ ? new HorizontalAxis({
262
+ range: () => domains().x.range,
263
+ markers: () => domains().x.ticks,
264
+ // A month or category tick is captioned with its cell (`Jan`), not the instant it parsed to.
265
+ captions: () => {
266
+ const column = columnOf(state);
267
+ return domains().x.ticks.map((value) => column.label(value));
268
+ },
269
+ axis: xConfig,
270
+ label: state.xAxis.label ?? "",
271
+ overhang: () => ({ left: overhang(style()), right: overhang(style()) }),
272
+ width: "fill",
273
+ height: "hug",
274
+ })
275
+ : undefined;
276
+ const ghost = vAxis && hAxis
277
+ ? columnGhost({
278
+ captions: yCaptions,
279
+ count: lineChartDomains(state).y.ticks.length,
280
+ text: () => yConfig().labelStyle,
281
+ title: state.yAxis.label ?? "",
282
+ titleText: () => yConfig().titleStyle,
283
+ tickPad: () => axisPads(yConfig(), "right").tick,
284
+ titlePad: () => axisPads(yConfig(), "right").title,
285
+ })
286
+ : undefined;
287
+ const spotlight = () => regionBox(state, domains(), plotSize());
288
+ const dimOpacity = () => 1 - (1 - SPOTLIGHT_DIM) * state.regionReveal;
200
289
  const lines = state.series.map((s, i) => new ChartLine({
201
- points: () => seriesPixels(state, domains(), i, plotSize()),
202
- stroke: () => resolveSeriesStroke(theme().seriesStyles, s, i),
290
+ points: () => pixels(i).points,
291
+ curve: () => style().curve,
292
+ stroke: () => ({ weight: style().strokeWidth, fill: seriesFill(style(), s, i), align: "center", cap: "round", join: "round" }),
293
+ dotSize: () => (style().dots ? style().dotSize : 0),
294
+ dotFill: () => seriesFill(style(), s, i),
203
295
  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,
296
+ spotlight,
297
+ dimOpacity,
207
298
  }));
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
- }));
299
+ const areas = state.series.map((s, i) => new ChartArea({
300
+ points: () => pixels(i).points,
301
+ base: () => pixels(i).base,
302
+ baseline: () => mapper(domains(), plotSize()).y(baselineValue(domains())),
303
+ curve: () => style().curve,
304
+ area: () => style().area,
305
+ fill: () => seriesFill(style(), s, i),
306
+ areaOpacity: () => style().areaOpacity,
307
+ // The line's own trace, switch and fade, so the two arrive, leave and dim as one.
308
+ progress: () => lines[i].progress,
309
+ enabled: () => lines[i].enabled,
310
+ opacity: () => lines[i].opacity,
311
+ spotlight,
312
+ dimOpacity,
313
+ }));
314
+ const labels = [];
315
+ if (built.values !== "none") {
316
+ const below = built.values === "below";
317
+ state.series.forEach((_, i) => {
318
+ state.data.forEach((_row, k) => {
319
+ const line = lines[i];
320
+ const label = new Text({
321
+ ...boundText(() => valueText(style())),
322
+ text: () => {
323
+ const value = pixels(i).values[k];
324
+ return value === undefined ? "" : yLabel(state, value, domains().y.ticks);
325
+ },
326
+ // Held inside the plot, so a figure on an end point does not run into the axis beside it.
327
+ x: () => {
328
+ const half = (plotSize().width - label.layoutBounds.width) / 2;
329
+ return clamp(pixels(i).points[k]?.x ?? 0, -Math.max(0, half), Math.max(0, half));
330
+ },
331
+ y: () => {
332
+ const point = pixels(i).points[k];
333
+ const clear = label.layoutBounds.height / 2 + (style().dots ? style().dotSize / 2 : style().strokeWidth / 2) + Math.round(sizeOf(style().valueText) * 0.25);
334
+ return (point?.y ?? 0) + (below ? -clear : clear);
335
+ },
336
+ // In once the trace has passed its point.
337
+ opacity: () => {
338
+ const point = pixels(i).points[k];
339
+ if (!line.enabled || !point)
340
+ return 0;
341
+ const reach = traceX(pixels(i).points, line.progress);
342
+ return line.opacity * (reach === Infinity ? 1 : clamp((reach - point.x) / 24, 0, 1));
343
+ },
344
+ });
345
+ labels.push(label);
346
+ });
347
+ });
218
348
  }
349
+ const legendAt = built.legend;
350
+ const legend = legendAt !== "none"
351
+ ? new ChartLegend({
352
+ series: state.series,
353
+ swatches: () => state.series.map((s, i) => seriesFill(style(), s, i)),
354
+ legend: () => legendConfigOf(style()),
355
+ flow: legendAt === "right" ? "vertical" : "horizontal",
356
+ })
357
+ : undefined;
219
358
  plot = new Rect({
220
- clip: true,
359
+ clip: () => state.clipped || overflows(state, domains()),
221
360
  children: [
222
361
  new ChartCanvas({
223
- fill: () => theme().plotArea.fill,
224
- shadow: () => theme().plotArea.shadow,
225
- xGridStroke: () => theme().xAxis.lineStyle,
226
- yGridStroke: () => theme().yAxis.lineStyle,
362
+ fill: () => style().gridBackground,
363
+ xGridStroke: () => style().verticalGrid,
364
+ yGridStroke: () => style().horizontalGrid,
227
365
  xMarkers: () => domains().x.ticks,
228
366
  yMarkers: () => domains().y.ticks,
229
367
  xExtent: () => domains().x.range,
230
368
  yExtent: () => domains().y.range,
369
+ axisStroke: () => style().axisStroke,
370
+ xAxisAt: () => baselineValue(domains()),
371
+ yAxisAt: () => domains().x.range[0],
231
372
  }),
232
- // Under the lines, so the highlight does not tint the stretch it picks out.
373
+ // Under the marks, so the highlight does not tint the stretch it picks out.
233
374
  new ChartRegion({
234
- box: () => regionBox(state, domains(), plotSize()),
235
- stroke: () => theme().region.stroke,
236
- fill: () => theme().region.fill,
375
+ box: spotlight,
376
+ stroke: () => ({ weight: 2, fill: fade(style().valueText.fill ?? "white", 0.7), dash: [14, 12], cap: "butt", align: "center" }),
377
+ fill: () => fade(style().valueText.fill ?? "white", 0.06),
237
378
  reveal: () => state.regionReveal,
238
379
  }),
380
+ ...areas,
239
381
  ...lines,
240
- new ChartBorder({ stroke: () => theme().plotArea.stroke }),
382
+ ...labels,
241
383
  ],
242
384
  });
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 })]);
385
+ return asParts([
386
+ cartesianFrame({
387
+ background: () => style().background,
388
+ left: vAxis,
389
+ ghost,
390
+ plot,
391
+ bottom: hAxis,
392
+ legend,
393
+ legendAt,
394
+ gap: () => frameGap(style()),
395
+ padding: () => ({
396
+ top: headroom(style()),
397
+ right: hAxis ? overhang(style()) : 0,
398
+ left: hAxis && !vAxis ? overhang(style()) : 0,
399
+ bottom: 0,
400
+ }),
401
+ }),
402
+ ]);
253
403
  },
254
404
  commands: {
255
405
  /**
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.
406
+ * Axes stagger in, lines (and their areas) trace left to right, the legend
407
+ * fades up last. `duration` is the whole entrance; a chart with a `region`
408
+ * lands its call-out in the final quarter.
259
409
  */
260
410
  enter: {
261
411
  args: {},
@@ -264,8 +414,6 @@ export const LineChart = component2d({
264
414
  easing: easeOut(),
265
415
  run: (timeline, state, children) => {
266
416
  const parts = partsOf(children);
267
- if (!parts)
268
- return timeline.wait(timeline.duration);
269
417
  const duration = timeline.duration;
270
418
  const revealing = hasCallout(state) && state.regionReveal === 0;
271
419
  const body = revealing ? duration * 0.75 : duration;
@@ -273,17 +421,20 @@ export const LineChart = component2d({
273
421
  const traced = body * ENTER_SHARE.marks;
274
422
  const { legend } = parts;
275
423
  const at0 = (piece) => ({ at: 0, duration: 0, piece });
424
+ const away = legendAway(state.style.legend);
276
425
  // The first frame arms what every later beat moves: a beat ahead of the
277
426
  // playhead writes nothing. A beat with nothing to do still takes its share.
278
427
  return beats(timeline, [
279
428
  ...(revealing ? [at0(timeline.animate(() => ({ regionReveal: 0 })))] : []),
280
429
  ...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 }) },
430
+ ...(legend ? [at0(legend.to({ opacity: 0, ...away }))] : []),
431
+ ...(parts.vAxis ? [{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) }] : []),
432
+ ...(parts.hAxis ? [{ at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) }] : []),
284
433
  ...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 }) }] : []),
434
+ ...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
286
435
  ...(revealing ? [{ at: body, duration: duration * 0.25, piece: timeline.animate((t) => ({ regionReveal: t })) }] : []),
436
+ // Takes the whole slot even with no parts to move.
437
+ { at: 0, duration, piece: timeline.wait(duration) },
287
438
  ]);
288
439
  },
289
440
  },
@@ -330,7 +481,7 @@ export const LineChart = component2d({
330
481
  ...(region.x && { x: { ...from.x, ...rescale(region.x, from.x.ticks) } }),
331
482
  ...(region.y && { y: { ...from.y, ...rescale(region.y, from.y.ticks) } }),
332
483
  };
333
- return timeline.animate((t) => ({ home, ...axesAt(from, to, t) }));
484
+ return timeline.animate((t) => ({ home, clipped: true, ...axesAt(from, to, t) }));
334
485
  },
335
486
  },
336
487
  /** Back to the domain the first zoom left; a hold if it never zoomed. */
@@ -342,33 +493,32 @@ export const LineChart = component2d({
342
493
  if (!home)
343
494
  return timeline.wait(timeline.duration);
344
495
  const from = lineChartDomains(state);
345
- return timeline.animate((t) => ({ home: null, ...axesAt(from, home, t) }));
496
+ return timeline.animate((t) => ({ home: null, clipped: t < 1, ...axesAt(from, home, t) }));
346
497
  },
347
498
  },
348
- /** Trace series `index` in, with its legend entry growing in. */
499
+ /** Bring series `index` in, with its legend entry growing in: traced, or in a stack risen out of the series below. */
349
500
  showSeries: {
350
501
  args: { seriesIndex: SERIES_ARG },
351
502
  easing: easeOut(),
352
- run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, true),
503
+ run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, true),
353
504
  },
354
- /** Retrace series `index` out as its legend entry shrinks away. */
505
+ /** The mirror of `showSeries`. */
355
506
  hideSeries: {
356
507
  args: { seriesIndex: SERIES_ARG },
357
508
  easing: easeOut(),
358
- run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, false),
509
+ run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, false),
359
510
  },
360
- /** Fade every other series, line and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
511
+ /** 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
512
  spotlightSeries: {
362
513
  args: { seriesIndex: SERIES_ARG },
363
514
  easing: easeOut(),
364
515
  run: (timeline, _state, children) => {
365
516
  const parts = partsOf(children);
366
- if (!parts)
367
- return timeline.wait(timeline.duration);
368
517
  const index = timeline.args.seriesIndex;
369
518
  return timeline.together([
370
519
  ...parts.lines.map((line, i) => line.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
371
520
  ...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
521
+ timeline.wait(timeline.duration),
372
522
  ]);
373
523
  },
374
524
  },