@motionscript/charts 0.1.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 (215) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +98 -28
  3. package/dist/bar-chart/bar-chart.d.ts +242 -119
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +231 -260
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +16 -38
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/category-axis.js +59 -68
  10. package/dist/bar-chart/category-axis.js.map +1 -1
  11. package/dist/bar-chart/chart-bars.d.ts +60 -91
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +88 -125
  14. package/dist/bar-chart/chart-bars.js.map +1 -1
  15. package/dist/bar-chart/index.d.ts +5 -13
  16. package/dist/bar-chart/index.d.ts.map +1 -1
  17. package/dist/bar-chart/index.js +2 -9
  18. package/dist/bar-chart/index.js.map +1 -1
  19. package/dist/bar-chart/shared.d.ts +40 -132
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +70 -158
  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 +8 -17
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +8 -17
  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 -35
  38. package/dist/kit/cartesian.d.ts.map +1 -1
  39. package/dist/kit/cartesian.js +80 -42
  40. package/dist/kit/cartesian.js.map +1 -1
  41. package/dist/kit/chart-canvas.d.ts +37 -40
  42. package/dist/kit/chart-canvas.d.ts.map +1 -1
  43. package/dist/kit/chart-canvas.js +53 -47
  44. package/dist/kit/chart-canvas.js.map +1 -1
  45. package/dist/kit/chart-legend.d.ts +16 -59
  46. package/dist/kit/chart-legend.d.ts.map +1 -1
  47. package/dist/kit/chart-legend.js +38 -76
  48. package/dist/kit/chart-legend.js.map +1 -1
  49. package/dist/kit/entries.d.ts +2 -0
  50. package/dist/kit/entries.d.ts.map +1 -1
  51. package/dist/kit/entries.js +2 -0
  52. package/dist/kit/entries.js.map +1 -1
  53. package/dist/kit/horizontal-axis.d.ts +77 -0
  54. package/dist/kit/horizontal-axis.d.ts.map +1 -0
  55. package/dist/kit/horizontal-axis.js +99 -0
  56. package/dist/kit/horizontal-axis.js.map +1 -0
  57. package/dist/kit/motion.d.ts +16 -3
  58. package/dist/kit/motion.d.ts.map +1 -1
  59. package/dist/kit/motion.js +15 -1
  60. package/dist/kit/motion.js.map +1 -1
  61. package/dist/{pie-chart → kit}/pie-legend.d.ts +14 -29
  62. package/dist/kit/pie-legend.d.ts.map +1 -0
  63. package/dist/kit/pie-legend.js +93 -0
  64. package/dist/kit/pie-legend.js.map +1 -0
  65. package/dist/kit/polar.d.ts +31 -0
  66. package/dist/kit/polar.d.ts.map +1 -0
  67. package/dist/kit/polar.js +33 -0
  68. package/dist/kit/polar.js.map +1 -0
  69. package/dist/kit/shared.d.ts +60 -214
  70. package/dist/kit/shared.d.ts.map +1 -1
  71. package/dist/kit/shared.js +91 -291
  72. package/dist/kit/shared.js.map +1 -1
  73. package/dist/kit/style.d.ts +254 -0
  74. package/dist/kit/style.d.ts.map +1 -0
  75. package/dist/kit/style.js +197 -0
  76. package/dist/kit/style.js.map +1 -0
  77. package/dist/kit/vertical-axis.d.ts +36 -45
  78. package/dist/kit/vertical-axis.d.ts.map +1 -1
  79. package/dist/kit/vertical-axis.js +69 -81
  80. package/dist/kit/vertical-axis.js.map +1 -1
  81. package/dist/line-chart/chart-line.d.ts +107 -40
  82. package/dist/line-chart/chart-line.d.ts.map +1 -1
  83. package/dist/line-chart/chart-line.js +172 -89
  84. package/dist/line-chart/chart-line.js.map +1 -1
  85. package/dist/line-chart/chart-region.d.ts +14 -27
  86. package/dist/line-chart/chart-region.d.ts.map +1 -1
  87. package/dist/line-chart/chart-region.js +13 -28
  88. package/dist/line-chart/chart-region.js.map +1 -1
  89. package/dist/line-chart/index.d.ts +5 -18
  90. package/dist/line-chart/index.d.ts.map +1 -1
  91. package/dist/line-chart/index.js +3 -16
  92. package/dist/line-chart/index.js.map +1 -1
  93. package/dist/line-chart/line-chart.d.ts +272 -119
  94. package/dist/line-chart/line-chart.d.ts.map +1 -1
  95. package/dist/line-chart/line-chart.js +309 -257
  96. package/dist/line-chart/line-chart.js.map +1 -1
  97. package/dist/line-chart/shared.d.ts +73 -130
  98. package/dist/line-chart/shared.d.ts.map +1 -1
  99. package/dist/line-chart/shared.js +195 -156
  100. package/dist/line-chart/shared.js.map +1 -1
  101. package/dist/line-chart/x-scale.d.ts +16 -55
  102. package/dist/line-chart/x-scale.d.ts.map +1 -1
  103. package/dist/line-chart/x-scale.js +44 -111
  104. package/dist/line-chart/x-scale.js.map +1 -1
  105. package/dist/nodes.d.ts +3 -13
  106. package/dist/nodes.d.ts.map +1 -1
  107. package/dist/nodes.js +9 -13
  108. package/dist/nodes.js.map +1 -1
  109. package/dist/pie-chart/index.d.ts +3 -15
  110. package/dist/pie-chart/index.d.ts.map +1 -1
  111. package/dist/pie-chart/index.js +1 -11
  112. package/dist/pie-chart/index.js.map +1 -1
  113. package/dist/pie-chart/pie-chart.d.ts +147 -85
  114. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  115. package/dist/pie-chart/pie-chart.js +65 -86
  116. package/dist/pie-chart/pie-chart.js.map +1 -1
  117. package/dist/pie-chart/pie-ring.d.ts +65 -196
  118. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  119. package/dist/pie-chart/pie-ring.js +154 -292
  120. package/dist/pie-chart/pie-ring.js.map +1 -1
  121. package/dist/pie-chart/shared.d.ts +78 -172
  122. package/dist/pie-chart/shared.d.ts.map +1 -1
  123. package/dist/pie-chart/shared.js +54 -177
  124. package/dist/pie-chart/shared.js.map +1 -1
  125. package/dist/radar-chart/index.d.ts +5 -0
  126. package/dist/radar-chart/index.d.ts.map +1 -0
  127. package/dist/radar-chart/index.js +5 -0
  128. package/dist/radar-chart/index.js.map +1 -0
  129. package/dist/radar-chart/radar-chart.d.ts +218 -0
  130. package/dist/radar-chart/radar-chart.d.ts.map +1 -0
  131. package/dist/radar-chart/radar-chart.js +224 -0
  132. package/dist/radar-chart/radar-chart.js.map +1 -0
  133. package/dist/radar-chart/radar-grid.d.ts +43 -0
  134. package/dist/radar-chart/radar-grid.d.ts.map +1 -0
  135. package/dist/radar-chart/radar-grid.js +61 -0
  136. package/dist/radar-chart/radar-grid.js.map +1 -0
  137. package/dist/radar-chart/radar-series.d.ts +59 -0
  138. package/dist/radar-chart/radar-series.d.ts.map +1 -0
  139. package/dist/radar-chart/radar-series.js +71 -0
  140. package/dist/radar-chart/radar-series.js.map +1 -0
  141. package/dist/radar-chart/shared.d.ts +73 -0
  142. package/dist/radar-chart/shared.d.ts.map +1 -0
  143. package/dist/radar-chart/shared.js +123 -0
  144. package/dist/radar-chart/shared.js.map +1 -0
  145. package/dist/radial-chart/index.d.ts +6 -0
  146. package/dist/radial-chart/index.d.ts.map +1 -0
  147. package/dist/radial-chart/index.js +4 -0
  148. package/dist/radial-chart/index.js.map +1 -0
  149. package/dist/radial-chart/radial-chart.d.ts +216 -0
  150. package/dist/radial-chart/radial-chart.d.ts.map +1 -0
  151. package/dist/radial-chart/radial-chart.js +174 -0
  152. package/dist/radial-chart/radial-chart.js.map +1 -0
  153. package/dist/radial-chart/radial-rings.d.ts +121 -0
  154. package/dist/radial-chart/radial-rings.d.ts.map +1 -0
  155. package/dist/radial-chart/radial-rings.js +273 -0
  156. package/dist/radial-chart/radial-rings.js.map +1 -0
  157. package/dist/radial-chart/shared.d.ts +73 -0
  158. package/dist/radial-chart/shared.d.ts.map +1 -0
  159. package/dist/radial-chart/shared.js +79 -0
  160. package/dist/radial-chart/shared.js.map +1 -0
  161. package/package.json +4 -4
  162. package/registry.json +21 -6
  163. package/src/bar-chart/bar-chart.ts +274 -315
  164. package/src/bar-chart/category-axis.ts +53 -88
  165. package/src/bar-chart/chart-bars.ts +108 -205
  166. package/src/bar-chart/index.ts +8 -19
  167. package/src/bar-chart/shared.ts +81 -309
  168. package/src/engine.ts +8 -17
  169. package/src/index.ts +2 -0
  170. package/src/kit/cartesian.ts +118 -55
  171. package/src/kit/chart-canvas.ts +50 -80
  172. package/src/kit/chart-legend.ts +36 -97
  173. package/src/kit/entries.ts +4 -1
  174. package/src/kit/horizontal-axis.ts +114 -0
  175. package/src/kit/motion.ts +18 -2
  176. package/src/{pie-chart → kit}/pie-legend.ts +25 -65
  177. package/src/kit/polar.ts +54 -0
  178. package/src/kit/shared.ts +104 -487
  179. package/src/kit/style.ts +257 -0
  180. package/src/kit/vertical-axis.ts +81 -115
  181. package/src/line-chart/chart-line.ts +185 -91
  182. package/src/line-chart/chart-region.ts +14 -31
  183. package/src/line-chart/index.ts +14 -22
  184. package/src/line-chart/line-chart.ts +377 -315
  185. package/src/line-chart/shared.ts +229 -304
  186. package/src/line-chart/x-scale.ts +44 -121
  187. package/src/nodes.ts +9 -13
  188. package/src/pie-chart/index.ts +3 -35
  189. package/src/pie-chart/pie-chart.ts +83 -134
  190. package/src/pie-chart/pie-ring.ts +162 -368
  191. package/src/pie-chart/shared.ts +92 -379
  192. package/src/radar-chart/index.ts +4 -0
  193. package/src/radar-chart/radar-chart.ts +287 -0
  194. package/src/radar-chart/radar-grid.ts +78 -0
  195. package/src/radar-chart/radar-series.ts +88 -0
  196. package/src/radar-chart/shared.ts +146 -0
  197. package/src/radial-chart/index.ts +5 -0
  198. package/src/radial-chart/radial-chart.ts +210 -0
  199. package/src/radial-chart/radial-rings.ts +328 -0
  200. package/src/radial-chart/shared.ts +114 -0
  201. package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
  202. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
  203. package/dist/kit/chart-border.d.ts +0 -27
  204. package/dist/kit/chart-border.d.ts.map +0 -1
  205. package/dist/kit/chart-border.js +0 -28
  206. package/dist/kit/chart-border.js.map +0 -1
  207. package/dist/line-chart/horizontal-axis.d.ts +0 -79
  208. package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
  209. package/dist/line-chart/horizontal-axis.js +0 -129
  210. package/dist/line-chart/horizontal-axis.js.map +0 -1
  211. package/dist/pie-chart/pie-legend.d.ts.map +0 -1
  212. package/dist/pie-chart/pie-legend.js +0 -117
  213. package/dist/pie-chart/pie-legend.js.map +0 -1
  214. package/src/kit/chart-border.ts +0 -50
  215. package/src/line-chart/horizontal-axis.ts +0 -151
@@ -1,102 +1,84 @@
1
- import { Rect, Types, component2d, easeOut, } from "@motionscript/sdk";
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
- import { asParts, fresh } from "../kit/entries.js";
8
- import { beats } from "../kit/motion.js";
9
- import { VerticalAxis } from "../kit/vertical-axis.js";
10
- import { clampUnit, formatMarker, lerpAxisDomain, lerpAxisEntry, markerStep, markerValues, num, resolveAxisDomain, settleAxisDomain, } from "../kit/shared.js";
11
- import { CategoryAxis } from "./category-axis.js";
6
+ import { SERIES_ARG, asParts, fresh } from "../kit/entries.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, 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 { 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
- /** The argument every per-series command takes: an index into `series`, offered by name. */
15
- const SERIES_ARG = Types.custom(0, { kind: "series", min: 0, step: 1 });
16
- /** How far the legend starts below its slot when it enters, in px. */
17
- const LEGEND_SLIDE = 20;
18
- /** Fallback marker count when the value axis states no ticks. */
19
- const DEFAULT_MARKERS = 5;
20
- /**
21
- * How `enter` divides its duration between its three beats: the axes arriving,
22
- * the columns rising, and the legend coming up. Shares of the whole, so they
23
- * must sum to 1.
24
- */
25
- const AXES_SHARE = 0.35;
26
- const BARS_SHARE = 0.5;
27
- const LEGEND_SHARE = 0.15;
28
- /** The value axis with its range and ticks resolved: the explicit ones, else five across the scale. */
14
+ import { BAR_LABELS, BAR_MARKS, barCategories, barSpans, resolveBarScale, } from "./shared.js";
15
+ const BAR_STYLE = Types.style(CHART_STYLE_FAMILY, { colors: CHART_COLORS, text: CHART_TEXT, grid: CARTESIAN_GRID, marks: BAR_MARKS, labels: BAR_LABELS, legend: CHART_LEGEND }, { presets: CHART_STYLE_PRESETS });
29
16
  function drawnValue(s, domain) {
30
17
  const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1;
31
- const range = resolveBarScale(s.data, s.series, s.stacked, domain.range, divisions);
32
- 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)) };
33
21
  }
34
- /** The last domain worked out from each data array, and what else it was read from. */
22
+ /** The last domain worked out from each data array, and what it was read from. */
35
23
  const domainsDrawn = new WeakMap();
36
- /** The value axis as a bar chart draws it now, an auto range or tick row read back as itself. */
24
+ /** The value axis as drawn now. A series being hidden keeps its room, so the ticks hold still while it goes. */
37
25
  export function barChartDomain(s) {
38
- const { data, series, stacked, valueAxis } = s;
39
- const kept = domainsDrawn.get(data);
40
- 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]))
41
29
  return kept.domain;
42
- const domain = settleAxisDomain(valueAxis, (d) => drawnValue(s, d));
43
- domainsDrawn.set(data, { series, stacked, valueAxis, domain });
30
+ const domain = settleAxisDomain(s.valueAxis, (d) => drawnValue(s, d));
31
+ domainsDrawn.set(s.data, { key, domain });
44
32
  return domain;
45
33
  }
46
- /**
47
- * A value written down as this chart's value axis says: the ticks and the
48
- * figures printed on the columns both read through here, so the two cannot
49
- * disagree about what a number looks like.
50
- */
34
+ /** Ticks and the figures printed by bars both read through here, so they cannot disagree about how a number looks. */
51
35
  function valueLabel(s, domain, value) {
52
36
  const pattern = s.valueAxis.format;
53
- 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;
54
41
  }
55
- /** A value on the plot's **centred** pixel space (origin at its centre, y-up). */
56
- 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) {
57
44
  const [min, max] = domain.range;
58
- if (max <= min)
59
- return -plot.height / 2;
60
- 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;
61
48
  }
62
- /**
63
- * Pixel y of the line every column grows from: zero clamped onto the scale, so
64
- * a cropped axis grows its columns from the plot floor.
65
- */
66
- function baselinePx(domain, plot) {
67
- const [min, max] = domain.range;
68
- 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));
69
53
  }
70
- /** Series `i`'s live share of the slot; 0 once it has been hidden. */
71
54
  function weightAt(s, index) {
72
55
  return Math.max(0, s.weights[index] ?? 1);
73
56
  }
74
- /** `row` with each series' value scaled by its live weight: what the stacked spans are measured from. */
75
- function weightedRow(s, row) {
76
- const scaled = { ...row };
77
- s.series.forEach((series, i) => {
78
- const value = row[series.field];
79
- if (typeof value === "number")
80
- scaled[series.field] = value * weightAt(s, i);
81
- });
82
- 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;
83
65
  }
84
66
  /**
85
- * Series `index`'s columns, mapped into the plot's centred pixel space. A
86
- * grouped series divides the slot with its siblings by their live weights, a
87
- * stacked one takes the whole slot and runs from the running total below it,
88
- * so 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.
89
71
  */
90
- function seriesBars(s, index, plot, captioned) {
72
+ function seriesBars(s, index, plot) {
91
73
  const series = s.series[index];
92
74
  if (!series)
93
75
  return [];
94
76
  const domain = barChartDomain(s);
77
+ const { along, across } = runs(s, plot);
95
78
  const n = s.data.length;
96
- const slotWidth = n > 0 ? plot.width / n : 0;
97
- const slotCenter = (i) => -plot.width / 2 + slotWidth * (i + 0.5);
98
- const inner = slotWidth * clampUnit(s.theme.bar.slotFill);
99
- 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";
100
82
  let total = 0;
101
83
  for (let i = 0; i < s.series.length; i++)
102
84
  total += weightAt(s, i);
@@ -105,66 +87,56 @@ function seriesBars(s, index, plot, captioned) {
105
87
  let before = 0;
106
88
  for (let i = 0; i < index; i++)
107
89
  before += weightAt(s, i);
90
+ const gap = s.series.length > 1 ? s.style.barGap : 0;
91
+ const values = s.style.values;
108
92
  return s.data.map((row, rowIndex) => {
109
- // The stack is built from the *live* contributions, so a series fading out
110
- // lowers the segments above it as it goes.
111
- const spans = barSpans(s.stacked ? weightedRow(s, row) : row, s.series, s.stacked);
112
- const span = spans[index] ?? { from: 0, to: 0, value: 0 };
113
- const cap = s.stacked ? stackCapIndex(row, s.series) : index;
114
- const slotLeft = slotCenter(rowIndex) - inner / 2;
115
- const x = s.stacked ? slotCenter(rowIndex) : slotLeft + unit * before + (unit * weight) / 2;
116
- const width = s.stacked ? inner : Math.max(0, unit * weight - (s.series.length > 1 ? gap : 0));
117
- // The figure printed is the row's own value, not the weighted one.
118
- 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
+ }
119
105
  return {
120
- x,
121
- width,
122
- from: valueToY(domain, plot, span.from),
123
- to: valueToY(domain, plot, span.to),
124
- round: cap === index,
125
- value,
126
- ...(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
+ caption,
127
112
  };
128
113
  });
129
114
  }
130
- /**
131
- * How far each category is dimmed by the spotlight, one entry per row: 1 while
132
- * nothing is spotlit, falling to the theme's `dimOpacity` for every category but
133
- * the chosen one as the reveal climbs.
134
- */
115
+ /** Per-row opacity: 1 while nothing is spotlit, falling to {@link SPOTLIGHT_DIM} for all but the chosen category. */
135
116
  function categoryDims(s) {
136
- const focus = clampUnit(s.spotlightReveal);
137
- const dim = s.theme.spotlight.dimOpacity;
117
+ const focus = clamp(s.spotlightReveal, 0, 1);
138
118
  const chosen = s.spotlight;
139
- return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - dim) * focus));
119
+ return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - SPOTLIGHT_DIM) * focus));
140
120
  }
141
121
  /** The parts a bar chart's commands drive, found through its children. */
142
122
  function partsOf(children) {
143
- const frame = cartesianViews(children);
144
- if (!frame)
145
- return null;
146
- const cAxis = frame.below.find(CategoryAxis.is);
147
- if (!VerticalAxis.is(frame.axis) || !cAxis)
148
- return null;
123
+ const views = chartViews(children);
149
124
  return {
150
- vAxis: frame.axis,
151
- cAxis,
152
- bars: frame.plot.filter(ChartBars.is),
153
- legend: frame.below.find(ChartLegend.is) ?? null,
125
+ valueAxis: views.find((view) => VerticalAxis.is(view) || HorizontalAxis.is(view)) ?? null,
126
+ categoryAxis: views.find(CategoryAxis.is) ?? null,
127
+ bars: views.filter(ChartBars.is),
128
+ legend: views.find(ChartLegend.is) ?? null,
154
129
  };
155
130
  }
156
131
  /**
157
- * Move series `index`'s slot share to `target` with its legend entry, the group
158
- * reflowing. Its columns are drawn for every frame the move covers, and switch
159
- * off only once a hide has landed, so arriving at a time backwards shows what
160
- * arriving forwards does.
132
+ * Move series `index`'s share to `target` with its legend entry. Its bars stay
133
+ * drawn for every frame the move covers and switch off only once a hide has
134
+ * landed, so arriving at a time backwards matches arriving forwards.
161
135
  */
162
136
  function toggling(timeline, state, children, index, target) {
163
137
  const parts = partsOf(children);
164
- if (!parts || !parts.bars[index])
138
+ if (!parts.bars[index])
165
139
  return timeline.wait(timeline.duration);
166
- // Read through `weightAt`, so a chart whose stored weights are short still
167
- // tweens from the 1 it is drawing at.
168
140
  const from = state.series.map((_, i) => weightAt(state, i));
169
141
  const start = from[index] ?? 1;
170
142
  const showing = state.series.map((s, i) => state.showing[i] ?? (s.enabled ?? true));
@@ -180,61 +152,42 @@ function toggling(timeline, state, children, index, target) {
180
152
  ]);
181
153
  }
182
154
  /**
183
- * A grouped or stacked bar chart, the sibling of the line chart and built the
184
- * same way, out of several of the same pieces:
185
- *
186
- * - `ChartCanvas` — the plot panel with a horizontal grid, under everything.
187
- * - `VerticalAxis` — the numeric value markers, staggering in bottom-to-top.
188
- * - {@link CategoryAxis} — the row of names beneath the baseline, one per row.
189
- * - {@link ChartBars} (one per series) — that series' column in every category,
190
- * plus the figures printed on them.
191
- * - `ChartLegend` — a row of entries below the chart, one per series.
155
+ * A bar chart, vertical or horizontal, its series side by side, stacked, or
156
+ * stacked as shares of 100. Built like the line chart from `ChartCanvas`, a
157
+ * value axis, a {@link CategoryAxis}, one {@link ChartBars} per series and
158
+ * `ChartLegend`. Zero is always on the scale and bars grow about it, so a
159
+ * negative value draws the other way from the same `growth` signal.
192
160
  *
193
- * Every part reads the chart's entries through bindings, and the columns map
194
- * across the plot panel's own laid-out box, so a column's end lands exactly on
195
- * its marker. **Zero is always on the scale**, and the columns grow about it, so
196
- * a negative value draws downward out of the same `growth` signal.
197
- *
198
- * **Resting state is the finished chart**; `enter` arms every part on its first
199
- * frame. `showSeries`/`hideSeries`/`spotlightSeries` pick out a *series* (hiding
200
- * one reflows the group), `spotlightCategory`/`clearCategory` a *column of the
201
- * table*. A `to` of the value axis's `range` or `ticks` tweens from the domain
202
- * as drawn, so an auto axis rescales the columns continuously.
161
+ * Resting state is the finished chart; `enter` arms every part on its first
162
+ * frame. A `to` of the value axis's `range` or `ticks` tweens from the domain as
163
+ * drawn, so an auto axis rescales the bars continuously.
203
164
  */
204
165
  export const BarChart = component2d({
205
166
  key: "barChart",
206
167
  state: {
207
- data: fresh(() => []),
168
+ data: fresh(() => [], { kind: "dataset" }),
169
+ // Names a column, not a value, like the line chart's `xField`.
208
170
  categoryField: "",
209
- series: fresh(() => []),
210
- stacked: false,
171
+ series: fresh(() => [], { kind: "series" }),
172
+ /** `vertical`: columns rise from a row of categories; `horizontal`: bars grow right from a column of them. */
173
+ orientation: Types.oneOf(["vertical", "horizontal"], { animatable: false }),
174
+ /** `stacked`: each series sits on the sum of those before it; `percent`: every category reads as shares of 100. */
175
+ stack: Types.oneOf(["none", "stacked", "percent"]),
211
176
  valueAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
212
177
  categoryAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisDomain }),
213
- theme: Types.custom(DEFAULT_BAR_CHART_THEME, { resolve: resolveBarChartTheme, lerp: lerpBarChartTheme }),
214
- /**
215
- * Global rise of every column in `[0, 1]`, measured about the baseline: 0
216
- * collapses them all onto it, 1 draws them at full length.
217
- */
178
+ style: BAR_STYLE,
179
+ /** Rise of every bar about the baseline, `[0, 1]`: 0 collapses them onto it. */
218
180
  growth: 1,
219
- /**
220
- * How much of the slot each series claims, one entry per series: a weight
221
- * rather than a switch, so hiding one is the others widening into the space.
222
- * Seeded from each series' `enabled`.
223
- */
224
- weights: fresh(() => []),
225
- /** The category spotlit, or `null` for none. The choice snaps; `spotlightReveal` is the animation. */
181
+ /** The category spotlit, or `null`. The choice snaps; `spotlightReveal` animates it. */
226
182
  spotlight: Types.custom(null),
227
- /** How far the category spotlight is drawn in, `[0, 1]`. */
228
183
  spotlightReveal: 0,
229
- /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
230
- showLegend: Types.boolean(true, { animatable: false }),
231
- /** Read as the chart is built: whether each column prints the figure it plots. */
232
- showValues: Types.boolean(false, { animatable: false }),
233
- /** Whether each series' columns are drawn: on for every frame a show or hide covers, off once a hide lands. */
184
+ /** Each series' share of the slot, seeded from `enabled`: a weight rather than a switch, so hiding one widens the others. */
185
+ weights: Types.custom([], { private: true }),
186
+ /** Whether each series' bars are drawn: on for every frame a show or hide covers, off once a hide lands. */
234
187
  showing: Types.custom([], { private: true }),
235
188
  },
236
189
  defaults: {
237
- // Its parts fill it; a node made of them would hug them.
190
+ // A node made of parts that fill would otherwise hug them.
238
191
  width: "fill",
239
192
  height: "fill",
240
193
  weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
@@ -242,162 +195,184 @@ export const BarChart = component2d({
242
195
  },
243
196
  allowedChildren: [],
244
197
  children: (state) => {
245
- const theme = () => state.theme;
198
+ const style = () => state.style;
246
199
  const domain = () => barChartDomain(state);
247
200
  let plot;
248
- // Before the panel is built or laid out it has no size, and the columns map onto nothing.
249
201
  const plotSize = () => laidOutSize(plot);
250
- const captioned = state.showValues === true;
251
- const vAxis = new VerticalAxis({
252
- range: () => domain().range,
253
- markers: () => domain().ticks,
254
- captions: () => {
255
- const d = domain();
256
- return d.ticks.map((value) => valueLabel(state, d, value));
257
- },
258
- axis: () => theme().yAxis,
259
- label: state.valueAxis.label ?? "",
260
- width: "hug",
261
- height: "fill",
262
- });
263
- const cAxis = new CategoryAxis({
264
- categories: barCategories(state.data, state.categoryField),
265
- axis: () => theme().xAxis,
266
- label: state.categoryAxis.label ?? "",
267
- dims: () => categoryDims(state),
268
- width: "fill",
269
- height: "hug",
270
- });
202
+ const built = state.style;
203
+ const horizontal = state.orientation === "horizontal";
204
+ const categories = barCategories(state.data, state.categoryField);
205
+ const valueConfig = () => axisConfigOf(style(), horizontal ? "x" : "y");
206
+ const categoryConfig = () => axisConfigOf(style(), horizontal ? "y" : "x");
207
+ const valueCaptions = () => {
208
+ const d = domain();
209
+ return d.ticks.map((value) => valueLabel(state, d, value));
210
+ };
211
+ const showValue = horizontal ? built.showXAxis : built.showYAxis;
212
+ const showCategory = horizontal ? built.showYAxis : built.showXAxis;
213
+ const valueAxis = showValue
214
+ ? (horizontal
215
+ ? new HorizontalAxis({
216
+ range: () => domain().range,
217
+ markers: () => domain().ticks,
218
+ captions: valueCaptions,
219
+ axis: valueConfig,
220
+ label: state.valueAxis.label ?? "",
221
+ overhang: () => ({ left: overhang(style()), right: overhang(style()) }),
222
+ width: "fill",
223
+ height: "hug",
224
+ })
225
+ : new VerticalAxis({
226
+ range: () => domain().range,
227
+ markers: () => domain().ticks,
228
+ captions: valueCaptions,
229
+ axis: valueConfig,
230
+ label: state.valueAxis.label ?? "",
231
+ width: "hug",
232
+ height: "fill",
233
+ }))
234
+ : undefined;
235
+ const categoryAxis = showCategory
236
+ ? new CategoryAxis({
237
+ categories,
238
+ axis: categoryConfig,
239
+ label: state.categoryAxis.label ?? "",
240
+ orientation: horizontal ? "vertical" : "horizontal",
241
+ dims: () => categoryDims(state),
242
+ width: horizontal ? "hug" : "fill",
243
+ height: horizontal ? "fill" : "hug",
244
+ })
245
+ : undefined;
246
+ const left = horizontal ? categoryAxis : valueAxis;
247
+ const bottom = horizontal ? valueAxis : categoryAxis;
248
+ const leftConfig = horizontal ? categoryConfig : valueConfig;
249
+ const ghost = left && bottom
250
+ ? columnGhost({
251
+ captions: horizontal ? () => categories : valueCaptions,
252
+ count: horizontal ? categories.length : barChartDomain(state).ticks.length,
253
+ text: () => leftConfig().labelStyle,
254
+ title: (horizontal ? state.categoryAxis.label : state.valueAxis.label) ?? "",
255
+ titleText: () => leftConfig().titleStyle,
256
+ tickPad: () => axisPads(leftConfig(), "right").tick,
257
+ titlePad: () => axisPads(leftConfig(), "right").title,
258
+ })
259
+ : undefined;
271
260
  const barSets = state.series.map((s, i) => new ChartBars({
272
- bars: () => seriesBars(state, i, plotSize(), captioned),
273
- baseline: () => baselinePx(domain(), plotSize()),
274
- fill: () => resolveBarFill(theme().seriesFills, s, i),
275
- barStroke: () => theme().bar.stroke,
276
- barShadow: () => theme().bar.shadow,
277
- cornerRadius: () => theme().bar.cornerRadius,
261
+ bars: () => seriesBars(state, i, plotSize()),
262
+ baseline: () => valueToPx(domain(), runs(state, plotSize()).along, baselineValue(domain())),
263
+ horizontal,
264
+ fill: () => seriesFill(style(), s, i),
265
+ cornerRadius: () => style().barRadius,
278
266
  growth: () => state.growth,
279
267
  dims: () => categoryDims(state),
280
- showValues: captioned,
281
- valueStyle: state.theme.valueStyle,
268
+ // A figure past a stack's end prints the stack's total; a percent stack has none worth printing, so prints its shares inside.
269
+ values: () => (built.values === "end" && state.stack === "percent" ? "inside" : built.values),
270
+ valueStyle: () => ({ ...valueText(style()), fontSize: Math.round(style().fontSize * 0.85) }),
282
271
  enabled: () => state.showing[i] ?? (s.enabled ?? true),
283
272
  }));
284
- const below = [cAxis];
285
- if (state.showLegend !== false) {
286
- below.push(new ChartLegend({
273
+ const legendAt = built.legend;
274
+ const legend = legendAt !== "none"
275
+ ? new ChartLegend({
287
276
  series: state.series,
288
- swatches: () => state.series.map((s, i) => resolveBarFill(theme().seriesFills, s, i)),
289
- legend: () => theme().legend,
290
- // The row's spacing is the theme's as the chart was built, like the
291
- // entries' type and chips: a theme tween re-paints the swatches only.
292
- gap: state.theme.legend.itemGap,
293
- padding: state.theme.legend.padding,
294
- }));
295
- }
277
+ swatches: () => state.series.map((s, i) => seriesFill(style(), s, i)),
278
+ legend: () => legendConfigOf(style()),
279
+ flow: legendAt === "right" ? "vertical" : "horizontal",
280
+ })
281
+ : undefined;
282
+ const n = state.data.length;
283
+ // Category lines fall between the slots, never through a bar.
284
+ const boundaries = () => Array.from({ length: Math.max(0, n - 1) }, (_, i) => i + 1);
285
+ const slots = () => [0, Math.max(1, n)];
296
286
  plot = new Rect({
297
- clip: true,
298
287
  children: [
299
288
  new ChartCanvas({
300
- fill: () => theme().plotArea.fill,
301
- shadow: () => theme().plotArea.shadow,
302
- xGridStroke: () => theme().xAxis.lineStyle,
303
- yGridStroke: () => theme().yAxis.lineStyle,
304
- // No vertical rules: the horizontal axis is categorical, so there is no
305
- // numeric position along it for a grid line to mark.
306
- xMarkers: [],
307
- yMarkers: () => domain().ticks,
308
- xExtent: () => [0, 1],
309
- yExtent: () => domain().range,
289
+ fill: () => style().background,
290
+ xGridStroke: () => style().verticalGrid,
291
+ yGridStroke: () => style().horizontalGrid,
292
+ xMarkers: horizontal ? () => domain().ticks : boundaries,
293
+ yMarkers: horizontal ? boundaries : () => domain().ticks,
294
+ xExtent: horizontal ? () => domain().range : slots,
295
+ yExtent: horizontal ? slots : () => domain().range,
296
+ axisStroke: () => style().axisStroke,
297
+ xAxisAt: () => (horizontal ? 0 : baselineValue(domain())),
298
+ yAxisAt: () => (horizontal ? baselineValue(domain()) : 0),
310
299
  }),
311
300
  ...barSets,
312
- new ChartBorder({ stroke: () => theme().plotArea.stroke }),
313
301
  ],
314
302
  });
315
- const gutter = new VerticalAxis({
316
- opacity: 0,
317
- range: () => domain().range,
318
- markers: () => domain().ticks,
319
- axis: () => theme().yAxis,
320
- label: state.valueAxis.label ?? "",
321
- width: "hug",
322
- height: 0,
323
- });
324
- return asParts([cartesianFrame({ axis: vAxis, plot, gutter, below })]);
303
+ return asParts([
304
+ cartesianFrame({
305
+ left,
306
+ ghost,
307
+ plot,
308
+ bottom,
309
+ legend,
310
+ legendAt,
311
+ gap: () => frameGap(style()),
312
+ padding: () => ({
313
+ top: headroom(style()),
314
+ right: horizontal && bottom ? overhang(style()) : 0,
315
+ left: horizontal && bottom && !left ? overhang(style()) : 0,
316
+ bottom: 0,
317
+ }),
318
+ }),
319
+ ]);
325
320
  },
326
321
  commands: {
327
- /**
328
- * Animate the whole chart in: the two axes stagger their markers and
329
- * captions in, the columns rise from the baseline, and the legend fades up
330
- * last. Its first frame arms every part: markers and captions shifted and
331
- * transparent, columns collapsed, the legend dropped and transparent.
332
- * `duration` is the **whole** entrance; the beats are shares of it.
333
- */
322
+ /** Axes stagger in, bars rise from the baseline, the legend fades up last. `duration` is the whole entrance. */
334
323
  enter: {
335
324
  args: {},
336
- // Held from the chart's arrival, so it never shows finished ahead of its draw.
325
+ // Held from arrival, so it never shows finished ahead of its draw.
337
326
  entrance: true,
338
327
  easing: easeOut(),
339
- run: (timeline, _state, children) => {
328
+ run: (timeline, state, children) => {
340
329
  const parts = partsOf(children);
341
- if (!parts)
342
- return timeline.wait(timeline.duration);
343
330
  const duration = timeline.duration;
344
- const axes = duration * AXES_SHARE;
345
- const rise = duration * BARS_SHARE;
331
+ const axes = duration * ENTER_SHARE.axes;
332
+ const rise = duration * ENTER_SHARE.marks;
346
333
  const { legend } = parts;
347
334
  const at0 = (piece) => ({ at: 0, duration: 0, piece });
348
- // Without a legend the beat is still its share of the whole.
349
335
  return beats(timeline, [
350
336
  at0(timeline.animate(() => ({ growth: 0 }))),
351
- ...(legend ? [at0(legend.to({ opacity: 0, y: -LEGEND_SLIDE }))] : []),
352
- { at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
353
- { at: 0, duration: axes, piece: parts.cAxis.run("enter", { slide: 24 }) },
337
+ ...(legend ? [at0(legend.to({ opacity: 0, ...legendAway(state.style.legend) }))] : []),
338
+ ...(parts.valueAxis ? [{ at: 0, duration: axes, piece: parts.valueAxis.run("enter", { slide: 24 }) }] : []),
339
+ ...(parts.categoryAxis ? [{ at: 0, duration: axes, piece: parts.categoryAxis.run("enter", { slide: 24 }) }] : []),
354
340
  // No priming to 0 first: the value at `t` is a function of `t`.
355
341
  { at: axes, duration: rise, piece: timeline.animate((t) => ({ growth: t })) },
356
- ...(legend ? [{ at: axes + rise, duration: duration * LEGEND_SHARE, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
342
+ ...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
357
343
  ]);
358
344
  },
359
345
  },
360
- /** Show series `index`: its columns come back, the group reflowing to make room, with its legend entry. */
346
+ /** Bring series `index` back, the group reflowing to make room, with its legend entry. */
361
347
  showSeries: {
362
348
  args: { seriesIndex: SERIES_ARG },
363
349
  easing: easeOut(),
364
350
  run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 1),
365
351
  },
366
- /**
367
- * Hide series `index`: its columns narrow away (or, stacked, sink out of the
368
- * stack) while the rest of the group widens into the space — the mirror of
369
- * `showSeries`.
370
- */
352
+ /** The mirror of `showSeries`: bars narrow away (stacked, sink out) while the rest widen into the space. */
371
353
  hideSeries: {
372
354
  args: { seriesIndex: SERIES_ARG },
373
355
  easing: easeOut(),
374
356
  run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 0),
375
357
  },
376
- /**
377
- * Fade every other series — its columns **and its legend entry** — to focus
378
- * on `index`, which comes back to full. Nothing moves, so the group doesn't
379
- * reflow.
380
- */
358
+ /** Fade every other series, bars and legend entry, to focus on `index`. Nothing moves, so the group does not reflow. */
381
359
  spotlightSeries: {
382
360
  args: { seriesIndex: SERIES_ARG },
383
361
  easing: easeOut(),
384
- run: (timeline, state, children) => {
362
+ run: (timeline, _state, children) => {
385
363
  const parts = partsOf(children);
386
- if (!parts)
387
- return timeline.wait(timeline.duration);
388
364
  const index = timeline.args.seriesIndex;
389
- const dim = state.theme.spotlight.dimOpacity;
390
365
  return timeline.together([
391
- ...parts.bars.map((bars, i) => bars.to({ opacity: i === index ? 1 : dim })),
366
+ ...parts.bars.map((bars, i) => bars.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
392
367
  ...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
368
+ timeline.wait(timeline.duration),
393
369
  ]);
394
370
  },
395
371
  },
396
372
  /**
397
- * Spotlight one **category**: every other category's columns and caption
398
- * fade to the theme's `dimOpacity`. Called while another is lit, the focus
399
- * moves directly: only the dimming changes, so nothing retracts first. A
400
- * category that is not there takes its slot and changes nothing.
373
+ * Fade every other category's bars and caption to {@link SPOTLIGHT_DIM}. While
374
+ * another is lit the focus moves directly. A missing category takes its slot
375
+ * and changes nothing.
401
376
  */
402
377
  spotlightCategory: {
403
378
  args: { categoryIndex: Types.number(0, { min: 0, step: 1 }) },
@@ -410,11 +385,7 @@ export const BarChart = component2d({
410
385
  return timeline.animate((t) => ({ spotlight: index, spotlightReveal: from + (1 - from) * t }));
411
386
  },
412
387
  },
413
- /**
414
- * Drop the category spotlight, every category back to full. The lit
415
- * category is held until the fade lands; a no-op when nothing is lit, which
416
- * still takes its slot.
417
- */
388
+ /** Every category back to full; the lit one is held until the fade lands. A no-op when nothing is lit, which still takes its slot. */
418
389
  clearCategory: {
419
390
  args: {},
420
391
  easing: easeOut(),