@motionscript/charts 0.1.0 → 0.1.1-alpha.0
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.
- package/CHANGELOG.md +19 -0
- package/dist/bar-chart/bar-chart.d.ts +22 -93
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +48 -121
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +11 -38
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +15 -38
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +43 -74
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +31 -59
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -10
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +0 -7
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +27 -90
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +15 -54
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-4SMRCU24.js +2 -0
- package/dist/browser/chunks/chunk-4SMRCU24.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/engine.d.ts +4 -16
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -16
- package/dist/engine.js.map +1 -1
- package/dist/kit/cartesian.d.ts +8 -28
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +7 -19
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-border.d.ts +9 -18
- package/dist/kit/chart-border.d.ts.map +1 -1
- package/dist/kit/chart-border.js +4 -9
- package/dist/kit/chart-border.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +25 -36
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +13 -13
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +14 -61
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +17 -65
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/kit/entries.d.ts +2 -0
- package/dist/kit/entries.d.ts.map +1 -1
- package/dist/kit/entries.js +2 -0
- package/dist/kit/entries.js.map +1 -1
- package/dist/kit/motion.d.ts +11 -3
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +7 -1
- package/dist/kit/motion.js.map +1 -1
- package/dist/kit/shared.d.ts +60 -175
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +40 -157
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/vertical-axis.d.ts +16 -45
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +17 -42
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +16 -41
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +14 -43
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +14 -27
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +8 -22
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/horizontal-axis.d.ts +16 -43
- package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
- package/dist/line-chart/horizontal-axis.js +13 -42
- package/dist/line-chart/horizontal-axis.js.map +1 -1
- package/dist/line-chart/index.d.ts +1 -13
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +0 -12
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +23 -109
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +45 -141
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +33 -107
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +28 -81
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +14 -55
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +29 -111
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +0 -12
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +0 -12
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +1 -12
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +0 -9
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +37 -71
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +31 -65
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-legend.d.ts +11 -26
- package/dist/pie-chart/pie-legend.d.ts.map +1 -1
- package/dist/pie-chart/pie-legend.js +14 -39
- package/dist/pie-chart/pie-legend.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +50 -192
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +75 -247
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +45 -131
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +29 -72
- package/dist/pie-chart/shared.js.map +1 -1
- package/package.json +4 -4
- package/src/bar-chart/bar-chart.ts +51 -165
- package/src/bar-chart/category-axis.ts +16 -57
- package/src/bar-chart/chart-bars.ts +43 -120
- package/src/bar-chart/index.ts +2 -10
- package/src/bar-chart/shared.ts +29 -97
- package/src/engine.ts +4 -16
- package/src/kit/cartesian.ts +11 -26
- package/src/kit/chart-border.ts +6 -20
- package/src/kit/chart-canvas.ts +15 -40
- package/src/kit/chart-legend.ts +17 -86
- package/src/kit/entries.ts +4 -1
- package/src/kit/motion.ts +11 -2
- package/src/kit/shared.ts +58 -219
- package/src/kit/vertical-axis.ts +18 -71
- package/src/line-chart/chart-line.ts +16 -44
- package/src/line-chart/chart-region.ts +10 -23
- package/src/line-chart/horizontal-axis.ts +13 -42
- package/src/line-chart/index.ts +1 -13
- package/src/line-chart/line-chart.ts +50 -184
- package/src/line-chart/shared.ts +51 -145
- package/src/line-chart/x-scale.ts +31 -121
- package/src/nodes.ts +0 -12
- package/src/pie-chart/index.ts +1 -12
- package/src/pie-chart/pie-chart.ts +34 -102
- package/src/pie-chart/pie-legend.ts +14 -55
- package/src/pie-chart/pie-ring.ts +80 -307
- package/src/pie-chart/shared.ts +50 -139
- package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
- package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Rect,
|
|
3
3
|
Types,
|
|
4
|
+
clamp,
|
|
4
5
|
component2d,
|
|
5
6
|
easeOut,
|
|
6
7
|
type ChildView,
|
|
7
8
|
type DataRecord,
|
|
8
|
-
type EntryOptions,
|
|
9
9
|
type NodeOf,
|
|
10
|
-
// Named so this component's emitted type reaches `Node2D` through the sdk, not core.
|
|
10
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
11
11
|
type Node2D,
|
|
12
|
-
type Node2DProps,
|
|
12
|
+
type Node2DProps as _Node2DProps,
|
|
13
13
|
type Size2D,
|
|
14
|
-
type Timeline,
|
|
15
14
|
} from "@motionscript/sdk"
|
|
16
15
|
import { formatNumber } from "@motionscript/sdk/component"
|
|
17
16
|
|
|
@@ -19,11 +18,10 @@ import { cartesianFrame, cartesianViews, laidOutSize } from "../kit/cartesian"
|
|
|
19
18
|
import { ChartBorder } from "../kit/chart-border"
|
|
20
19
|
import { ChartCanvas } from "../kit/chart-canvas"
|
|
21
20
|
import { ChartLegend } from "../kit/chart-legend"
|
|
22
|
-
import { asParts, fresh } from "../kit/entries"
|
|
23
|
-
import { beats, type Beat } from "../kit/motion"
|
|
21
|
+
import { SERIES_ARG, asParts, fresh } from "../kit/entries"
|
|
22
|
+
import { DEFAULT_MARKERS, ENTER_SHARE, LEGEND_SLIDE, beats, type AnyTimeline, type Beat } from "../kit/motion"
|
|
24
23
|
import { VerticalAxis } from "../kit/vertical-axis"
|
|
25
24
|
import {
|
|
26
|
-
clampUnit,
|
|
27
25
|
formatMarker,
|
|
28
26
|
lerpAxisDomain,
|
|
29
27
|
lerpAxisEntry,
|
|
@@ -34,6 +32,7 @@ import {
|
|
|
34
32
|
settleAxisDomain,
|
|
35
33
|
type AxisDomainConfig,
|
|
36
34
|
type Range,
|
|
35
|
+
type ResolvedAxisDomain,
|
|
37
36
|
} from "../kit/shared"
|
|
38
37
|
import { CategoryAxis } from "./category-axis"
|
|
39
38
|
import { ChartBars, type BarGeometry } from "./chart-bars"
|
|
@@ -51,39 +50,6 @@ import {
|
|
|
51
50
|
type BarSeries,
|
|
52
51
|
} from "./shared"
|
|
53
52
|
|
|
54
|
-
export interface BarChartProps extends Node2DProps {
|
|
55
|
-
/** One row per category — a plain record of field name to value. */
|
|
56
|
-
data: DataRecord[]
|
|
57
|
-
/**
|
|
58
|
-
* Key into each row for its category caption, drawn beneath the baseline.
|
|
59
|
-
* Named `categoryField` rather than `category` for the same reason the line
|
|
60
|
-
* chart's is `xField`: it names a *column*, not a value.
|
|
61
|
-
*/
|
|
62
|
-
categoryField: string
|
|
63
|
-
/** One entry per bar series: which column it plots, its `label`, its `fill`. */
|
|
64
|
-
series: BarSeries[]
|
|
65
|
-
/**
|
|
66
|
-
* When `true` every category's series stack into one column; when `false`
|
|
67
|
-
* (the default) each series gets its own bar, grouped side by side within the
|
|
68
|
-
* category's slot.
|
|
69
|
-
*/
|
|
70
|
-
stacked?: boolean
|
|
71
|
-
/** The value axis's domain: caption, scale range, and tick placement. */
|
|
72
|
-
valueAxis?: AxisDomainConfig
|
|
73
|
-
/** The category axis's domain. Only its `label` is read — the slots are the rows. */
|
|
74
|
-
categoryAxis?: AxisDomainConfig
|
|
75
|
-
/**
|
|
76
|
-
* The chart's visual theme — bar palette and geometry, axis furniture, plot
|
|
77
|
-
* panel paint, legend, value figures. Every branch is optional and deep-merged
|
|
78
|
-
* onto {@link DEFAULT_BAR_CHART_THEME}.
|
|
79
|
-
*/
|
|
80
|
-
theme?: BarChartTheme
|
|
81
|
-
/** Whether the legend row below the plot is drawn at all. */
|
|
82
|
-
showLegend?: boolean
|
|
83
|
-
/** Whether each column prints the figure it plots. */
|
|
84
|
-
showValues?: boolean
|
|
85
|
-
}
|
|
86
|
-
|
|
87
53
|
/** What a bar chart plots, as its entries hold it. */
|
|
88
54
|
export interface BarChartValues {
|
|
89
55
|
readonly data: DataRecord[]
|
|
@@ -93,27 +59,8 @@ export interface BarChartValues {
|
|
|
93
59
|
}
|
|
94
60
|
|
|
95
61
|
/** The value axis's domain with its range and ticks resolved. */
|
|
96
|
-
export type BarChartDomain =
|
|
97
|
-
|
|
98
|
-
/** The argument every per-series command takes: an index into `series`, offered by name. */
|
|
99
|
-
const SERIES_ARG = Types.custom<number, number, EntryOptions & { readonly min: number; readonly step: number }>(0, { kind: "series", min: 0, step: 1 })
|
|
100
|
-
|
|
101
|
-
/** How far the legend starts below its slot when it enters, in px. */
|
|
102
|
-
const LEGEND_SLIDE = 20
|
|
103
|
-
|
|
104
|
-
/** Fallback marker count when the value axis states no ticks. */
|
|
105
|
-
const DEFAULT_MARKERS = 5
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* How `enter` divides its duration between its three beats: the axes arriving,
|
|
109
|
-
* the columns rising, and the legend coming up. Shares of the whole, so they
|
|
110
|
-
* must sum to 1.
|
|
111
|
-
*/
|
|
112
|
-
const AXES_SHARE = 0.35
|
|
113
|
-
const BARS_SHARE = 0.5
|
|
114
|
-
const LEGEND_SHARE = 0.15
|
|
62
|
+
export type BarChartDomain = ResolvedAxisDomain
|
|
115
63
|
|
|
116
|
-
/** What the geometry reads: what the chart plots, its theme and the signals its commands move. */
|
|
117
64
|
interface BarState extends BarChartValues {
|
|
118
65
|
readonly theme: BarChartConfig
|
|
119
66
|
readonly weights: number[]
|
|
@@ -121,17 +68,16 @@ interface BarState extends BarChartValues {
|
|
|
121
68
|
readonly spotlightReveal: number
|
|
122
69
|
}
|
|
123
70
|
|
|
124
|
-
/** The value axis with its range and ticks resolved: the explicit ones, else five across the scale. */
|
|
125
71
|
function drawnValue(s: BarChartValues, domain: AxisDomainConfig): AxisDomainConfig {
|
|
126
72
|
const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
|
|
127
73
|
const range = resolveBarScale(s.data, s.series, s.stacked, domain.range, divisions)
|
|
128
74
|
return { ...domain, range, ticks: domain.ticks ?? markerValues(range, DEFAULT_MARKERS - 1) }
|
|
129
75
|
}
|
|
130
76
|
|
|
131
|
-
/** The last domain worked out from each data array, and what
|
|
77
|
+
/** The last domain worked out from each data array, and what it was read from. */
|
|
132
78
|
const domainsDrawn = new WeakMap<DataRecord[], Omit<BarChartValues, "data"> & { readonly domain: BarChartDomain }>()
|
|
133
79
|
|
|
134
|
-
/** The value axis as
|
|
80
|
+
/** The value axis as drawn now. */
|
|
135
81
|
export function barChartDomain(s: BarChartValues): BarChartDomain {
|
|
136
82
|
const { data, series, stacked, valueAxis } = s
|
|
137
83
|
const kept = domainsDrawn.get(data)
|
|
@@ -141,38 +87,30 @@ export function barChartDomain(s: BarChartValues): BarChartDomain {
|
|
|
141
87
|
return domain
|
|
142
88
|
}
|
|
143
89
|
|
|
144
|
-
/**
|
|
145
|
-
* A value written down as this chart's value axis says: the ticks and the
|
|
146
|
-
* figures printed on the columns both read through here, so the two cannot
|
|
147
|
-
* disagree about what a number looks like.
|
|
148
|
-
*/
|
|
90
|
+
/** Ticks and the figures printed on columns both read through here, so they cannot disagree about how a number looks. */
|
|
149
91
|
function valueLabel(s: BarChartValues, domain: BarChartDomain, value: number): string {
|
|
150
92
|
const pattern = s.valueAxis.format
|
|
151
93
|
return pattern === undefined ? formatMarker(value, markerStep(domain.ticks)) : formatNumber(value, pattern)
|
|
152
94
|
}
|
|
153
95
|
|
|
154
|
-
/** A value
|
|
96
|
+
/** A value in the plot's centred pixel space (origin at its centre, y-up). */
|
|
155
97
|
function valueToY(domain: BarChartDomain, plot: Size2D, value: number): number {
|
|
156
98
|
const [min, max] = domain.range
|
|
157
99
|
if (max <= min) return -plot.height / 2
|
|
158
100
|
return ((value - min) / (max - min) - 0.5) * plot.height
|
|
159
101
|
}
|
|
160
102
|
|
|
161
|
-
/**
|
|
162
|
-
* Pixel y of the line every column grows from: zero clamped onto the scale, so
|
|
163
|
-
* a cropped axis grows its columns from the plot floor.
|
|
164
|
-
*/
|
|
103
|
+
/** Pixel y columns grow from: zero clamped onto the scale, so a cropped axis grows them from the plot floor. */
|
|
165
104
|
function baselinePx(domain: BarChartDomain, plot: Size2D): number {
|
|
166
105
|
const [min, max] = domain.range
|
|
167
106
|
return valueToY(domain, plot, Math.min(Math.max(0, min), max))
|
|
168
107
|
}
|
|
169
108
|
|
|
170
|
-
/** Series `i`'s live share of the slot; 0 once it has been hidden. */
|
|
171
109
|
function weightAt(s: BarState, index: number): number {
|
|
172
110
|
return Math.max(0, s.weights[index] ?? 1)
|
|
173
111
|
}
|
|
174
112
|
|
|
175
|
-
/** `row` with each series' value scaled by its live weight
|
|
113
|
+
/** `row` with each series' value scaled by its live weight, which stacked spans are measured from. */
|
|
176
114
|
function weightedRow(s: BarState, row: DataRecord): DataRecord {
|
|
177
115
|
const scaled: DataRecord = { ...row }
|
|
178
116
|
s.series.forEach((series, i) => {
|
|
@@ -183,10 +121,9 @@ function weightedRow(s: BarState, row: DataRecord): DataRecord {
|
|
|
183
121
|
}
|
|
184
122
|
|
|
185
123
|
/**
|
|
186
|
-
* Series `index`'s columns
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* so hiding a series reflows rather than punching a hole.
|
|
124
|
+
* Series `index`'s columns in centred pixel space. A grouped series divides the
|
|
125
|
+
* slot with its siblings by live weight, a stacked one takes the whole slot, so
|
|
126
|
+
* hiding a series reflows rather than punching a hole.
|
|
190
127
|
*/
|
|
191
128
|
function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean): BarGeometry[] {
|
|
192
129
|
const series = s.series[index]
|
|
@@ -195,7 +132,7 @@ function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean
|
|
|
195
132
|
const n = s.data.length
|
|
196
133
|
const slotWidth = n > 0 ? plot.width / n : 0
|
|
197
134
|
const slotCenter = (i: number) => -plot.width / 2 + slotWidth * (i + 0.5)
|
|
198
|
-
const inner = slotWidth *
|
|
135
|
+
const inner = slotWidth * clamp(s.theme.bar.slotFill, 0, 1)
|
|
199
136
|
const gap = s.theme.bar.groupGap
|
|
200
137
|
let total = 0
|
|
201
138
|
for (let i = 0; i < s.series.length; i++) total += weightAt(s, i)
|
|
@@ -205,15 +142,14 @@ function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean
|
|
|
205
142
|
for (let i = 0; i < index; i++) before += weightAt(s, i)
|
|
206
143
|
|
|
207
144
|
return s.data.map((row, rowIndex) => {
|
|
208
|
-
//
|
|
209
|
-
// lowers the segments above it as it goes.
|
|
145
|
+
// Built from live contributions, so a fading series lowers the segments above it.
|
|
210
146
|
const spans = barSpans(s.stacked ? weightedRow(s, row) : row, s.series, s.stacked)
|
|
211
147
|
const span = spans[index] ?? { from: 0, to: 0, value: 0 }
|
|
212
148
|
const cap = s.stacked ? stackCapIndex(row, s.series) : index
|
|
213
149
|
const slotLeft = slotCenter(rowIndex) - inner / 2
|
|
214
150
|
const x = s.stacked ? slotCenter(rowIndex) : slotLeft + unit * before + (unit * weight) / 2
|
|
215
151
|
const width = s.stacked ? inner : Math.max(0, unit * weight - (s.series.length > 1 ? gap : 0))
|
|
216
|
-
// The figure
|
|
152
|
+
// The printed figure is the row's own value, not the weighted one.
|
|
217
153
|
const value = num(row, series.field)
|
|
218
154
|
return {
|
|
219
155
|
x,
|
|
@@ -227,20 +163,14 @@ function seriesBars(s: BarState, index: number, plot: Size2D, captioned: boolean
|
|
|
227
163
|
})
|
|
228
164
|
}
|
|
229
165
|
|
|
230
|
-
/**
|
|
231
|
-
* How far each category is dimmed by the spotlight, one entry per row: 1 while
|
|
232
|
-
* nothing is spotlit, falling to the theme's `dimOpacity` for every category but
|
|
233
|
-
* the chosen one as the reveal climbs.
|
|
234
|
-
*/
|
|
166
|
+
/** Per-row opacity: 1 while nothing is spotlit, falling to the theme's `dimOpacity` for all but the chosen category. */
|
|
235
167
|
function categoryDims(s: BarState): number[] {
|
|
236
|
-
const focus =
|
|
168
|
+
const focus = clamp(s.spotlightReveal, 0, 1)
|
|
237
169
|
const dim = s.theme.spotlight.dimOpacity
|
|
238
170
|
const chosen = s.spotlight
|
|
239
171
|
return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - dim) * focus))
|
|
240
172
|
}
|
|
241
173
|
|
|
242
|
-
type AnyTimeline = Timeline<any, any>
|
|
243
|
-
|
|
244
174
|
/** The parts a bar chart's commands drive, found through its children. */
|
|
245
175
|
function partsOf(children: readonly ChildView[]) {
|
|
246
176
|
const frame = cartesianViews(children)
|
|
@@ -256,16 +186,14 @@ function partsOf(children: readonly ChildView[]) {
|
|
|
256
186
|
}
|
|
257
187
|
|
|
258
188
|
/**
|
|
259
|
-
* Move series `index`'s slot share to `target` with its legend entry
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* arriving forwards does.
|
|
189
|
+
* Move series `index`'s slot share to `target` with its legend entry. Its
|
|
190
|
+
* columns stay drawn for every frame the move covers and switch off only once a
|
|
191
|
+
* hide has landed, so arriving at a time backwards matches arriving forwards.
|
|
263
192
|
*/
|
|
264
193
|
function toggling(timeline: AnyTimeline, state: BarState & { readonly showing: boolean[] }, children: readonly ChildView[], index: number, target: number) {
|
|
265
194
|
const parts = partsOf(children)
|
|
266
195
|
if (!parts || !parts.bars[index]) return timeline.wait(timeline.duration)
|
|
267
|
-
//
|
|
268
|
-
// tweens from the 1 it is drawing at.
|
|
196
|
+
// Through `weightAt`, so short stored weights still tween from the 1 drawn.
|
|
269
197
|
const from = state.series.map((_, i) => weightAt(state, i))
|
|
270
198
|
const start = from[index] ?? 1
|
|
271
199
|
const showing = state.series.map((s, i) => state.showing[i] ?? (s.enabled ?? true))
|
|
@@ -282,51 +210,32 @@ function toggling(timeline: AnyTimeline, state: BarState & { readonly showing: b
|
|
|
282
210
|
}
|
|
283
211
|
|
|
284
212
|
/**
|
|
285
|
-
* A grouped or stacked bar chart,
|
|
286
|
-
*
|
|
213
|
+
* A grouped or stacked bar chart, built like the line chart from `ChartCanvas`,
|
|
214
|
+
* `VerticalAxis`, {@link CategoryAxis}, one {@link ChartBars} per series and
|
|
215
|
+
* `ChartLegend`. Zero is always on the scale and columns grow about it, so a
|
|
216
|
+
* negative value draws downward from the same `growth` signal.
|
|
287
217
|
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* - {@link ChartBars} (one per series) — that series' column in every category,
|
|
292
|
-
* plus the figures printed on them.
|
|
293
|
-
* - `ChartLegend` — a row of entries below the chart, one per series.
|
|
294
|
-
*
|
|
295
|
-
* Every part reads the chart's entries through bindings, and the columns map
|
|
296
|
-
* across the plot panel's own laid-out box, so a column's end lands exactly on
|
|
297
|
-
* its marker. **Zero is always on the scale**, and the columns grow about it, so
|
|
298
|
-
* a negative value draws downward out of the same `growth` signal.
|
|
299
|
-
*
|
|
300
|
-
* **Resting state is the finished chart**; `enter` arms every part on its first
|
|
301
|
-
* frame. `showSeries`/`hideSeries`/`spotlightSeries` pick out a *series* (hiding
|
|
302
|
-
* one reflows the group), `spotlightCategory`/`clearCategory` a *column of the
|
|
303
|
-
* table*. A `to` of the value axis's `range` or `ticks` tweens from the domain
|
|
304
|
-
* as drawn, so an auto axis rescales the columns continuously.
|
|
218
|
+
* Resting state is the finished chart; `enter` arms every part on its first
|
|
219
|
+
* frame. A `to` of the value axis's `range` or `ticks` tweens from the domain as
|
|
220
|
+
* drawn, so an auto axis rescales the columns continuously.
|
|
305
221
|
*/
|
|
306
222
|
export const BarChart = component2d({
|
|
307
223
|
key: "barChart",
|
|
308
224
|
state: {
|
|
309
225
|
data: fresh<DataRecord[]>(() => []),
|
|
226
|
+
// Names a column, not a value, like the line chart's `xField`.
|
|
310
227
|
categoryField: "",
|
|
311
228
|
series: fresh<BarSeries[]>(() => []),
|
|
312
229
|
stacked: false,
|
|
313
230
|
valueAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
314
231
|
categoryAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisDomain }),
|
|
315
232
|
theme: Types.custom<BarChartTheme, BarChartConfig>(DEFAULT_BAR_CHART_THEME, { resolve: resolveBarChartTheme, lerp: lerpBarChartTheme }),
|
|
316
|
-
/**
|
|
317
|
-
* Global rise of every column in `[0, 1]`, measured about the baseline: 0
|
|
318
|
-
* collapses them all onto it, 1 draws them at full length.
|
|
319
|
-
*/
|
|
233
|
+
/** Rise of every column about the baseline, `[0, 1]`: 0 collapses them onto it. */
|
|
320
234
|
growth: 1,
|
|
321
|
-
/**
|
|
322
|
-
* How much of the slot each series claims, one entry per series: a weight
|
|
323
|
-
* rather than a switch, so hiding one is the others widening into the space.
|
|
324
|
-
* Seeded from each series' `enabled`.
|
|
325
|
-
*/
|
|
235
|
+
/** Each series' share of the slot, seeded from `enabled`: a weight rather than a switch, so hiding one widens the others. */
|
|
326
236
|
weights: fresh<number[]>(() => []),
|
|
327
|
-
/** The category spotlit, or `null
|
|
237
|
+
/** The category spotlit, or `null`. The choice snaps; `spotlightReveal` animates it. */
|
|
328
238
|
spotlight: Types.custom<number | null>(null),
|
|
329
|
-
/** How far the category spotlight is drawn in, `[0, 1]`. */
|
|
330
239
|
spotlightReveal: 0,
|
|
331
240
|
/** Read as the chart is built: the legend is a part the chart builds or doesn't. */
|
|
332
241
|
showLegend: Types.boolean(true, { animatable: false }),
|
|
@@ -336,7 +245,7 @@ export const BarChart = component2d({
|
|
|
336
245
|
showing: Types.custom<boolean[]>([], { private: true }),
|
|
337
246
|
},
|
|
338
247
|
defaults: {
|
|
339
|
-
//
|
|
248
|
+
// A node made of parts that fill would otherwise hug them.
|
|
340
249
|
width: "fill",
|
|
341
250
|
height: "fill",
|
|
342
251
|
weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
|
|
@@ -347,7 +256,6 @@ export const BarChart = component2d({
|
|
|
347
256
|
const theme = () => state.theme
|
|
348
257
|
const domain = () => barChartDomain(state)
|
|
349
258
|
let plot: Node2D | undefined
|
|
350
|
-
// Before the panel is built or laid out it has no size, and the columns map onto nothing.
|
|
351
259
|
const plotSize = () => laidOutSize(plot)
|
|
352
260
|
const captioned = state.showValues === true
|
|
353
261
|
|
|
@@ -397,8 +305,7 @@ export const BarChart = component2d({
|
|
|
397
305
|
series: state.series,
|
|
398
306
|
swatches: () => state.series.map((s, i) => resolveBarFill(theme().seriesFills, s, i)),
|
|
399
307
|
legend: () => theme().legend,
|
|
400
|
-
//
|
|
401
|
-
// entries' type and chips: a theme tween re-paints the swatches only.
|
|
308
|
+
// Spacing is fixed as built; a theme tween re-paints the swatches only.
|
|
402
309
|
gap: state.theme.legend.itemGap,
|
|
403
310
|
padding: state.theme.legend.padding,
|
|
404
311
|
}) as unknown as Node2D
|
|
@@ -413,8 +320,7 @@ export const BarChart = component2d({
|
|
|
413
320
|
shadow: () => theme().plotArea.shadow,
|
|
414
321
|
xGridStroke: () => theme().xAxis.lineStyle,
|
|
415
322
|
yGridStroke: () => theme().yAxis.lineStyle,
|
|
416
|
-
//
|
|
417
|
-
// numeric position along it for a grid line to mark.
|
|
323
|
+
// Categorical x axis: no numeric position for a vertical rule to mark.
|
|
418
324
|
xMarkers: [],
|
|
419
325
|
yMarkers: () => domain().ticks,
|
|
420
326
|
xExtent: () => [0, 1] as Range,
|
|
@@ -438,27 +344,20 @@ export const BarChart = component2d({
|
|
|
438
344
|
return asParts([cartesianFrame({ axis: vAxis as unknown as Node2D, plot, gutter, below })])
|
|
439
345
|
},
|
|
440
346
|
commands: {
|
|
441
|
-
/**
|
|
442
|
-
* Animate the whole chart in: the two axes stagger their markers and
|
|
443
|
-
* captions in, the columns rise from the baseline, and the legend fades up
|
|
444
|
-
* last. Its first frame arms every part: markers and captions shifted and
|
|
445
|
-
* transparent, columns collapsed, the legend dropped and transparent.
|
|
446
|
-
* `duration` is the **whole** entrance; the beats are shares of it.
|
|
447
|
-
*/
|
|
347
|
+
/** Axes stagger in, columns rise from the baseline, the legend fades up last. `duration` is the whole entrance. */
|
|
448
348
|
enter: {
|
|
449
349
|
args: {},
|
|
450
|
-
// Held from
|
|
350
|
+
// Held from arrival, so it never shows finished ahead of its draw.
|
|
451
351
|
entrance: true,
|
|
452
352
|
easing: easeOut(),
|
|
453
353
|
run: (timeline, _state, children) => {
|
|
454
354
|
const parts = partsOf(children)
|
|
455
355
|
if (!parts) return timeline.wait(timeline.duration)
|
|
456
356
|
const duration = timeline.duration
|
|
457
|
-
const axes = duration *
|
|
458
|
-
const rise = duration *
|
|
357
|
+
const axes = duration * ENTER_SHARE.axes
|
|
358
|
+
const rise = duration * ENTER_SHARE.marks
|
|
459
359
|
const { legend } = parts
|
|
460
360
|
const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
|
|
461
|
-
// Without a legend the beat is still its share of the whole.
|
|
462
361
|
return beats(timeline, [
|
|
463
362
|
at0(timeline.animate(() => ({ growth: 0 }))),
|
|
464
363
|
...(legend ? [at0(legend.to({ opacity: 0, y: -LEGEND_SLIDE }))] : []),
|
|
@@ -466,31 +365,23 @@ export const BarChart = component2d({
|
|
|
466
365
|
{ at: 0, duration: axes, piece: parts.cAxis.run("enter", { slide: 24 }) },
|
|
467
366
|
// No priming to 0 first: the value at `t` is a function of `t`.
|
|
468
367
|
{ at: axes, duration: rise, piece: timeline.animate((t) => ({ growth: t })) },
|
|
469
|
-
...(legend ? [{ at: axes + rise, duration: duration *
|
|
368
|
+
...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
|
|
470
369
|
])
|
|
471
370
|
},
|
|
472
371
|
},
|
|
473
|
-
/**
|
|
372
|
+
/** Bring series `index` back, the group reflowing to make room, with its legend entry. */
|
|
474
373
|
showSeries: {
|
|
475
374
|
args: { seriesIndex: SERIES_ARG },
|
|
476
375
|
easing: easeOut(),
|
|
477
376
|
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 1),
|
|
478
377
|
},
|
|
479
|
-
/**
|
|
480
|
-
* Hide series `index`: its columns narrow away (or, stacked, sink out of the
|
|
481
|
-
* stack) while the rest of the group widens into the space — the mirror of
|
|
482
|
-
* `showSeries`.
|
|
483
|
-
*/
|
|
378
|
+
/** The mirror of `showSeries`: columns narrow away (stacked, sink out) while the rest widen into the space. */
|
|
484
379
|
hideSeries: {
|
|
485
380
|
args: { seriesIndex: SERIES_ARG },
|
|
486
381
|
easing: easeOut(),
|
|
487
382
|
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 0),
|
|
488
383
|
},
|
|
489
|
-
/**
|
|
490
|
-
* Fade every other series — its columns **and its legend entry** — to focus
|
|
491
|
-
* on `index`, which comes back to full. Nothing moves, so the group doesn't
|
|
492
|
-
* reflow.
|
|
493
|
-
*/
|
|
384
|
+
/** Fade every other series, columns and legend entry, to focus on `index`. Nothing moves, so the group does not reflow. */
|
|
494
385
|
spotlightSeries: {
|
|
495
386
|
args: { seriesIndex: SERIES_ARG },
|
|
496
387
|
easing: easeOut(),
|
|
@@ -506,10 +397,9 @@ export const BarChart = component2d({
|
|
|
506
397
|
},
|
|
507
398
|
},
|
|
508
399
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
* category that is not there takes its slot and changes nothing.
|
|
400
|
+
* Fade every other category's columns and caption to `dimOpacity`. While
|
|
401
|
+
* another is lit the focus moves directly. A missing category takes its slot
|
|
402
|
+
* and changes nothing.
|
|
513
403
|
*/
|
|
514
404
|
spotlightCategory: {
|
|
515
405
|
args: { categoryIndex: Types.number(0, { min: 0, step: 1 }) },
|
|
@@ -521,11 +411,7 @@ export const BarChart = component2d({
|
|
|
521
411
|
return timeline.animate((t) => ({ spotlight: index, spotlightReveal: from + (1 - from) * t }))
|
|
522
412
|
},
|
|
523
413
|
},
|
|
524
|
-
/**
|
|
525
|
-
* Drop the category spotlight, every category back to full. The lit
|
|
526
|
-
* category is held until the fade lands; a no-op when nothing is lit, which
|
|
527
|
-
* still takes its slot.
|
|
528
|
-
*/
|
|
414
|
+
/** 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. */
|
|
529
415
|
clearCategory: {
|
|
530
416
|
args: {},
|
|
531
417
|
easing: easeOut(),
|
|
@@ -2,63 +2,32 @@ import {
|
|
|
2
2
|
Rect,
|
|
3
3
|
Text,
|
|
4
4
|
Types,
|
|
5
|
+
clamp,
|
|
5
6
|
component2d,
|
|
6
7
|
insetsOps,
|
|
7
8
|
textOps,
|
|
8
9
|
type NodeOf,
|
|
10
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
9
11
|
type Node2D,
|
|
10
|
-
type Node2DProps,
|
|
12
|
+
type Node2DProps as _Node2DProps,
|
|
11
13
|
} from "@motionscript/sdk"
|
|
14
|
+
import { enterBudget } from "@motionscript/sdk/component"
|
|
12
15
|
|
|
13
16
|
import { asParts, fresh } from "../kit/entries"
|
|
14
17
|
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
|
|
15
18
|
import {
|
|
16
19
|
DEFAULT_X_AXIS,
|
|
17
|
-
clampUnit,
|
|
18
|
-
enterBudget,
|
|
19
20
|
type AxisConfig,
|
|
20
21
|
} from "../kit/shared"
|
|
21
22
|
|
|
22
|
-
export interface CategoryAxisProps extends Node2DProps {
|
|
23
|
-
/** One caption per category slot, left to right — the chart's rows, in order. */
|
|
24
|
-
categories: string[]
|
|
25
|
-
/** Caption/title text style and gutters — see {@link AxisConfig}. */
|
|
26
|
-
axis: AxisConfig
|
|
27
|
-
/**
|
|
28
|
-
* Caption drawn centred in the gutter beneath the category row. Empty string
|
|
29
|
-
* (the default) hides it.
|
|
30
|
-
*/
|
|
31
|
-
label: string
|
|
32
|
-
/**
|
|
33
|
-
* Per-category opacity multiplier — how the chart's category spotlight reaches
|
|
34
|
-
* the captions, so the name under a dimmed column dims with it. Missing
|
|
35
|
-
* entries read as 1.
|
|
36
|
-
*/
|
|
37
|
-
dims: number[]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** How far a caption starts above its slot when it enters, in px. */
|
|
41
23
|
const SLIDE = 24
|
|
42
24
|
|
|
43
25
|
/**
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* folded into it: that axis places markers by *value* across a numeric range,
|
|
50
|
-
* this one divides its width into equal slots and centres a name in each. The
|
|
51
|
-
* two look alike on screen and share nothing underneath — one reads a scale,
|
|
52
|
-
* the other counts rows.
|
|
53
|
-
*
|
|
54
|
-
* The slot arithmetic is the same as the plot's, because this node is laid out
|
|
55
|
-
* to exactly the plot's width (the chart reserves the axis gutter with an
|
|
56
|
-
* invisible twin of the value axis, as the line chart does), so a caption lands
|
|
57
|
-
* under the columns it names without either side being told about the other.
|
|
58
|
-
*
|
|
59
|
-
* `enter` staggers the captions in left-to-right, each lifted a little above
|
|
60
|
-
* its slot and transparent; the resting state is the landed one, so a chart
|
|
61
|
-
* that is never told to enter still reads as finished.
|
|
26
|
+
* The categorical axis of a {@link BarChart}: one {@link Text} caption per row,
|
|
27
|
+
* centred on its slot. Not folded into `HorizontalAxis`, which places markers by
|
|
28
|
+
* value on a scale; this divides its width into equal slots. It is laid out to
|
|
29
|
+
* exactly the plot's width, so each caption lands under its columns with neither
|
|
30
|
+
* side told about the other. The resting state is landed.
|
|
62
31
|
*/
|
|
63
32
|
export const CategoryAxis = component2d({
|
|
64
33
|
key: "categoryAxis",
|
|
@@ -66,10 +35,11 @@ export const CategoryAxis = component2d({
|
|
|
66
35
|
categories: fresh<string[]>(() => []),
|
|
67
36
|
axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
|
|
68
37
|
label: "",
|
|
38
|
+
/** Per-category opacity multiplier, so the caption under a dimmed column dims with it. Missing reads as 1. */
|
|
69
39
|
dims: fresh<number[]>(() => []),
|
|
70
40
|
arrivals: arrivalsEntry(),
|
|
71
41
|
},
|
|
72
|
-
// A
|
|
42
|
+
// A node made of captions would otherwise hug them.
|
|
73
43
|
defaults: { width: "fill", height: "fill" },
|
|
74
44
|
children: (state) => {
|
|
75
45
|
const tickPad = insetsOps.resolve(state.axis.tickPadding).top
|
|
@@ -78,23 +48,19 @@ export const CategoryAxis = component2d({
|
|
|
78
48
|
const column: Node2D[] = []
|
|
79
49
|
// The column's parent is the axis: its box is what the slots divide.
|
|
80
50
|
const width = () => (column[0]?.parent as Node2D | null | undefined)?.layoutBounds.width ?? 0
|
|
81
|
-
/** Centre x of slot `i`, in the node's centred space. */
|
|
82
51
|
const slotCenter = (index: number) => {
|
|
83
52
|
const n = state.categories.length
|
|
84
53
|
if (n <= 0) return 0
|
|
85
54
|
return -width() / 2 + (width() / n) * (index + 0.5)
|
|
86
55
|
}
|
|
87
56
|
|
|
88
|
-
//
|
|
89
|
-
// entrance can animate the text's own opacity without the two fighting over
|
|
90
|
-
// one value — opacity multiplies down the tree, which is exactly the
|
|
91
|
-
// composition wanted here.
|
|
57
|
+
// The spotlight dim sits on a hug box around each caption, so the entrance animates the text's own opacity without the two fighting over one value.
|
|
92
58
|
const slots = state.categories.map((caption, i) =>
|
|
93
59
|
new Rect({
|
|
94
60
|
width: "hug",
|
|
95
61
|
height: "hug",
|
|
96
62
|
flow: "freeform",
|
|
97
|
-
opacity: () =>
|
|
63
|
+
opacity: () => clamp(state.dims[i] ?? 1, 0, 1),
|
|
98
64
|
x: () => slotCenter(i),
|
|
99
65
|
children: [
|
|
100
66
|
new Text({
|
|
@@ -124,16 +90,9 @@ export const CategoryAxis = component2d({
|
|
|
124
90
|
},
|
|
125
91
|
commands: {
|
|
126
92
|
/**
|
|
127
|
-
* Slide
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* The timeline's duration is the **whole** entrance — the stagger and the
|
|
132
|
-
* title are carved out of it rather than added on top. See {@link enterBudget}.
|
|
133
|
-
*
|
|
134
|
-
* `stagger` is seconds between successive captions (derived when omitted);
|
|
135
|
-
* `slide` is the starting upward offset in px (node space is y-up, so a
|
|
136
|
-
* positive start slides *down* into place).
|
|
93
|
+
* Slide and fade captions in left to right, then the title. The duration is
|
|
94
|
+
* the whole entrance ({@link enterBudget}). `stagger` is seconds between
|
|
95
|
+
* captions (derived when omitted); `slide` is the starting offset in px.
|
|
137
96
|
*/
|
|
138
97
|
enter: {
|
|
139
98
|
args: {
|