@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,78 +1,84 @@
1
1
  import { Rect, 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 { cartesianFrame, chartViews, columnGhost, 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 { formatMarker, lerpAxisDomain, lerpAxisEntry, markerStep, markerValues, num, resolveAxisDomain, settleAxisDomain, } from "../kit/shared.js";
11
- import { CategoryAxis } from "./category-axis.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, niceTicks, num, 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, migrateChartStyle, legendConfigOf, seriesFill, valueText, } from "../kit/style.js";
11
+ import { VerticalAxis, axisPads } from "../kit/vertical-axis.js";
12
+ import { CategoryAxis, slotCentre } from "./category-axis.js";
12
13
  import { ChartBars } from "./chart-bars.js";
13
- import { DEFAULT_BAR_CHART_THEME, barCategories, barSpans, lerpBarChartTheme, resolveBarChartTheme, resolveBarFill, resolveBarScale, stackCapIndex, } from "./shared.js";
14
+ import { BAR_LABELS, BAR_MARKS, barCategories, barSpans, resolveBarScale, } from "./shared.js";
15
+ const BAR_STYLE = Types.style(CHART_STYLE_FAMILY, { series: CHART_SERIES, background: GRID_BACKGROUND, text: CARTESIAN_TEXT, grid: CARTESIAN_GRID, marks: BAR_MARKS, labels: BAR_LABELS, legend: CHART_LEGEND }, { presets: CHART_STYLE_PRESETS });
14
16
  function drawnValue(s, domain) {
15
17
  const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1;
16
- const range = resolveBarScale(s.data, s.series, s.stacked, domain.range, divisions);
17
- return { ...domain, range, ticks: domain.ticks ?? markerValues(range, DEFAULT_MARKERS - 1) };
18
+ const range = resolveBarScale(s.data, s.series, s.stack, domain.range, divisions);
19
+ const even = domain.range !== undefined || s.stack === "percent";
20
+ return { ...domain, range, ticks: domain.ticks ?? (even ? markerValues(range, DEFAULT_MARKERS - 1) : niceTicks(range, DEFAULT_MARKERS - 1)) };
18
21
  }
19
22
  /** The last domain worked out from each data array, and what it was read from. */
20
23
  const domainsDrawn = new WeakMap();
21
- /** The value axis as drawn now. */
24
+ /** The value axis as drawn now. A series being hidden keeps its room, so the ticks hold still while it goes. */
22
25
  export function barChartDomain(s) {
23
- const { data, series, stacked, valueAxis } = s;
24
- const kept = domainsDrawn.get(data);
25
- if (kept && kept.series === series && kept.stacked === stacked && kept.valueAxis === valueAxis)
26
+ const key = [s.series, s.stack, s.valueAxis];
27
+ const kept = domainsDrawn.get(s.data);
28
+ if (kept && kept.key.every((value, i) => value === key[i]))
26
29
  return kept.domain;
27
- const domain = settleAxisDomain(valueAxis, (d) => drawnValue(s, d));
28
- domainsDrawn.set(data, { series, stacked, valueAxis, domain });
30
+ const domain = settleAxisDomain(s.valueAxis, (d) => drawnValue(s, d));
31
+ domainsDrawn.set(s.data, { key, domain });
29
32
  return domain;
30
33
  }
31
- /** Ticks and the figures printed on columns both read through here, so they cannot disagree about how a number looks. */
34
+ /** Ticks and the figures printed by bars both read through here, so they cannot disagree about how a number looks. */
32
35
  function valueLabel(s, domain, value) {
33
36
  const pattern = s.valueAxis.format;
34
- return pattern === undefined ? formatMarker(value, markerStep(domain.ticks)) : formatNumber(value, pattern);
37
+ if (pattern !== undefined)
38
+ return formatNumber(value, pattern);
39
+ const text = formatMarker(value, markerStep(domain.ticks));
40
+ return s.stack === "percent" ? `${text}%` : text;
35
41
  }
36
- /** A value in the plot's centred pixel space (origin at its centre, y-up). */
37
- function valueToY(domain, plot, value) {
42
+ /** A value along the value axis, a run of `length` px centred on 0. */
43
+ function valueToPx(domain, length, value) {
38
44
  const [min, max] = domain.range;
39
- if (max <= min)
40
- return -plot.height / 2;
41
- return ((value - min) / (max - min) - 0.5) * plot.height;
45
+ if (max === min)
46
+ return -length / 2;
47
+ return ((value - min) / (max - min) - 0.5) * length;
42
48
  }
43
- /** Pixel y columns grow from: zero clamped onto the scale, so a cropped axis grows them from the plot floor. */
44
- function baselinePx(domain, plot) {
45
- const [min, max] = domain.range;
46
- return valueToY(domain, plot, Math.min(Math.max(0, min), max));
49
+ /** Where bars grow from: zero held on the scale, so a cropped axis grows them from the plot's edge. */
50
+ function baselineValue(domain) {
51
+ const [lo, hi] = domain.range;
52
+ return clamp(0, Math.min(lo, hi), Math.max(lo, hi));
47
53
  }
48
54
  function weightAt(s, index) {
49
55
  return Math.max(0, s.weights[index] ?? 1);
50
56
  }
51
- /** `row` with each series' value scaled by its live weight, which stacked spans are measured from. */
52
- function weightedRow(s, row) {
53
- const scaled = { ...row };
54
- s.series.forEach((series, i) => {
55
- const value = row[series.field];
56
- if (typeof value === "number")
57
- scaled[series.field] = value * weightAt(s, i);
58
- });
59
- return scaled;
57
+ /** The plot's run along the value axis and across the categories. */
58
+ function runs(s, plot) {
59
+ return s.orientation === "horizontal" ? { along: plot.width, across: plot.height } : { along: plot.height, across: plot.width };
60
+ }
61
+ /** Category `row`'s slot centre across the plot: left to right, or top to bottom. */
62
+ function slotAt(s, row, across) {
63
+ const centre = slotCentre(row, s.data.length, across);
64
+ return s.orientation === "horizontal" ? -centre : centre;
60
65
  }
61
66
  /**
62
- * Series `index`'s columns in centred pixel space. A grouped series divides the
63
- * slot with its siblings by live weight, a stacked one takes the whole slot, so
64
- * hiding a series reflows rather than punching a hole.
67
+ * Series `index`'s bars in centred pixel space. Side by side, a series divides
68
+ * the slot with its siblings by live weight; stacked, it takes the whole slot
69
+ * and its weight scales its length, so hiding a series reflows rather than
70
+ * punching a hole.
65
71
  */
66
- function seriesBars(s, index, plot, captioned) {
72
+ function seriesBars(s, index, plot) {
67
73
  const series = s.series[index];
68
74
  if (!series)
69
75
  return [];
70
76
  const domain = barChartDomain(s);
77
+ const { along, across } = runs(s, plot);
71
78
  const n = s.data.length;
72
- const slotWidth = n > 0 ? plot.width / n : 0;
73
- const slotCenter = (i) => -plot.width / 2 + slotWidth * (i + 0.5);
74
- const inner = slotWidth * clamp(s.theme.bar.slotFill, 0, 1);
75
- const gap = s.theme.bar.groupGap;
79
+ const slot = n > 0 ? across / n : 0;
80
+ const inner = slot * clamp(s.style.barWidth, 0, 1);
81
+ const stacked = s.stack !== "none";
76
82
  let total = 0;
77
83
  for (let i = 0; i < s.series.length; i++)
78
84
  total += weightAt(s, i);
@@ -81,59 +87,57 @@ function seriesBars(s, index, plot, captioned) {
81
87
  let before = 0;
82
88
  for (let i = 0; i < index; i++)
83
89
  before += weightAt(s, i);
90
+ const gap = s.series.length > 1 ? s.style.barGap : 0;
91
+ const values = s.style.values;
84
92
  return s.data.map((row, rowIndex) => {
85
- // Built from live contributions, so a fading series lowers the segments above it.
86
- const spans = barSpans(s.stacked ? weightedRow(s, row) : row, s.series, s.stacked);
87
- const span = spans[index] ?? { from: 0, to: 0, value: 0 };
88
- const cap = s.stacked ? stackCapIndex(row, s.series) : index;
89
- const slotLeft = slotCenter(rowIndex) - inner / 2;
90
- const x = s.stacked ? slotCenter(rowIndex) : slotLeft + unit * before + (unit * weight) / 2;
91
- const width = s.stacked ? inner : Math.max(0, unit * weight - (s.series.length > 1 ? gap : 0));
92
- // The printed figure is the row's own value, not the weighted one.
93
- const value = num(row, series.field);
93
+ // Built from live contributions, so a fading series lowers the segments beyond it.
94
+ const spans = barSpans(row, s.series, s.stack, s.weights);
95
+ const span = spans[index] ?? { from: 0, to: 0, value: 0, outer: false };
96
+ const centre = slotAt(s, rowIndex, across);
97
+ const offset = -inner / 2 + unit * before + (unit * weight) / 2;
98
+ let caption = "";
99
+ if (values !== "none" && span.value !== 0) {
100
+ if (!stacked || values === "inside" || s.stack === "percent")
101
+ caption = valueLabel(s, domain, stacked ? span.value : num(row, series.field));
102
+ else if (span.outer)
103
+ caption = valueLabel(s, domain, span.to);
104
+ }
94
105
  return {
95
- x,
96
- width,
97
- from: valueToY(domain, plot, span.from),
98
- to: valueToY(domain, plot, span.to),
99
- round: cap === index,
100
- value,
101
- ...(captioned && { caption: valueLabel(s, domain, value) }),
106
+ center: stacked ? centre : centre + (s.orientation === "horizontal" ? -offset : offset),
107
+ thickness: stacked ? inner : Math.max(0, unit * weight - gap),
108
+ from: valueToPx(domain, along, span.from),
109
+ to: valueToPx(domain, along, span.to),
110
+ round: span.outer,
111
+ grounded: !stacked || span.from === 0,
112
+ caption,
102
113
  };
103
114
  });
104
115
  }
105
- /** Per-row opacity: 1 while nothing is spotlit, falling to the theme's `dimOpacity` for all but the chosen category. */
116
+ /** Per-row opacity: 1 while nothing is spotlit, falling to {@link SPOTLIGHT_DIM} for all but the chosen category. */
106
117
  function categoryDims(s) {
107
118
  const focus = clamp(s.spotlightReveal, 0, 1);
108
- const dim = s.theme.spotlight.dimOpacity;
109
119
  const chosen = s.spotlight;
110
- return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - dim) * focus));
120
+ return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - SPOTLIGHT_DIM) * focus));
111
121
  }
112
122
  /** The parts a bar chart's commands drive, found through its children. */
113
123
  function partsOf(children) {
114
- const frame = cartesianViews(children);
115
- if (!frame)
116
- return null;
117
- const cAxis = frame.below.find(CategoryAxis.is);
118
- if (!VerticalAxis.is(frame.axis) || !cAxis)
119
- return null;
124
+ const views = chartViews(children);
120
125
  return {
121
- vAxis: frame.axis,
122
- cAxis,
123
- bars: frame.plot.filter(ChartBars.is),
124
- legend: frame.below.find(ChartLegend.is) ?? null,
126
+ valueAxis: views.find((view) => VerticalAxis.is(view) || HorizontalAxis.is(view)) ?? null,
127
+ categoryAxis: views.find(CategoryAxis.is) ?? null,
128
+ bars: views.filter(ChartBars.is),
129
+ legend: views.find(ChartLegend.is) ?? null,
125
130
  };
126
131
  }
127
132
  /**
128
- * Move series `index`'s slot share to `target` with its legend entry. Its
129
- * columns stay drawn for every frame the move covers and switch off only once a
130
- * hide has landed, so arriving at a time backwards matches arriving forwards.
133
+ * Move series `index`'s share to `target` with its legend entry. Its bars stay
134
+ * drawn for every frame the move covers and switch off only once a hide has
135
+ * landed, so arriving at a time backwards matches arriving forwards.
131
136
  */
132
137
  function toggling(timeline, state, children, index, target) {
133
138
  const parts = partsOf(children);
134
- if (!parts || !parts.bars[index])
139
+ if (!parts.bars[index])
135
140
  return timeline.wait(timeline.duration);
136
- // Through `weightAt`, so short stored weights still tween from the 1 drawn.
137
141
  const from = state.series.map((_, i) => weightAt(state, i));
138
142
  const start = from[index] ?? 1;
139
143
  const showing = state.series.map((s, i) => state.showing[i] ?? (s.enabled ?? true));
@@ -149,38 +153,38 @@ function toggling(timeline, state, children, index, target) {
149
153
  ]);
150
154
  }
151
155
  /**
152
- * A grouped or stacked bar chart, built like the line chart from `ChartCanvas`,
153
- * `VerticalAxis`, {@link CategoryAxis}, one {@link ChartBars} per series and
154
- * `ChartLegend`. Zero is always on the scale and columns grow about it, so a
155
- * negative value draws downward from the same `growth` signal.
156
+ * A bar chart, vertical or horizontal, its series side by side, stacked, or
157
+ * stacked as shares of 100. Built like the line chart from `ChartCanvas`, a
158
+ * value axis, a {@link CategoryAxis}, one {@link ChartBars} per series and
159
+ * `ChartLegend`. Zero is always on the scale and bars grow about it, so a
160
+ * negative value draws the other way from the same `growth` signal.
156
161
  *
157
162
  * Resting state is the finished chart; `enter` arms every part on its first
158
163
  * frame. A `to` of the value axis's `range` or `ticks` tweens from the domain as
159
- * drawn, so an auto axis rescales the columns continuously.
164
+ * drawn, so an auto axis rescales the bars continuously.
160
165
  */
161
166
  export const BarChart = component2d({
162
167
  key: "barChart",
163
168
  state: {
164
- data: fresh(() => []),
169
+ data: fresh(() => [], { kind: "dataset" }),
165
170
  // Names a column, not a value, like the line chart's `xField`.
166
171
  categoryField: "",
167
- series: fresh(() => []),
168
- stacked: false,
172
+ series: fresh(() => [], { kind: "series" }),
173
+ /** `vertical`: columns rise from a row of categories; `horizontal`: bars grow right from a column of them. */
174
+ orientation: Types.oneOf(["vertical", "horizontal"], { animatable: false }),
175
+ /** `stacked`: each series sits on the sum of those before it; `percent`: every category reads as shares of 100. */
176
+ stack: Types.oneOf(["none", "stacked", "percent"]),
169
177
  valueAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
170
178
  categoryAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisDomain }),
171
- theme: Types.custom(DEFAULT_BAR_CHART_THEME, { resolve: resolveBarChartTheme, lerp: lerpBarChartTheme }),
172
- /** Rise of every column about the baseline, `[0, 1]`: 0 collapses them onto it. */
179
+ style: BAR_STYLE,
180
+ /** Rise of every bar about the baseline, `[0, 1]`: 0 collapses them onto it. */
173
181
  growth: 1,
174
- /** Each series' share of the slot, seeded from `enabled`: a weight rather than a switch, so hiding one widens the others. */
175
- weights: fresh(() => []),
176
182
  /** The category spotlit, or `null`. The choice snaps; `spotlightReveal` animates it. */
177
183
  spotlight: Types.custom(null),
178
184
  spotlightReveal: 0,
179
- /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
180
- showLegend: Types.boolean(true, { animatable: false }),
181
- /** Read as the chart is built: whether each column prints the figure it plots. */
182
- showValues: Types.boolean(false, { animatable: false }),
183
- /** Whether each series' columns are drawn: on for every frame a show or hide covers, off once a hide lands. */
185
+ /** Each series' share of the slot, seeded from `enabled`: a weight rather than a switch, so hiding one widens the others. */
186
+ weights: Types.custom([], { private: true }),
187
+ /** Whether each series' bars are drawn: on for every frame a show or hide covers, off once a hide lands. */
184
188
  showing: Types.custom([], { private: true }),
185
189
  },
186
190
  defaults: {
@@ -190,97 +194,143 @@ export const BarChart = component2d({
190
194
  weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
191
195
  showing: (state) => state.series.map((s) => s.enabled ?? true),
192
196
  },
197
+ migrate: { style: migrateChartStyle("cartesian") },
193
198
  allowedChildren: [],
194
199
  children: (state) => {
195
- const theme = () => state.theme;
200
+ const style = () => state.style;
196
201
  const domain = () => barChartDomain(state);
197
202
  let plot;
198
203
  const plotSize = () => laidOutSize(plot);
199
- const captioned = state.showValues === true;
200
- const vAxis = new VerticalAxis({
201
- range: () => domain().range,
202
- markers: () => domain().ticks,
203
- captions: () => {
204
- const d = domain();
205
- return d.ticks.map((value) => valueLabel(state, d, value));
206
- },
207
- axis: () => theme().yAxis,
208
- label: state.valueAxis.label ?? "",
209
- width: "hug",
210
- height: "fill",
211
- });
212
- const cAxis = new CategoryAxis({
213
- categories: barCategories(state.data, state.categoryField),
214
- axis: () => theme().xAxis,
215
- label: state.categoryAxis.label ?? "",
216
- dims: () => categoryDims(state),
217
- width: "fill",
218
- height: "hug",
219
- });
204
+ const built = state.style;
205
+ const horizontal = state.orientation === "horizontal";
206
+ const categories = barCategories(state.data, state.categoryField);
207
+ const valueConfig = () => axisConfigOf(style(), horizontal ? "x" : "y");
208
+ const categoryConfig = () => axisConfigOf(style(), horizontal ? "y" : "x");
209
+ const valueCaptions = () => {
210
+ const d = domain();
211
+ return d.ticks.map((value) => valueLabel(state, d, value));
212
+ };
213
+ const showValue = horizontal ? built.showXAxis : built.showYAxis;
214
+ const showCategory = horizontal ? built.showYAxis : built.showXAxis;
215
+ const valueAxis = showValue
216
+ ? (horizontal
217
+ ? new HorizontalAxis({
218
+ range: () => domain().range,
219
+ markers: () => domain().ticks,
220
+ captions: valueCaptions,
221
+ axis: valueConfig,
222
+ label: state.valueAxis.label ?? "",
223
+ overhang: () => ({ left: overhang(style()), right: overhang(style()) }),
224
+ width: "fill",
225
+ height: "hug",
226
+ })
227
+ : new VerticalAxis({
228
+ range: () => domain().range,
229
+ markers: () => domain().ticks,
230
+ captions: valueCaptions,
231
+ axis: valueConfig,
232
+ label: state.valueAxis.label ?? "",
233
+ width: "hug",
234
+ height: "fill",
235
+ }))
236
+ : undefined;
237
+ const categoryAxis = showCategory
238
+ ? new CategoryAxis({
239
+ categories,
240
+ axis: categoryConfig,
241
+ label: state.categoryAxis.label ?? "",
242
+ orientation: horizontal ? "vertical" : "horizontal",
243
+ dims: () => categoryDims(state),
244
+ width: horizontal ? "hug" : "fill",
245
+ height: horizontal ? "fill" : "hug",
246
+ })
247
+ : undefined;
248
+ const left = horizontal ? categoryAxis : valueAxis;
249
+ const bottom = horizontal ? valueAxis : categoryAxis;
250
+ const leftConfig = horizontal ? categoryConfig : valueConfig;
251
+ const ghost = left && bottom
252
+ ? columnGhost({
253
+ captions: horizontal ? () => categories : valueCaptions,
254
+ count: horizontal ? categories.length : barChartDomain(state).ticks.length,
255
+ text: () => leftConfig().labelStyle,
256
+ title: (horizontal ? state.categoryAxis.label : state.valueAxis.label) ?? "",
257
+ titleText: () => leftConfig().titleStyle,
258
+ tickPad: () => axisPads(leftConfig(), "right").tick,
259
+ titlePad: () => axisPads(leftConfig(), "right").title,
260
+ })
261
+ : undefined;
220
262
  const barSets = state.series.map((s, i) => new ChartBars({
221
- bars: () => seriesBars(state, i, plotSize(), captioned),
222
- baseline: () => baselinePx(domain(), plotSize()),
223
- fill: () => resolveBarFill(theme().seriesFills, s, i),
224
- barStroke: () => theme().bar.stroke,
225
- barShadow: () => theme().bar.shadow,
226
- cornerRadius: () => theme().bar.cornerRadius,
263
+ bars: () => seriesBars(state, i, plotSize()),
264
+ baseline: () => valueToPx(domain(), runs(state, plotSize()).along, baselineValue(domain())),
265
+ horizontal,
266
+ fill: () => seriesFill(style(), s, i),
267
+ cornerRadius: () => style().barCornerRadius,
268
+ cornerStyle: () => style().barCornerStyle,
227
269
  growth: () => state.growth,
228
270
  dims: () => categoryDims(state),
229
- showValues: captioned,
230
- valueStyle: state.theme.valueStyle,
271
+ // A figure past a stack's end prints the stack's total; a percent stack has none worth printing, so prints its shares inside.
272
+ values: () => (built.values === "end" && state.stack === "percent" ? "inside" : built.values),
273
+ valueStyle: () => valueText(style()),
231
274
  enabled: () => state.showing[i] ?? (s.enabled ?? true),
232
275
  }));
233
- const below = [cAxis];
234
- if (state.showLegend !== false) {
235
- below.push(new ChartLegend({
276
+ const legendAt = built.legend;
277
+ const legend = legendAt !== "none"
278
+ ? new ChartLegend({
236
279
  series: state.series,
237
- swatches: () => state.series.map((s, i) => resolveBarFill(theme().seriesFills, s, i)),
238
- legend: () => theme().legend,
239
- // Spacing is fixed as built; a theme tween re-paints the swatches only.
240
- gap: state.theme.legend.itemGap,
241
- padding: state.theme.legend.padding,
242
- }));
243
- }
280
+ swatches: () => state.series.map((s, i) => seriesFill(style(), s, i)),
281
+ legend: () => legendConfigOf(style()),
282
+ flow: legendAt === "right" ? "vertical" : "horizontal",
283
+ })
284
+ : undefined;
285
+ const n = state.data.length;
286
+ // Category lines fall between the slots, never through a bar.
287
+ const boundaries = () => Array.from({ length: Math.max(0, n - 1) }, (_, i) => i + 1);
288
+ const slots = () => [0, Math.max(1, n)];
244
289
  plot = new Rect({
245
- clip: true,
246
290
  children: [
247
291
  new ChartCanvas({
248
- fill: () => theme().plotArea.fill,
249
- shadow: () => theme().plotArea.shadow,
250
- xGridStroke: () => theme().xAxis.lineStyle,
251
- yGridStroke: () => theme().yAxis.lineStyle,
252
- // Categorical x axis: no numeric position for a vertical rule to mark.
253
- xMarkers: [],
254
- yMarkers: () => domain().ticks,
255
- xExtent: () => [0, 1],
256
- yExtent: () => domain().range,
292
+ fill: () => style().gridBackground,
293
+ xGridStroke: () => style().verticalGrid,
294
+ yGridStroke: () => style().horizontalGrid,
295
+ xMarkers: horizontal ? () => domain().ticks : boundaries,
296
+ yMarkers: horizontal ? boundaries : () => domain().ticks,
297
+ xExtent: horizontal ? () => domain().range : slots,
298
+ yExtent: horizontal ? slots : () => domain().range,
299
+ axisStroke: () => style().axisStroke,
300
+ xAxisAt: () => (horizontal ? 0 : baselineValue(domain())),
301
+ yAxisAt: () => (horizontal ? baselineValue(domain()) : 0),
257
302
  }),
258
303
  ...barSets,
259
- new ChartBorder({ stroke: () => theme().plotArea.stroke }),
260
304
  ],
261
305
  });
262
- const gutter = new VerticalAxis({
263
- opacity: 0,
264
- range: () => domain().range,
265
- markers: () => domain().ticks,
266
- axis: () => theme().yAxis,
267
- label: state.valueAxis.label ?? "",
268
- width: "hug",
269
- height: 0,
270
- });
271
- return asParts([cartesianFrame({ axis: vAxis, plot, gutter, below })]);
306
+ return asParts([
307
+ cartesianFrame({
308
+ background: () => style().background,
309
+ left,
310
+ ghost,
311
+ plot,
312
+ bottom,
313
+ legend,
314
+ legendAt,
315
+ gap: () => frameGap(style()),
316
+ padding: () => ({
317
+ top: headroom(style()),
318
+ right: horizontal && bottom ? overhang(style()) : 0,
319
+ left: horizontal && bottom && !left ? overhang(style()) : 0,
320
+ bottom: 0,
321
+ }),
322
+ }),
323
+ ]);
272
324
  },
273
325
  commands: {
274
- /** Axes stagger in, columns rise from the baseline, the legend fades up last. `duration` is the whole entrance. */
326
+ /** Axes stagger in, bars rise from the baseline, the legend fades up last. `duration` is the whole entrance. */
275
327
  enter: {
276
328
  args: {},
277
329
  // Held from arrival, so it never shows finished ahead of its draw.
278
330
  entrance: true,
279
331
  easing: easeOut(),
280
- run: (timeline, _state, children) => {
332
+ run: (timeline, state, children) => {
281
333
  const parts = partsOf(children);
282
- if (!parts)
283
- return timeline.wait(timeline.duration);
284
334
  const duration = timeline.duration;
285
335
  const axes = duration * ENTER_SHARE.axes;
286
336
  const rise = duration * ENTER_SHARE.marks;
@@ -288,12 +338,12 @@ export const BarChart = component2d({
288
338
  const at0 = (piece) => ({ at: 0, duration: 0, piece });
289
339
  return beats(timeline, [
290
340
  at0(timeline.animate(() => ({ growth: 0 }))),
291
- ...(legend ? [at0(legend.to({ opacity: 0, y: -LEGEND_SLIDE }))] : []),
292
- { at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
293
- { at: 0, duration: axes, piece: parts.cAxis.run("enter", { slide: 24 }) },
341
+ ...(legend ? [at0(legend.to({ opacity: 0, ...legendAway(state.style.legend) }))] : []),
342
+ ...(parts.valueAxis ? [{ at: 0, duration: axes, piece: parts.valueAxis.run("enter", { slide: 24 }) }] : []),
343
+ ...(parts.categoryAxis ? [{ at: 0, duration: axes, piece: parts.categoryAxis.run("enter", { slide: 24 }) }] : []),
294
344
  // No priming to 0 first: the value at `t` is a function of `t`.
295
345
  { at: axes, duration: rise, piece: timeline.animate((t) => ({ growth: t })) },
296
- ...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
346
+ ...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
297
347
  ]);
298
348
  },
299
349
  },
@@ -303,30 +353,28 @@ export const BarChart = component2d({
303
353
  easing: easeOut(),
304
354
  run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 1),
305
355
  },
306
- /** The mirror of `showSeries`: columns narrow away (stacked, sink out) while the rest widen into the space. */
356
+ /** The mirror of `showSeries`: bars narrow away (stacked, sink out) while the rest widen into the space. */
307
357
  hideSeries: {
308
358
  args: { seriesIndex: SERIES_ARG },
309
359
  easing: easeOut(),
310
360
  run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 0),
311
361
  },
312
- /** Fade every other series, columns and legend entry, to focus on `index`. Nothing moves, so the group does not reflow. */
362
+ /** Fade every other series, bars and legend entry, to focus on `index`. Nothing moves, so the group does not reflow. */
313
363
  spotlightSeries: {
314
364
  args: { seriesIndex: SERIES_ARG },
315
365
  easing: easeOut(),
316
- run: (timeline, state, children) => {
366
+ run: (timeline, _state, children) => {
317
367
  const parts = partsOf(children);
318
- if (!parts)
319
- return timeline.wait(timeline.duration);
320
368
  const index = timeline.args.seriesIndex;
321
- const dim = state.theme.spotlight.dimOpacity;
322
369
  return timeline.together([
323
- ...parts.bars.map((bars, i) => bars.to({ opacity: i === index ? 1 : dim })),
370
+ ...parts.bars.map((bars, i) => bars.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
324
371
  ...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
372
+ timeline.wait(timeline.duration),
325
373
  ]);
326
374
  },
327
375
  },
328
376
  /**
329
- * Fade every other category's columns and caption to `dimOpacity`. While
377
+ * Fade every other category's bars and caption to {@link SPOTLIGHT_DIM}. While
330
378
  * another is lit the focus moves directly. A missing category takes its slot
331
379
  * and changes nothing.
332
380
  */