@motionscript/charts 0.1.0-alpha.5 → 0.1.0-alpha.7

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 (146) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +30 -16
  3. package/dist/bar-chart/bar-chart.d.ts +133 -242
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +378 -569
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +35 -41
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/category-axis.js +93 -115
  10. package/dist/bar-chart/category-axis.js.map +1 -1
  11. package/dist/bar-chart/chart-bars.d.ts +50 -70
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +156 -210
  14. package/dist/bar-chart/chart-bars.js.map +1 -1
  15. package/dist/bar-chart/index.d.ts +2 -2
  16. package/dist/bar-chart/index.d.ts.map +1 -1
  17. package/dist/bar-chart/index.js +1 -1
  18. package/dist/bar-chart/index.js.map +1 -1
  19. package/dist/bar-chart/shared.d.ts +3 -3
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +3 -3
  22. package/dist/bar-chart/shared.js.map +1 -1
  23. package/dist/browser/chunks/chunk-XROBIRQK.js +2 -0
  24. package/dist/browser/chunks/chunk-XROBIRQK.js.map +7 -0
  25. package/dist/browser/index.js +1 -1
  26. package/dist/browser/index.js.map +3 -3
  27. package/dist/browser/kit.js +1 -1
  28. package/dist/browser/manifest.json +1 -1
  29. package/dist/engine.d.ts +8 -6
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +8 -6
  32. package/dist/engine.js.map +1 -1
  33. package/dist/kit/cartesian.d.ts +44 -0
  34. package/dist/kit/cartesian.d.ts.map +1 -0
  35. package/dist/kit/cartesian.js +59 -0
  36. package/dist/kit/cartesian.js.map +1 -0
  37. package/dist/kit/chart-border.d.ts +10 -11
  38. package/dist/kit/chart-border.d.ts.map +1 -1
  39. package/dist/kit/chart-border.js +15 -32
  40. package/dist/kit/chart-border.js.map +1 -1
  41. package/dist/kit/chart-canvas.d.ts +32 -21
  42. package/dist/kit/chart-canvas.d.ts.map +1 -1
  43. package/dist/kit/chart-canvas.js +33 -90
  44. package/dist/kit/chart-canvas.js.map +1 -1
  45. package/dist/kit/chart-legend.d.ts +123 -64
  46. package/dist/kit/chart-legend.d.ts.map +1 -1
  47. package/dist/kit/chart-legend.js +129 -154
  48. package/dist/kit/chart-legend.js.map +1 -1
  49. package/dist/kit/entries.d.ts +17 -0
  50. package/dist/kit/entries.d.ts.map +1 -0
  51. package/dist/kit/entries.js +19 -0
  52. package/dist/kit/entries.js.map +1 -0
  53. package/dist/kit/motion.d.ts +48 -0
  54. package/dist/kit/motion.d.ts.map +1 -0
  55. package/dist/kit/motion.js +63 -0
  56. package/dist/kit/motion.js.map +1 -0
  57. package/dist/kit/paints.d.ts +10 -0
  58. package/dist/kit/paints.d.ts.map +1 -0
  59. package/dist/kit/paints.js +12 -0
  60. package/dist/kit/paints.js.map +1 -0
  61. package/dist/kit/shared.d.ts +31 -3
  62. package/dist/kit/shared.d.ts.map +1 -1
  63. package/dist/kit/shared.js +42 -3
  64. package/dist/kit/shared.js.map +1 -1
  65. package/dist/kit/vertical-axis.d.ts +48 -61
  66. package/dist/kit/vertical-axis.d.ts.map +1 -1
  67. package/dist/kit/vertical-axis.js +97 -132
  68. package/dist/kit/vertical-axis.js.map +1 -1
  69. package/dist/line-chart/chart-line.d.ts +60 -77
  70. package/dist/line-chart/chart-line.d.ts.map +1 -1
  71. package/dist/line-chart/chart-line.js +100 -121
  72. package/dist/line-chart/chart-line.js.map +1 -1
  73. package/dist/line-chart/chart-region.d.ts +32 -33
  74. package/dist/line-chart/chart-region.d.ts.map +1 -1
  75. package/dist/line-chart/chart-region.js +31 -65
  76. package/dist/line-chart/chart-region.js.map +1 -1
  77. package/dist/line-chart/horizontal-axis.d.ts +58 -64
  78. package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
  79. package/dist/line-chart/horizontal-axis.js +94 -123
  80. package/dist/line-chart/horizontal-axis.js.map +1 -1
  81. package/dist/line-chart/index.d.ts +2 -2
  82. package/dist/line-chart/index.d.ts.map +1 -1
  83. package/dist/line-chart/index.js +1 -1
  84. package/dist/line-chart/index.js.map +1 -1
  85. package/dist/line-chart/line-chart.d.ts +156 -319
  86. package/dist/line-chart/line-chart.d.ts.map +1 -1
  87. package/dist/line-chart/line-chart.js +403 -663
  88. package/dist/line-chart/line-chart.js.map +1 -1
  89. package/dist/line-chart/shared.d.ts +3 -3
  90. package/dist/line-chart/shared.d.ts.map +1 -1
  91. package/dist/line-chart/shared.js +3 -3
  92. package/dist/line-chart/shared.js.map +1 -1
  93. package/dist/nodes.d.ts +6 -9
  94. package/dist/nodes.d.ts.map +1 -1
  95. package/dist/nodes.js +7 -9
  96. package/dist/nodes.js.map +1 -1
  97. package/dist/pie-chart/index.d.ts +2 -2
  98. package/dist/pie-chart/index.d.ts.map +1 -1
  99. package/dist/pie-chart/index.js +1 -1
  100. package/dist/pie-chart/index.js.map +1 -1
  101. package/dist/pie-chart/pie-chart.d.ts +108 -132
  102. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  103. package/dist/pie-chart/pie-chart.js +200 -271
  104. package/dist/pie-chart/pie-chart.js.map +1 -1
  105. package/dist/pie-chart/pie-legend.d.ts +84 -31
  106. package/dist/pie-chart/pie-legend.d.ts.map +1 -1
  107. package/dist/pie-chart/pie-legend.js +93 -107
  108. package/dist/pie-chart/pie-legend.js.map +1 -1
  109. package/dist/pie-chart/pie-ring.d.ts +160 -131
  110. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  111. package/dist/pie-chart/pie-ring.js +333 -391
  112. package/dist/pie-chart/pie-ring.js.map +1 -1
  113. package/dist/pie-chart/shared.d.ts +2 -2
  114. package/dist/pie-chart/shared.d.ts.map +1 -1
  115. package/dist/pie-chart/shared.js +2 -2
  116. package/dist/pie-chart/shared.js.map +1 -1
  117. package/package.json +4 -4
  118. package/src/bar-chart/bar-chart.ts +415 -701
  119. package/src/bar-chart/category-axis.ts +95 -110
  120. package/src/bar-chart/chart-bars.ts +178 -200
  121. package/src/bar-chart/index.ts +2 -2
  122. package/src/bar-chart/shared.ts +3 -3
  123. package/src/engine.ts +8 -6
  124. package/src/kit/cartesian.ts +81 -0
  125. package/src/kit/chart-border.ts +22 -33
  126. package/src/kit/chart-canvas.ts +44 -79
  127. package/src/kit/chart-legend.ts +173 -189
  128. package/src/kit/entries.ts +20 -0
  129. package/src/kit/motion.ts +90 -0
  130. package/src/kit/paints.ts +15 -0
  131. package/src/kit/shared.ts +52 -3
  132. package/src/kit/vertical-axis.ts +110 -140
  133. package/src/line-chart/chart-line.ts +102 -154
  134. package/src/line-chart/chart-region.ts +35 -80
  135. package/src/line-chart/horizontal-axis.ts +101 -152
  136. package/src/line-chart/index.ts +2 -2
  137. package/src/line-chart/line-chart.ts +449 -825
  138. package/src/line-chart/shared.ts +3 -3
  139. package/src/nodes.ts +8 -10
  140. package/src/pie-chart/index.ts +2 -2
  141. package/src/pie-chart/pie-chart.ts +221 -311
  142. package/src/pie-chart/pie-legend.ts +124 -114
  143. package/src/pie-chart/pie-ring.ts +386 -473
  144. package/src/pie-chart/shared.ts +2 -2
  145. package/dist/browser/chunks/chunk-5ZPBV6BU.js +0 -2
  146. package/dist/browser/chunks/chunk-5ZPBV6BU.js.map +0 -7
@@ -1,614 +1,423 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- var BarChart_1;
8
- import { command, node, Node2D, Rect, easeOut, property, } from "@motionscript/sdk";
1
+ import { Rect, Types, component2d, easeOut, } from "@motionscript/sdk";
2
+ import { formatNumber } from "@motionscript/sdk/component";
3
+ import { cartesianFrame, cartesianViews, laidOutSize } from "../kit/cartesian.js";
9
4
  import { ChartBorder } from "../kit/chart-border.js";
10
5
  import { ChartCanvas } from "../kit/chart-canvas.js";
11
6
  import { ChartLegend } from "../kit/chart-legend.js";
7
+ import { asParts, fresh } from "../kit/entries.js";
8
+ import { beats } from "../kit/motion.js";
12
9
  import { VerticalAxis } from "../kit/vertical-axis.js";
13
- import { clampUnit, formatMarker, markerStep, axisTweenTarget, lerpAxisDomain, markerValues, num, resolveAxisDomain, } from "../kit/shared.js";
14
- import { formatNumber } from "@motionscript/sdk/component";
15
- import { asCommand, drive, hold, sequence as sequenceOf, together, } from "@motionscript/sdk/component";
10
+ import { clampUnit, formatMarker, lerpAxisDomain, lerpAxisEntry, markerStep, markerValues, num, resolveAxisDomain, settleAxisDomain, } from "../kit/shared.js";
16
11
  import { CategoryAxis } from "./category-axis.js";
17
12
  import { ChartBars } from "./chart-bars.js";
18
13
  import { DEFAULT_BAR_CHART_THEME, barCategories, barSpans, lerpBarChartTheme, resolveBarChartTheme, resolveBarFill, resolveBarScale, stackCapIndex, } from "./shared.js";
19
- /**
20
- * Headroom above the plot, in px — the line chart's, so the two sit at the same
21
- * size in the same box and a scene can swap one for the other without re-tuning
22
- * its layout. The plot's top grid line is the one place a marker would
23
- * otherwise be drawn outside the chart's own box.
24
- */
25
- const TOP_HEADROOM = 24;
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 });
26
16
  /** How far the legend starts below its slot when it enters, in px. */
27
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
+ */
28
25
  const AXES_SHARE = 0.35;
29
26
  const BARS_SHARE = 0.5;
30
27
  const LEGEND_SHARE = 0.15;
28
+ /** The value axis with its range and ticks resolved: the explicit ones, else five across the scale. */
29
+ function drawnValue(s, domain) {
30
+ 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) };
33
+ }
34
+ /** The value axis as a bar chart draws it now, an auto range or tick row read back as itself. */
35
+ export function barChartDomain(s) {
36
+ return settleAxisDomain(s.valueAxis, (d) => drawnValue(s, d));
37
+ }
38
+ /**
39
+ * A value written down as this chart's value axis says: the ticks and the
40
+ * figures printed on the columns both read through here, so the two cannot
41
+ * disagree about what a number looks like.
42
+ */
43
+ function valueLabel(s, domain, value) {
44
+ const pattern = s.valueAxis.format;
45
+ return pattern === undefined ? formatMarker(value, markerStep(domain.ticks)) : formatNumber(value, pattern);
46
+ }
47
+ /** A value on the plot's **centred** pixel space (origin at its centre, y-up). */
48
+ function valueToY(domain, plot, value) {
49
+ const [min, max] = domain.range;
50
+ if (max <= min)
51
+ return -plot.height / 2;
52
+ return ((value - min) / (max - min) - 0.5) * plot.height;
53
+ }
54
+ /**
55
+ * Pixel y of the line every column grows from: zero clamped onto the scale, so
56
+ * a cropped axis grows its columns from the plot floor.
57
+ */
58
+ function baselinePx(domain, plot) {
59
+ const [min, max] = domain.range;
60
+ return valueToY(domain, plot, Math.min(Math.max(0, min), max));
61
+ }
62
+ /** Series `i`'s live share of the slot; 0 once it has been hidden. */
63
+ function weightAt(s, index) {
64
+ return Math.max(0, s.weights[index] ?? 1);
65
+ }
66
+ /** `row` with each series' value scaled by its live weight: what the stacked spans are measured from. */
67
+ function weightedRow(s, row) {
68
+ const scaled = { ...row };
69
+ s.series.forEach((series, i) => {
70
+ const value = row[series.field];
71
+ if (typeof value === "number")
72
+ scaled[series.field] = value * weightAt(s, i);
73
+ });
74
+ return scaled;
75
+ }
31
76
  /**
32
- * A grouped or stacked bar chart, composed from independent parts rather than
33
- * one monolithic draw — the sibling of the {@link LineChart} family, built the
34
- * same way and out of several of the same pieces:
77
+ * Series `index`'s columns, mapped into the plot's centred pixel space. A
78
+ * grouped series divides the slot with its siblings by their live weights, a
79
+ * stacked one takes the whole slot and runs from the running total below it,
80
+ * so hiding a series reflows rather than punching a hole.
81
+ */
82
+ function seriesBars(s, index, plot, captioned) {
83
+ const series = s.series[index];
84
+ if (!series)
85
+ return [];
86
+ const domain = barChartDomain(s);
87
+ const n = s.data.length;
88
+ const slotWidth = n > 0 ? plot.width / n : 0;
89
+ const slotCenter = (i) => -plot.width / 2 + slotWidth * (i + 0.5);
90
+ const inner = slotWidth * clampUnit(s.theme.bar.slotFill);
91
+ const gap = s.theme.bar.groupGap;
92
+ let total = 0;
93
+ for (let i = 0; i < s.series.length; i++)
94
+ total += weightAt(s, i);
95
+ const unit = inner / (total > 0 ? total : 1);
96
+ const weight = weightAt(s, index);
97
+ let before = 0;
98
+ for (let i = 0; i < index; i++)
99
+ before += weightAt(s, i);
100
+ return s.data.map((row, rowIndex) => {
101
+ // The stack is built from the *live* contributions, so a series fading out
102
+ // lowers the segments above it as it goes.
103
+ const spans = barSpans(s.stacked ? weightedRow(s, row) : row, s.series, s.stacked);
104
+ const span = spans[index] ?? { from: 0, to: 0, value: 0 };
105
+ const cap = s.stacked ? stackCapIndex(row, s.series) : index;
106
+ const slotLeft = slotCenter(rowIndex) - inner / 2;
107
+ const x = s.stacked ? slotCenter(rowIndex) : slotLeft + unit * before + (unit * weight) / 2;
108
+ const width = s.stacked ? inner : Math.max(0, unit * weight - (s.series.length > 1 ? gap : 0));
109
+ // The figure printed is the row's own value, not the weighted one.
110
+ const value = num(row, series.field);
111
+ return {
112
+ x,
113
+ width,
114
+ from: valueToY(domain, plot, span.from),
115
+ to: valueToY(domain, plot, span.to),
116
+ round: cap === index,
117
+ value,
118
+ ...(captioned && { caption: valueLabel(s, domain, value) }),
119
+ };
120
+ });
121
+ }
122
+ /**
123
+ * How far each category is dimmed by the spotlight, one entry per row: 1 while
124
+ * nothing is spotlit, falling to the theme's `dimOpacity` for every category but
125
+ * the chosen one as the reveal climbs.
126
+ */
127
+ function categoryDims(s) {
128
+ const focus = clampUnit(s.spotlightReveal);
129
+ const dim = s.theme.spotlight.dimOpacity;
130
+ const chosen = s.spotlight;
131
+ return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - dim) * focus));
132
+ }
133
+ /** The parts a bar chart's commands drive, found through its children. */
134
+ function partsOf(children) {
135
+ const frame = cartesianViews(children);
136
+ if (!frame)
137
+ return null;
138
+ const cAxis = frame.below.find(CategoryAxis.is);
139
+ if (!VerticalAxis.is(frame.axis) || !cAxis)
140
+ return null;
141
+ return {
142
+ vAxis: frame.axis,
143
+ cAxis,
144
+ bars: frame.plot.filter(ChartBars.is),
145
+ legend: frame.below.find(ChartLegend.is) ?? null,
146
+ };
147
+ }
148
+ /**
149
+ * Move series `index`'s slot share to `target` with its legend entry, the group
150
+ * reflowing. Its columns are drawn for every frame the move covers, and switch
151
+ * off only once a hide has landed, so arriving at a time backwards shows what
152
+ * arriving forwards does.
153
+ */
154
+ function toggling(timeline, state, children, index, target) {
155
+ const parts = partsOf(children);
156
+ if (!parts || !parts.bars[index])
157
+ return timeline.wait(timeline.duration);
158
+ // Read through `weightAt`, so a chart whose stored weights are short still
159
+ // tweens from the 1 it is drawing at.
160
+ const from = state.series.map((_, i) => weightAt(state, i));
161
+ const start = from[index] ?? 1;
162
+ const showing = state.series.map((s, i) => state.showing[i] ?? (s.enabled ?? true));
163
+ return timeline.together([
164
+ timeline.animate((t) => {
165
+ const weights = [...from];
166
+ weights[index] = start + (target - start) * timeline.ease(t);
167
+ const shown = [...showing];
168
+ shown[index] = target > 0 || t < 1;
169
+ return { weights, showing: shown };
170
+ }, { eased: false }),
171
+ ...(parts.legend ? [parts.legend.run(target > 0 ? "showSeries" : "hideSeries", { index })] : []),
172
+ ]);
173
+ }
174
+ /**
175
+ * A grouped or stacked bar chart, the sibling of the line chart and built the
176
+ * same way, out of several of the same pieces:
35
177
  *
36
178
  * - `ChartCanvas` — the plot panel with a horizontal grid, under everything.
37
179
  * - `VerticalAxis` — the numeric value markers, staggering in bottom-to-top.
38
- * - {@link CategoryAxis} — the row of names beneath the baseline, one per row of
39
- * the data, staggering in left-to-right.
180
+ * - {@link CategoryAxis} — the row of names beneath the baseline, one per row.
40
181
  * - {@link ChartBars} (one per series) — that series' column in every category,
41
182
  * plus the figures printed on them.
42
183
  * - `ChartLegend` — a row of entries below the chart, one per series.
43
184
  *
44
- * The parts are laid out from this node's `layoutBounds` exactly as the line
45
- * chart's are: the value axis on the left, the category captions below, the
46
- * plot filling what's left, and the legend row beneath the whole thing. The
47
- * columns are mapped across the plot panel's own laid-out box and read the same
48
- * resolved scale the axis does, so a column's end lands exactly on its marker
49
- * and the chart re-flows if its box changes.
50
- *
51
- * **Resting state is the finished chart** — the same bargain the line chart
52
- * makes: an editor paints a node's Initial state on a paused canvas, so a chart
53
- * that only appeared once a command had run would be edited blind. {@link enter}
54
- * arms every part back to its pre-entrance state before animating.
185
+ * Every part reads the chart's entries through bindings, and the columns map
186
+ * across the plot panel's own laid-out box, so a column's end lands exactly on
187
+ * its marker. **Zero is always on the scale**, and the columns grow about it, so
188
+ * a negative value draws downward out of the same `growth` signal.
55
189
  *
56
- * **Zero is always on the scale** (see `barValueExtent`), and the columns
57
- * grow about it rather than about the plot floor, so a negative value draws
58
- * downward out of the same `growth` signal that draws a positive one up.
59
- *
60
- * Past the entrance, two things move the chart:
61
- * {@link showSeries}/{@link hideSeries}/{@link spotlightSeries} pick out a
62
- * *series* — and hiding one **reflows the group**, the remaining columns
63
- * widening into the space as it goes, rather than leaving a hole where it was —
64
- * while {@link spotlightCategory}/{@link clearCategory} pick out a *column of
65
- * the table*, dimming every other category and its caption with it.
190
+ * **Resting state is the finished chart**; `enter` arms every part on its first
191
+ * frame. `showSeries`/`hideSeries`/`spotlightSeries` pick out a *series* (hiding
192
+ * one reflows the group), `spotlightCategory`/`clearCategory` a *column of the
193
+ * table*. A `to` of the value axis's `range` or `ticks` tweens from the domain
194
+ * as drawn, so an auto axis rescales the columns continuously.
66
195
  */
67
- let BarChart = class BarChart extends Node2D {
68
- static { BarChart_1 = this; }
69
- // ---- Part handles (built in the constructor) --------------------------
70
- vAxis;
71
- cAxis;
72
- legend = null;
73
- barSets = [];
74
- /** The plot panel: the box the grid, the columns and the border all share. */
75
- plotRect;
76
- /**
77
- * `this.theme` read back through its *resolved* shape — the same
78
- * loose-declared/resolved-stored idiom as a shape's `fill`/`stroke`, and as
79
- * the line chart's own theme.
80
- */
81
- get resolvedTheme() {
82
- return this.theme;
83
- }
84
- // ---- Derived scale ----------------------------------------------------
85
- /** Fallback marker count when the value axis states no ticks. */
86
- static DEFAULT_MARKERS = 5;
87
- get valueScale() {
88
- const divisions = (this.valueAxis.ticks?.length ?? BarChart_1.DEFAULT_MARKERS) - 1;
89
- return resolveBarScale(this.data, this.series, this.stacked, this.valueAxis.range, divisions);
90
- }
91
- /**
92
- * A value written down as this chart's value axis says — the axis ticks and
93
- * the figures printed on the columns both read through here, so the two
94
- * cannot disagree about what a number looks like.
95
- */
96
- valueLabel(value) {
97
- const pattern = this.valueAxis.format;
98
- return pattern === undefined
99
- ? formatMarker(value, markerStep(this.resolvedMarkers))
100
- : formatNumber(value, pattern);
101
- }
102
- /** The value axis ticks: the explicit ones, else five across the scale. */
103
- get resolvedMarkers() {
104
- return (this.valueAxis.ticks ??
105
- markerValues(this.valueScale, BarChart_1.DEFAULT_MARKERS - 1));
106
- }
107
- // ---- Plot geometry ------------------------------------------------------
108
- /**
109
- * The plot panel's size, **as laid out**.
110
- *
111
- * The value axis is `width: "hug"` and the category row and legend are as tall
112
- * as their text, so the panel's real box is whatever the layout makes it. An
113
- * estimate built from gutter constants never matched it, and the columns sat a
114
- * few percent off the grid line they were supposed to end on. Asking the panel
115
- * is exact by construction: the grid, the columns and the border all resolve
116
- * against the one box they are drawn in. Zero until the first layout pass, like
117
- * any other `layoutBounds` read; the bindings that call this re-run on the
118
- * next frame.
119
- */
120
- plotSize() {
121
- const { width, height } = this.plotRect.layoutBounds;
122
- return { w: width, h: height };
123
- }
124
- /** Map a value onto the plot's **centred** pixel space (origin at its centre, y-up). */
125
- valueToY(value) {
126
- const [min, max] = this.valueScale;
127
- const { h } = this.plotSize();
128
- if (max <= min)
129
- return -h / 2;
130
- return ((value - min) / (max - min) - 0.5) * h;
131
- }
132
- /**
133
- * Pixel y of the line every column grows from.
134
- *
135
- * Zero, clamped onto the scale — so an author who *did* crop the axis to
136
- * `[40, 100]` gets columns rising from the plot floor rather than from a
137
- * baseline off the bottom of the panel.
138
- */
139
- get baselinePx() {
140
- const [min, max] = this.valueScale;
141
- return this.valueToY(Math.min(Math.max(0, min), max));
142
- }
143
- // ---- Slot geometry ----------------------------------------------------
144
- /** Width of one category's slot across the plot. */
145
- get slotWidth() {
146
- const n = this.data.length;
147
- return n > 0 ? this.plotSize().w / n : 0;
148
- }
149
- /** Centre x of category `i`'s slot, in the plot's centred space. */
150
- slotCenter(index) {
151
- return -this.plotSize().w / 2 + this.slotWidth * (index + 0.5);
152
- }
153
- /** The part of a slot the columns fill — the rest is the channel between categories. */
154
- get slotInner() {
155
- return this.slotWidth * clampUnit(this.resolvedTheme.bar.slotFill);
156
- }
157
- /** Series `i`'s live share of the slot; 0 once it has been hidden. */
158
- weightAt(index) {
159
- return Math.max(0, this.weights[index] ?? 1);
160
- }
161
- /** Total live share across the series — the slot's inner width divides by this. */
162
- get totalWeight() {
163
- let sum = 0;
164
- for (let i = 0; i < this.series.length; i++)
165
- sum += this.weightAt(i);
166
- return sum > 0 ? sum : 1;
167
- }
168
- /** Live share of every series *before* `index`, for the grouped offset. */
169
- weightBefore(index) {
170
- let sum = 0;
171
- for (let i = 0; i < index; i++)
172
- sum += this.weightAt(i);
173
- return sum;
174
- }
175
- /**
176
- * Series `index`'s columns, mapped into the plot's centred pixel space.
177
- *
178
- * Both modes come out of one walk over the rows: a grouped series divides the
179
- * slot with its siblings and every column runs from zero, a stacked one takes
180
- * the whole slot and each column runs from the running total below it. The
181
- * live {@link weights} scale the grouped widths and the stacked contributions
182
- * alike, which is what lets `hideSeries` reflow rather than punch a hole.
183
- */
184
- seriesBars(index) {
185
- const series = this.series[index];
186
- if (!series)
187
- return [];
188
- const stacked = this.stacked;
189
- const inner = this.slotInner;
190
- const gap = this.resolvedTheme.bar.groupGap;
191
- const unit = inner / this.totalWeight;
192
- const weight = this.weightAt(index);
193
- return this.data.map((row, rowIndex) => {
194
- // The stack is built from the *live* contributions, so a series fading out
195
- // lowers the segments above it as it goes instead of dropping them by a
196
- // step when it finally switches off. A grouped row needs no such scaling —
197
- // there its weight moves the column's width, not its length.
198
- const spans = barSpans(stacked ? this.weightedRow(row) : row, this.series, stacked);
199
- const span = spans[index] ?? { from: 0, to: 0, value: 0 };
200
- const cap = stacked ? stackCapIndex(row, this.series) : index;
201
- const slotLeft = this.slotCenter(rowIndex) - inner / 2;
202
- const x = stacked
203
- ? this.slotCenter(rowIndex)
204
- : slotLeft + unit * this.weightBefore(index) + (unit * weight) / 2;
205
- const width = stacked
206
- ? inner
207
- : Math.max(0, unit * weight - (this.series.length > 1 ? gap : 0));
208
- return {
209
- x,
210
- width,
211
- from: this.valueToY(span.from),
212
- to: this.valueToY(span.to),
213
- round: cap === index,
214
- // The figure printed is the row's own value, not the weighted one — a
215
- // series on its way out should not be seen counting down.
216
- value: num(row, series.field),
217
- };
218
- });
219
- }
220
- /**
221
- * `row` with each series' value scaled by that series' live {@link weights}
222
- * entry — what the stacked spans are measured from.
223
- */
224
- weightedRow(row) {
225
- const scaled = { ...row };
226
- this.series.forEach((s, i) => {
227
- const value = row[s.field];
228
- if (typeof value === "number")
229
- scaled[s.field] = value * this.weightAt(i);
230
- });
231
- return scaled;
232
- }
233
- /**
234
- * How far each category is dimmed by the spotlight, one entry per row —
235
- * 1 while nothing is spotlit, falling to the theme's `dimOpacity` for every
236
- * category but the chosen one as the reveal climbs.
237
- */
238
- categoryDims() {
239
- const focus = clampUnit(this.spotlightReveal);
240
- const dim = this.resolvedTheme.spotlight.dimOpacity;
241
- const chosen = this.spotlight;
242
- return this.data.map((_, i) => focus <= 0 || chosen === i ? 1 : 1 - (1 - dim) * focus);
243
- }
244
- // ---- Build ------------------------------------------------------------
245
- constructor(props) {
246
- super(props);
247
- // The whole chart is composed from plain props/`data` — no context needed,
248
- // so the parts are built here (super() applied the props above). Everything
249
- // a *leaf* reads live in `renderSelf` — each series' geometry, paint and
250
- // dim, the panel's fill, the border's stroke — is passed as a closure rather
251
- // than a snapshot, so `chart.to({ theme })` and
252
- // `chart.to({ valueAxis: { range } })` genuinely animate the drawn chart.
253
- const enabled = this.series.map((s) => s.enabled ?? true);
254
- this.weights = enabled.map((on) => (on ? 1 : 0));
255
- const showLegend = props?.showLegend !== false;
256
- const showValues = props?.showValues === true;
257
- this.vAxis = new VerticalAxis({
258
- range: () => this.valueScale,
259
- markers: () => this.resolvedMarkers,
260
- axis: () => this.resolvedTheme.yAxis,
261
- label: this.valueAxis.label ?? "",
196
+ export const BarChart = component2d({
197
+ key: "barChart",
198
+ state: {
199
+ data: fresh(() => []),
200
+ categoryField: "",
201
+ series: fresh(() => []),
202
+ stacked: false,
203
+ valueAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
204
+ categoryAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisDomain }),
205
+ theme: Types.custom(DEFAULT_BAR_CHART_THEME, { resolve: resolveBarChartTheme, lerp: lerpBarChartTheme }),
206
+ /**
207
+ * Global rise of every column in `[0, 1]`, measured about the baseline: 0
208
+ * collapses them all onto it, 1 draws them at full length.
209
+ */
210
+ growth: 1,
211
+ /**
212
+ * How much of the slot each series claims, one entry per series: a weight
213
+ * rather than a switch, so hiding one is the others widening into the space.
214
+ * Seeded from each series' `enabled`.
215
+ */
216
+ weights: fresh(() => []),
217
+ /** The category spotlit, or `null` for none. The choice snaps; `spotlightReveal` is the animation. */
218
+ spotlight: Types.custom(null),
219
+ /** How far the category spotlight is drawn in, `[0, 1]`. */
220
+ spotlightReveal: 0,
221
+ /** Read as the chart is built: the legend is a part the chart builds or doesn't. */
222
+ showLegend: Types.boolean(true, { animatable: false }),
223
+ /** Read as the chart is built: whether each column prints the figure it plots. */
224
+ showValues: Types.boolean(false, { animatable: false }),
225
+ /** Whether each series' columns are drawn: on for every frame a show or hide covers, off once a hide lands. */
226
+ showing: Types.custom([], { private: true }),
227
+ },
228
+ defaults: {
229
+ // Its parts fill it; a node made of them would hug them.
230
+ width: "fill",
231
+ height: "fill",
232
+ weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
233
+ showing: (state) => state.series.map((s) => s.enabled ?? true),
234
+ },
235
+ children: (state) => {
236
+ const theme = () => state.theme;
237
+ const domain = () => barChartDomain(state);
238
+ let plot;
239
+ // Before the panel is built or laid out it has no size, and the columns map onto nothing.
240
+ const plotSize = () => laidOutSize(plot);
241
+ const captioned = state.showValues === true;
242
+ const vAxis = new VerticalAxis({
243
+ range: () => domain().range,
244
+ markers: () => domain().ticks,
245
+ captions: () => {
246
+ const d = domain();
247
+ return d.ticks.map((value) => valueLabel(state, d, value));
248
+ },
249
+ axis: () => theme().yAxis,
250
+ label: state.valueAxis.label ?? "",
262
251
  width: "hug",
263
252
  height: "fill",
264
253
  });
265
- this.vAxis.labelFor = (value) => this.valueLabel(value);
266
- this.cAxis = new CategoryAxis({
267
- categories: barCategories(this.data, this.categoryField),
268
- axis: () => this.resolvedTheme.xAxis,
269
- label: this.categoryAxis.label ?? "",
270
- dims: () => this.categoryDims(),
254
+ const cAxis = new CategoryAxis({
255
+ categories: barCategories(state.data, state.categoryField),
256
+ axis: () => theme().xAxis,
257
+ label: state.categoryAxis.label ?? "",
258
+ dims: () => categoryDims(state),
271
259
  width: "fill",
272
260
  height: "hug",
273
261
  });
274
- this.barSets = this.series.map((s, i) => new ChartBars({
275
- bars: () => this.seriesBars(i),
276
- baseline: () => this.baselinePx,
277
- fill: () => resolveBarFill(this.resolvedTheme.seriesFills, s, i),
278
- barStroke: () => this.resolvedTheme.bar.stroke,
279
- barShadow: () => this.resolvedTheme.bar.shadow,
280
- cornerRadius: () => this.resolvedTheme.bar.cornerRadius,
281
- growth: () => this.growth,
282
- dims: () => this.categoryDims(),
283
- showValues,
284
- valueStyle: this.resolvedTheme.valueStyle,
285
- enabled: enabled[i],
262
+ const barSets = state.series.map((s, i) => new ChartBars({
263
+ bars: () => seriesBars(state, i, plotSize(), captioned),
264
+ baseline: () => baselinePx(domain(), plotSize()),
265
+ fill: () => resolveBarFill(theme().seriesFills, s, i),
266
+ barStroke: () => theme().bar.stroke,
267
+ barShadow: () => theme().bar.shadow,
268
+ cornerRadius: () => theme().bar.cornerRadius,
269
+ growth: () => state.growth,
270
+ dims: () => categoryDims(state),
271
+ showValues: captioned,
272
+ valueStyle: state.theme.valueStyle,
273
+ enabled: () => state.showing[i] ?? (s.enabled ?? true),
286
274
  }));
287
- // The figures on the columns are the same numbers the value axis ticks, so
288
- // they read through the same format.
289
- for (const set of this.barSets)
290
- set.labelFor = (value) => this.valueLabel(value);
291
- if (showLegend) {
292
- this.legend = new ChartLegend({
293
- series: this.series,
294
- swatches: () => this.series.map((s, i) => resolveBarFill(this.resolvedTheme.seriesFills, s, i)),
295
- legend: () => this.resolvedTheme.legend,
296
- });
275
+ const below = [cAxis];
276
+ if (state.showLegend !== false) {
277
+ below.push(new ChartLegend({
278
+ series: state.series,
279
+ swatches: () => state.series.map((s, i) => resolveBarFill(theme().seriesFills, s, i)),
280
+ legend: () => theme().legend,
281
+ // The row's spacing is the theme's as the chart was built, like the
282
+ // entries' type and chips: a theme tween re-paints the swatches only.
283
+ gap: state.theme.legend.itemGap,
284
+ padding: state.theme.legend.padding,
285
+ }));
297
286
  }
298
- this.plotRect = new Rect({
287
+ plot = new Rect({
299
288
  clip: true,
300
289
  children: [
301
290
  new ChartCanvas({
302
- fill: () => this.resolvedTheme.plotArea.fill,
303
- shadow: () => this.resolvedTheme.plotArea.shadow,
304
- xGridStroke: () => this.resolvedTheme.xAxis.lineStyle,
305
- yGridStroke: () => this.resolvedTheme.yAxis.lineStyle,
306
- // No vertical rules: the horizontal axis is categorical, so there is
307
- // no numeric position along it for a grid line to mark. The channel
308
- // between slots does that job, and better.
291
+ fill: () => theme().plotArea.fill,
292
+ shadow: () => theme().plotArea.shadow,
293
+ xGridStroke: () => theme().xAxis.lineStyle,
294
+ yGridStroke: () => theme().yAxis.lineStyle,
295
+ // No vertical rules: the horizontal axis is categorical, so there is no
296
+ // numeric position along it for a grid line to mark.
309
297
  xMarkers: [],
310
- yMarkers: () => this.resolvedMarkers,
298
+ yMarkers: () => domain().ticks,
311
299
  xExtent: () => [0, 1],
312
- yExtent: () => this.valueScale,
313
- }),
314
- ...this.barSets,
315
- new ChartBorder({
316
- stroke: () => this.resolvedTheme.plotArea.stroke,
300
+ yExtent: () => domain().range,
317
301
  }),
302
+ ...barSets,
303
+ new ChartBorder({ stroke: () => theme().plotArea.stroke }),
318
304
  ],
319
305
  });
320
- // Invisible twin of the value axis, reserving the same left gutter under the
321
- // plot so the category row starts at the plot's left edge rather than the
322
- // chart's — and so a caption lands under the columns it names.
323
- const gutterSpacer = new VerticalAxis({
306
+ const gutter = new VerticalAxis({
324
307
  opacity: 0,
325
- range: () => this.valueScale,
326
- markers: () => this.resolvedMarkers,
327
- axis: () => this.resolvedTheme.yAxis,
328
- label: this.valueAxis.label ?? "",
308
+ range: () => domain().range,
309
+ markers: () => domain().ticks,
310
+ axis: () => theme().yAxis,
311
+ label: state.valueAxis.label ?? "",
329
312
  width: "hug",
330
313
  height: 0,
331
314
  });
332
- const below = [this.cAxis];
333
- if (this.legend)
334
- below.push(this.legend);
335
- this.add(new Rect({ flow: "vertical",
336
- width: "fill",
337
- height: "fill",
338
- // See {@link TOP_HEADROOM}.
339
- padding: { top: TOP_HEADROOM },
340
- children: [
341
- new Rect({ flow: "horizontal",
342
- height: "fill",
343
- width: "fill",
344
- children: [this.vAxis, this.plotRect],
345
- }),
346
- new Rect({ flow: "horizontal",
347
- width: "fill",
348
- children: [
349
- gutterSpacer,
350
- new Rect({ flow: "vertical", width: "fill", gap: 20, children: below }),
351
- ],
352
- }),
353
- ],
354
- }));
355
- }
356
- // ---- Orchestrated entrance --------------------------------------------
357
- /**
358
- * Prime every part at its pre-entrance state — axis markers and captions
359
- * shifted and transparent, columns collapsed onto the baseline, legend dropped
360
- * and transparent.
361
- *
362
- * Called by {@link enter} before its first frame so nothing flashes ahead of
363
- * its beat. Idempotent, and public so a scene can hide a chart ahead of time
364
- * without starting the entrance.
365
- */
366
- arm() {
367
- this.vAxis.arm();
368
- this.cAxis.arm();
369
- this.growth = 0;
370
- this.legend?.set({ opacity: 0, y: -LEGEND_SLIDE });
371
- }
372
- /**
373
- * Animate the whole chart in: the two axes stagger their markers and captions
374
- * in, the columns rise from the baseline, and the legend fades up last.
375
- *
376
- * `duration` is the **whole** entrance. The phases are shares of it, not
377
- * multiples — a command that declares 1.4 seconds has to take 1.4 seconds,
378
- * because that is the slot the timeline lays out for it, the length its
379
- * parallel siblings are timed against, and its contribution to the scene's own
380
- * duration.
381
- */
382
- enter(args) {
383
- const { duration } = args;
384
- const easing = args.easing ?? easeOut();
385
- this.arm();
386
- return asCommand(sequenceOf(together(this.vAxis.enter({ data: { slide: 24 }, duration: duration * AXES_SHARE, easing }), this.cAxis.enter({ data: { slide: 24 }, duration: duration * AXES_SHARE, easing })), this.riseBars(duration * BARS_SHARE, easing),
387
- // Without a legend the beat is still its share of the whole, so the
388
- // entrance ends exactly when it was laid out to.
389
- this.legend
390
- ? this.legend.to({ data: { opacity: 1, y: 0 }, duration: duration * LEGEND_SHARE, easing })
391
- : hold(duration * LEGEND_SHARE)), duration);
392
- }
393
- /**
394
- * Move props to new values; a restated value-axis `range` or `ticks` tweens
395
- * from the domain as drawn, so an auto axis (the default) rescales the columns
396
- * continuously instead of cutting at the midpoint of the move.
397
- */
398
- to(args) {
399
- const data = (args.data ?? {});
400
- const drawn = {
401
- ...this.valueAxis,
402
- range: this.valueScale,
403
- ticks: this.resolvedMarkers,
404
- };
405
- const target = axisTweenTarget(drawn, data.valueAxis);
406
- if (!target)
407
- return super.to(args);
408
- const { valueAxis: _valueAxis, ...rest } = data;
409
- const axis = this.command((t) => ({ valueAxis: lerpAxisDomain(drawn, target, t) }), args.duration, args.easing);
410
- if (Object.keys(rest).length === 0)
411
- return axis;
412
- return asCommand(together(super.to({ ...args, data: rest }), axis), args.duration);
413
- }
414
- /** Tween {@link growth} `0 → 1`, raising every column about the baseline. */
415
- riseBars(duration, easing) {
416
- // No priming to 0 first: the value at `t` is a function of `t`, so a repeat
417
- // call re-raises by definition rather than by having reset something.
418
- return this.command((t) => ({ growth: t }), duration, easing);
419
- }
420
- /**
421
- * Show series `index`: bring its columns back, the group reflowing to make
422
- * room as they arrive, in lockstep with its legend entry growing in.
423
- */
424
- showSeries(args) {
425
- const { seriesIndex = 0 } = args.data ?? {};
426
- return this.toggleSeries(seriesIndex, 1, args.duration ?? 0.35, args.easing ?? easeOut());
427
- }
428
- /**
429
- * Hide series `index`: its columns narrow away (or, stacked, sink out of the
430
- * stack) while the rest of the group widens into the space, its legend entry
431
- * shrinking beside them — the mirror of {@link showSeries}.
432
- */
433
- hideSeries(args) {
434
- const { seriesIndex = 0 } = args.data ?? {};
435
- return this.toggleSeries(seriesIndex, 0, args.duration ?? 0.35, args.easing ?? easeOut());
436
- }
437
- /**
438
- * Move series `index`'s slot share to `target`, with its legend entry.
439
- *
440
- * One method for both directions, so the pair cannot drift apart. `enabled`
441
- * rides in the same value as the weight rather than being set before or after,
442
- * which is what makes arriving at a time backwards show what arriving forwards
443
- * does.
444
- */
445
- toggleSeries(index, target, duration, easing) {
446
- const bars = this.barSets[index];
447
- if (!bars)
448
- return asCommand(hold(duration));
449
- return asCommand(together(this.tweenWeight(index, target, duration, easing), drive(duration, (t) => {
450
- const share = t >= 1 ? target : Math.max(target, 1 - t);
451
- bars.set({ enabled: target > 0 || share > 0 });
452
- }), ...(this.legend
453
- ? [
454
- target > 0
455
- ? this.legend.showSeries({ data: { index }, duration, easing })
456
- : this.legend.hideSeries({ data: { index }, duration, easing }),
457
- ]
458
- : [])), duration);
459
- }
460
- /**
461
- * Fade out every other series — its columns **and its legend entry** — to
462
- * focus attention on `index`, which comes back to full. Nothing moves, so the
463
- * group doesn't reflow.
464
- */
465
- spotlightSeries(args) {
466
- const { seriesIndex = 0 } = args.data ?? {};
467
- const duration = args.duration ?? 0.35;
468
- const easing = args.easing ?? easeOut();
469
- const dim = this.resolvedTheme.spotlight.dimOpacity;
470
- return asCommand(together(...this.barSets.map((bars, i) => bars.to({ data: { opacity: i === seriesIndex ? 1 : dim }, duration, easing })), ...(this.legend
471
- ? [this.legend.spotlightSeries({ data: { index: seriesIndex }, duration, easing })]
472
- : [])), duration);
473
- }
474
- /** Ease series `index`'s slot share toward `target`, reflowing the group. */
475
- tweenWeight(index, target, duration, easing) {
476
- // Read through `weightAt` rather than off the array, so a chart whose stored
477
- // weights are short (or absent) still tweens from the 1 it is drawing at.
478
- const from = this.series.map((_, i) => this.weightAt(i));
479
- const start = from[index] ?? 1;
480
- if (start === target)
481
- return hold(duration);
482
- return drive(duration, (t) => {
483
- const next = [...from];
484
- next[index] = start + (target - start) * easing(t);
485
- this.weights = next;
486
- });
487
- }
488
- // ---- Per-category ------------------------------------------------------
489
- /**
490
- * Spotlight one **category** — one column of the table rather than one series:
491
- * every other category's bars and its caption fade to the theme's
492
- * `dimOpacity`, and the chosen one stays full.
493
- *
494
- * The counterpart of the line chart's `spotlightRegion`, and the same shape of
495
- * thing: it picks out a slice of the plot without changing what is drawn.
496
- * Calling it while another category is already lit moves the focus directly —
497
- * only the dimming changes, so there is nothing to retract first.
498
- */
499
- spotlightCategory(args) {
500
- const { categoryIndex = 0 } = args.data ?? {};
501
- const duration = args.duration ?? 0.4;
502
- const easing = args.easing ?? easeOut();
503
- if (categoryIndex < 0 || categoryIndex >= this.data.length)
504
- return this.holdSpotlight(duration);
505
- const from = this.spotlightReveal;
506
- // Which category is lit is part of the value, set from the first frame: the
507
- // focus moves directly, so unlike the line chart's region there is nothing to
508
- // retract before it.
509
- return this.command((t) => ({ spotlight: categoryIndex, spotlightReveal: from + (1 - from) * t }), duration, easing);
510
- }
511
- /**
512
- * Drop the category spotlight, bringing every category back to full. A no-op
513
- * when nothing is lit — but it still takes its slot on the timeline, for the
514
- * reason `LineChart.clearRegion` does.
515
- */
516
- clearCategory(args) {
517
- const duration = args.duration ?? 0.4;
518
- const easing = args.easing ?? easeOut();
519
- const from = this.spotlightReveal;
520
- if (from === 0)
521
- return this.holdSpotlight(duration);
522
- // The lit category is held until the fade lands — cleared at `t === 1` rather
523
- // than up front, or there would be nothing left to fade.
524
- return this.command((t) => t >= 1
525
- ? { spotlight: null, spotlightReveal: 0 }
526
- : { spotlightReveal: from * (1 - t) }, duration, easing);
527
- }
528
- /** The no-op both of the above return — see {@link hold}. */
529
- holdSpotlight(duration) {
530
- return this.command(() => ({}), duration);
531
- }
532
- };
533
- __decorate([
534
- property({ default: () => [] })
535
- ], BarChart.prototype, "data", void 0);
536
- __decorate([
537
- property({ default: "" })
538
- ], BarChart.prototype, "categoryField", void 0);
539
- __decorate([
540
- property({ default: () => [] })
541
- ], BarChart.prototype, "series", void 0);
542
- __decorate([
543
- property({ default: false })
544
- ], BarChart.prototype, "stacked", void 0);
545
- __decorate([
546
- property({
547
- default: {},
548
- mapper: resolveAxisDomain,
549
- tween: lerpAxisDomain,
550
- })
551
- ], BarChart.prototype, "valueAxis", void 0);
552
- __decorate([
553
- property({
554
- default: {},
555
- mapper: resolveAxisDomain,
556
- tween: lerpAxisDomain,
557
- })
558
- ], BarChart.prototype, "categoryAxis", void 0);
559
- __decorate([
560
- property({
561
- default: DEFAULT_BAR_CHART_THEME,
562
- mapper: resolveBarChartTheme,
563
- tween: lerpBarChartTheme,
564
- })
565
- ], BarChart.prototype, "theme", void 0);
566
- __decorate([
567
- property({ default: 1 })
568
- ], BarChart.prototype, "growth", void 0);
569
- __decorate([
570
- property({ default: () => [] })
571
- ], BarChart.prototype, "weights", void 0);
572
- __decorate([
573
- property({ default: null })
574
- ], BarChart.prototype, "spotlight", void 0);
575
- __decorate([
576
- property({ default: 0 })
577
- ], BarChart.prototype, "spotlightReveal", void 0);
578
- __decorate([
579
- command()
580
- ], BarChart.prototype, "enter", null);
581
- __decorate([
582
- command({ args: [{ key: "seriesIndex", kind: "series", default: 0, min: 0, step: 1 }] })
583
- ], BarChart.prototype, "showSeries", null);
584
- __decorate([
585
- command({ args: [{ key: "seriesIndex", kind: "series", default: 0, min: 0, step: 1 }] })
586
- ], BarChart.prototype, "hideSeries", null);
587
- __decorate([
588
- command({ args: [{ key: "seriesIndex", kind: "series", default: 0, min: 0, step: 1 }] })
589
- ], BarChart.prototype, "spotlightSeries", null);
590
- __decorate([
591
- command({ args: [{ key: "categoryIndex", kind: "number", default: 0, min: 0, step: 1 }] })
592
- ], BarChart.prototype, "spotlightCategory", null);
593
- __decorate([
594
- command()
595
- ], BarChart.prototype, "clearCategory", null);
596
- BarChart = BarChart_1 = __decorate([
597
- node({
598
- key: "barChart",
599
- parentKey: "node",
600
- forkable: true,
601
- layout: {
602
- children: "freeform",
603
- defaultWidthMode: "fixed",
604
- defaultHeightMode: "fixed",
605
- acceptsChildren: true,
315
+ return asParts([
316
+ ...state.children,
317
+ cartesianFrame({ axis: vAxis, plot, gutter, below }),
318
+ ]);
319
+ },
320
+ commands: {
321
+ /**
322
+ * Animate the whole chart in: the two axes stagger their markers and
323
+ * captions in, the columns rise from the baseline, and the legend fades up
324
+ * last. Its first frame arms every part: markers and captions shifted and
325
+ * transparent, columns collapsed, the legend dropped and transparent.
326
+ * `duration` is the **whole** entrance; the beats are shares of it.
327
+ */
328
+ enter: {
329
+ args: {},
330
+ easing: easeOut(),
331
+ run: (timeline, _state, children) => {
332
+ const parts = partsOf(children);
333
+ if (!parts)
334
+ return timeline.wait(timeline.duration);
335
+ const duration = timeline.duration;
336
+ const axes = duration * AXES_SHARE;
337
+ const rise = duration * BARS_SHARE;
338
+ const { legend } = parts;
339
+ const at0 = (piece) => ({ at: 0, duration: 0, piece });
340
+ // Without a legend the beat is still its share of the whole.
341
+ return beats(timeline, [
342
+ at0(timeline.animate(() => ({ growth: 0 }))),
343
+ ...(legend ? [at0(legend.to({ opacity: 0, y: -LEGEND_SLIDE }))] : []),
344
+ { at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
345
+ { at: 0, duration: axes, piece: parts.cAxis.run("enter", { slide: 24 }) },
346
+ // No priming to 0 first: the value at `t` is a function of `t`.
347
+ { at: axes, duration: rise, piece: timeline.animate((t) => ({ growth: t })) },
348
+ ...(legend ? [{ at: axes + rise, duration: duration * LEGEND_SHARE, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
349
+ ]);
350
+ },
351
+ },
352
+ /** Show series `index`: its columns come back, the group reflowing to make room, with its legend entry. */
353
+ showSeries: {
354
+ args: { seriesIndex: SERIES_ARG },
355
+ easing: easeOut(),
356
+ run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 1),
357
+ },
358
+ /**
359
+ * Hide series `index`: its columns narrow away (or, stacked, sink out of the
360
+ * stack) while the rest of the group widens into the space — the mirror of
361
+ * `showSeries`.
362
+ */
363
+ hideSeries: {
364
+ args: { seriesIndex: SERIES_ARG },
365
+ easing: easeOut(),
366
+ run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 0),
367
+ },
368
+ /**
369
+ * Fade every other series — its columns **and its legend entry** — to focus
370
+ * on `index`, which comes back to full. Nothing moves, so the group doesn't
371
+ * reflow.
372
+ */
373
+ spotlightSeries: {
374
+ args: { seriesIndex: SERIES_ARG },
375
+ easing: easeOut(),
376
+ run: (timeline, state, children) => {
377
+ const parts = partsOf(children);
378
+ if (!parts)
379
+ return timeline.wait(timeline.duration);
380
+ const index = timeline.args.seriesIndex;
381
+ const dim = state.theme.spotlight.dimOpacity;
382
+ return timeline.together([
383
+ ...parts.bars.map((bars, i) => bars.to({ opacity: i === index ? 1 : dim })),
384
+ ...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
385
+ ]);
386
+ },
387
+ },
388
+ /**
389
+ * Spotlight one **category**: every other category's columns and caption
390
+ * fade to the theme's `dimOpacity`. Called while another is lit, the focus
391
+ * moves directly: only the dimming changes, so nothing retracts first. A
392
+ * category that is not there takes its slot and changes nothing.
393
+ */
394
+ spotlightCategory: {
395
+ args: { categoryIndex: Types.number(0, { min: 0, step: 1 }) },
396
+ easing: easeOut(),
397
+ run: (timeline, state) => {
398
+ const index = timeline.args.categoryIndex;
399
+ if (index < 0 || index >= state.data.length)
400
+ return timeline.wait(timeline.duration);
401
+ const from = state.spotlightReveal;
402
+ return timeline.animate((t) => ({ spotlight: index, spotlightReveal: from + (1 - from) * t }));
403
+ },
606
404
  },
607
- seed: {
608
- width: 760,
609
- height: 460,
405
+ /**
406
+ * Drop the category spotlight, every category back to full. The lit
407
+ * category is held until the fade lands; a no-op when nothing is lit, which
408
+ * still takes its slot.
409
+ */
410
+ clearCategory: {
411
+ args: {},
412
+ easing: easeOut(),
413
+ run: (timeline, state) => {
414
+ const from = state.spotlightReveal;
415
+ if (from === 0)
416
+ return timeline.wait(timeline.duration);
417
+ const lit = state.spotlight;
418
+ return timeline.animate((t) => (t >= 1 ? { spotlight: null, spotlightReveal: 0 } : { spotlight: lit, spotlightReveal: from * (1 - t) }));
419
+ },
610
420
  },
611
- })
612
- ], BarChart);
613
- export { BarChart };
421
+ },
422
+ });
614
423
  //# sourceMappingURL=bar-chart.js.map