@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
|
@@ -5,13 +5,11 @@ import {
|
|
|
5
5
|
easeOut,
|
|
6
6
|
type ChildView,
|
|
7
7
|
type DataRecord,
|
|
8
|
-
type EntryOptions,
|
|
9
8
|
type NodeOf,
|
|
10
|
-
// Named so this component's emitted type reaches `Node2D` through the sdk, not core.
|
|
9
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
11
10
|
type Node2D,
|
|
12
|
-
type Node2DProps,
|
|
11
|
+
type Node2DProps as _Node2DProps,
|
|
13
12
|
type Size2D,
|
|
14
|
-
type Timeline,
|
|
15
13
|
type Vector2,
|
|
16
14
|
} from "@motionscript/sdk"
|
|
17
15
|
import { formatNumber } from "@motionscript/sdk/component"
|
|
@@ -20,8 +18,8 @@ import { cartesianFrame, cartesianViews, laidOutSize } from "../kit/cartesian"
|
|
|
20
18
|
import { ChartBorder } from "../kit/chart-border"
|
|
21
19
|
import { ChartCanvas } from "../kit/chart-canvas"
|
|
22
20
|
import { ChartLegend } from "../kit/chart-legend"
|
|
23
|
-
import { asParts, fresh } from "../kit/entries"
|
|
24
|
-
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"
|
|
25
23
|
import { VerticalAxis } from "../kit/vertical-axis"
|
|
26
24
|
import {
|
|
27
25
|
SPOTLIGHT_DIM,
|
|
@@ -36,6 +34,7 @@ import {
|
|
|
36
34
|
settleAxisDomain,
|
|
37
35
|
type AxisDomainConfig,
|
|
38
36
|
type Range,
|
|
37
|
+
type ResolvedAxisDomain,
|
|
39
38
|
} from "../kit/shared"
|
|
40
39
|
import { ChartLine } from "./chart-line"
|
|
41
40
|
import { ChartRegion } from "./chart-region"
|
|
@@ -55,41 +54,10 @@ import {
|
|
|
55
54
|
} from "./shared"
|
|
56
55
|
import { resolveXScale, type XScale } from "./x-scale"
|
|
57
56
|
|
|
58
|
-
export interface LineChartProps extends Node2DProps {
|
|
59
|
-
/** One row per sample — a plain record of field name to value. */
|
|
60
|
-
data: DataRecord[]
|
|
61
|
-
/**
|
|
62
|
-
* Key into each row for its x value, shared by every series. Named `xField`
|
|
63
|
-
* rather than `x` because every node already has an `x` — its position.
|
|
64
|
-
*/
|
|
65
|
-
xField: string
|
|
66
|
-
/** One polyline per entry: which field it plots, its legend `label`, its `fill`. */
|
|
67
|
-
series: LineSeries[]
|
|
68
|
-
/** X-axis data domain: caption, scale range, and tick placement. */
|
|
69
|
-
xAxis?: AxisDomainConfig
|
|
70
|
-
/** Y-axis data domain: caption, scale range, and tick placement. */
|
|
71
|
-
yAxis?: AxisDomainConfig
|
|
72
|
-
/**
|
|
73
|
-
* A slice of the plot to spotlight, in **data units**. Dashed rules mark the
|
|
74
|
-
* bounded edges and every series fades outside the box — see
|
|
75
|
-
* `spotlightRegion`, which animates the same thing. `null` (the default) is no
|
|
76
|
-
* region.
|
|
77
|
-
*/
|
|
78
|
-
region?: PlotRegion | null
|
|
79
|
-
/**
|
|
80
|
-
* The chart's visual theme — series palette, axis text/grid/gutters, plot
|
|
81
|
-
* panel paint, and legend styling. Every branch is optional and deep-merged
|
|
82
|
-
* onto {@link DEFAULT_CHART_THEME}, so pass only what you want to override.
|
|
83
|
-
*/
|
|
84
|
-
theme?: ChartTheme
|
|
85
|
-
/** Whether the legend row below the plot is drawn at all. */
|
|
86
|
-
showLegend?: boolean
|
|
87
|
-
}
|
|
88
|
-
|
|
89
57
|
/** Both axes' domains with their ranges and ticks resolved. */
|
|
90
58
|
export interface LineChartDomains {
|
|
91
|
-
readonly x:
|
|
92
|
-
readonly y:
|
|
59
|
+
readonly x: ResolvedAxisDomain
|
|
60
|
+
readonly y: ResolvedAxisDomain
|
|
93
61
|
}
|
|
94
62
|
|
|
95
63
|
/** What a line chart plots, as its entries hold it. */
|
|
@@ -101,12 +69,6 @@ export interface LineChartValues {
|
|
|
101
69
|
readonly yAxis: AxisDomainConfig
|
|
102
70
|
}
|
|
103
71
|
|
|
104
|
-
/**
|
|
105
|
-
* The argument every per-series command takes: an index into `series`. Its own
|
|
106
|
-
* kind rather than `number` so a host can offer the series by name.
|
|
107
|
-
*/
|
|
108
|
-
const SERIES_ARG = Types.custom<number, number, EntryOptions & { readonly min: number; readonly step: number }>(0, { kind: "series", min: 0, step: 1 })
|
|
109
|
-
|
|
110
72
|
/** One argument, two controls: the commands take a `PlotRegion`, so the x and y spans a host draws are declared inside it. */
|
|
111
73
|
const REGION_ARG = Types.custom<PlotRegion | undefined>(undefined, {
|
|
112
74
|
kind: "region",
|
|
@@ -116,21 +78,6 @@ const REGION_ARG = Types.custom<PlotRegion | undefined>(undefined, {
|
|
|
116
78
|
},
|
|
117
79
|
})
|
|
118
80
|
|
|
119
|
-
/** How far the legend starts below its slot when it enters, in px. */
|
|
120
|
-
const LEGEND_SLIDE = 20
|
|
121
|
-
|
|
122
|
-
/** Fallback marker count when neither axis's ticks are given. */
|
|
123
|
-
const DEFAULT_MARKERS = 5
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* How `enter` divides its duration between its three beats: the axes arriving,
|
|
127
|
-
* the lines tracing, and the legend coming up. Shares of the whole, so they must
|
|
128
|
-
* sum to 1 — the entrance takes exactly the time it was given.
|
|
129
|
-
*/
|
|
130
|
-
const AXES_SHARE = 0.35
|
|
131
|
-
const LINES_SHARE = 0.5
|
|
132
|
-
const LEGEND_SHARE = 0.15
|
|
133
|
-
|
|
134
81
|
/** What the chart's own commands read: its entries, and the private `home` a zoom remembers. */
|
|
135
82
|
interface LineState extends LineChartValues {
|
|
136
83
|
readonly theme: ChartConfig
|
|
@@ -139,30 +86,19 @@ interface LineState extends LineChartValues {
|
|
|
139
86
|
readonly home: LineChartDomains | null
|
|
140
87
|
}
|
|
141
88
|
|
|
142
|
-
/**
|
|
143
|
-
* What the x column *is* — a number, an instant, or a place in the file — and
|
|
144
|
-
* with it how each row is positioned, ticked and captioned. See `x-scale.ts`.
|
|
145
|
-
*/
|
|
89
|
+
/** What the x column is (a number, an instant, or a place in the file), and so how rows are positioned, ticked and captioned. */
|
|
146
90
|
function columnOf(s: LineChartValues, format = s.xAxis.format): XScale {
|
|
147
91
|
return resolveXScale(s.data, s.xField, format)
|
|
148
92
|
}
|
|
149
93
|
|
|
150
|
-
/**
|
|
151
|
-
* The horizontal axis with its range and ticks resolved: the explicit ones, else
|
|
152
|
-
* the column's extent and five ticks across it, asked of the column because only
|
|
153
|
-
* a numeric one has round numbers between its samples.
|
|
154
|
-
*/
|
|
94
|
+
/** The explicit range and ticks, else the column's extent and ticks: only a numeric column has round numbers between its samples. */
|
|
155
95
|
function drawnX(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
|
|
156
96
|
const column = columnOf(s, domain.format)
|
|
157
97
|
const range = domain.range ?? column.extent
|
|
158
98
|
return { ...domain, range, ticks: domain.ticks ?? column.ticks(range, DEFAULT_MARKERS - 1) }
|
|
159
99
|
}
|
|
160
100
|
|
|
161
|
-
/**
|
|
162
|
-
* The vertical axis with its range and ticks resolved. Auto-ranging steps into
|
|
163
|
-
* one fewer segment than the intended marker count, so the tidy bounds frame the
|
|
164
|
-
* ticks the axis draws.
|
|
165
|
-
*/
|
|
101
|
+
/** Auto-ranging steps into one fewer segment than the marker count, so the tidy bounds frame the ticks drawn. */
|
|
166
102
|
function drawnY(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
|
|
167
103
|
const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
|
|
168
104
|
const range = resolveYScale(s.data, s.series, domain.range, divisions)
|
|
@@ -175,7 +111,7 @@ const domainsDrawn = new WeakMap<
|
|
|
175
111
|
Pick<LineChartValues, "xField" | "xAxis" | "yAxis" | "series"> & { readonly domains: LineChartDomains }
|
|
176
112
|
>()
|
|
177
113
|
|
|
178
|
-
/** Both axes as
|
|
114
|
+
/** Both axes as drawn now. */
|
|
179
115
|
export function lineChartDomains(s: LineChartValues): LineChartDomains {
|
|
180
116
|
const { data, xField, xAxis, yAxis, series } = s
|
|
181
117
|
const kept = domainsDrawn.get(data)
|
|
@@ -190,10 +126,7 @@ export function lineChartDomains(s: LineChartValues): LineChartDomains {
|
|
|
190
126
|
return domains
|
|
191
127
|
}
|
|
192
128
|
|
|
193
|
-
/**
|
|
194
|
-
* Map one series' data points into the plot's **centred** pixel space (origin at
|
|
195
|
-
* the plot centre, y-up): x runs `[-w/2, +w/2]`, y runs `[-h/2, +h/2]`.
|
|
196
|
-
*/
|
|
129
|
+
/** One series' points in the plot's centred pixel space (origin at its centre, y-up). */
|
|
197
130
|
function seriesPixels(s: LineChartValues, domains: LineChartDomains, index: number, plot: Size2D): Vector2[] {
|
|
198
131
|
const series = s.series[index]
|
|
199
132
|
if (!series) return []
|
|
@@ -204,46 +137,37 @@ function seriesPixels(s: LineChartValues, domains: LineChartDomains, index: numb
|
|
|
204
137
|
const [yMin, yMax] = domains.y.range
|
|
205
138
|
const xToPx = (x: number) => (xMax <= xMin ? 0 : ((x - xMin) / (xMax - xMin) - 0.5) * w)
|
|
206
139
|
const yToPx = (y: number) => (yMax <= yMin ? 0 : ((y - yMin) / (yMax - yMin) - 0.5) * h)
|
|
207
|
-
// `seriesPoints` already sorted them along x.
|
|
208
140
|
return points.map(([x, y]) => ({ x: xToPx(x), y: yToPx(y) }))
|
|
209
141
|
}
|
|
210
142
|
|
|
211
|
-
/** `region`
|
|
143
|
+
/** `region` in centred pixel space against the live scales, so it travels with a zoom. */
|
|
212
144
|
function regionBox(s: LineChartValues & { readonly region: PlotRegion | null }, domains: LineChartDomains, plot: Size2D): RegionBox {
|
|
213
145
|
const { width: w, height: h } = plot
|
|
214
146
|
return resolveRegionBox(s.region, { left: -w / 2, right: w / 2, bottom: -h / 2, top: h / 2 }, domains.x.range, domains.y.range)
|
|
215
147
|
}
|
|
216
148
|
|
|
217
|
-
/**
|
|
218
|
-
* Whether a call-out is authored at all. Not whether it resolves to pixels: a
|
|
219
|
-
* command is built before the first layout, and a region on a chart not yet
|
|
220
|
-
* measured is a real one. A region bounding neither axis is none, as `null` is.
|
|
221
|
-
*/
|
|
149
|
+
/** Whether a call-out is authored, not whether it resolves to pixels: a command is built before the first layout. A region bounding neither axis is none. */
|
|
222
150
|
function hasCallout(s: { readonly region: PlotRegion | null }): boolean {
|
|
223
151
|
const region = s.region
|
|
224
152
|
return region != null && (region.x !== undefined || region.y !== undefined)
|
|
225
153
|
}
|
|
226
154
|
|
|
227
|
-
/** The `region`
|
|
155
|
+
/** The `region` argument, or `{}`: a command is added before its arguments are filled in. */
|
|
228
156
|
function regionArg(region: PlotRegion | null | undefined): PlotRegion {
|
|
229
157
|
return region != null && typeof region === "object" ? region : {}
|
|
230
158
|
}
|
|
231
159
|
|
|
232
|
-
/** A range
|
|
160
|
+
/** A range ordered low to high. */
|
|
233
161
|
function span([a, b]: Range): Range {
|
|
234
162
|
return a <= b ? [a, b] : [b, a]
|
|
235
163
|
}
|
|
236
164
|
|
|
237
|
-
/**
|
|
238
|
-
* Rescale an axis to `range`, keeping the tick *count* it already has, so each
|
|
239
|
-
* marker slides from its old value to its new one instead of the row popping.
|
|
240
|
-
*/
|
|
165
|
+
/** Rescale to `range` keeping the tick count, so each marker slides rather than the row popping. */
|
|
241
166
|
function rescale(range: Range, ticks: readonly number[]): AxisDomainConfig {
|
|
242
167
|
const to = span(range)
|
|
243
168
|
return { range: to, ticks: markerValues(to, Math.max(2, ticks.length) - 1) }
|
|
244
169
|
}
|
|
245
170
|
|
|
246
|
-
/** The axis writes of a move from `from` toward `to` at `t`. */
|
|
247
171
|
function axesAt(from: LineChartDomains, to: Partial<Record<"x" | "y", AxisDomainConfig>>, t: number) {
|
|
248
172
|
return {
|
|
249
173
|
...(to.x && { xAxis: lerpAxisDomain(from.x, to.x, t) }),
|
|
@@ -251,8 +175,6 @@ function axesAt(from: LineChartDomains, to: Partial<Record<"x" | "y", AxisDomain
|
|
|
251
175
|
}
|
|
252
176
|
}
|
|
253
177
|
|
|
254
|
-
type AnyTimeline = Timeline<any, any>
|
|
255
|
-
|
|
256
178
|
/** The parts a line chart's commands drive, found through its children. */
|
|
257
179
|
function partsOf(children: readonly ChildView[]) {
|
|
258
180
|
const frame = cartesianViews(children)
|
|
@@ -267,17 +189,15 @@ function partsOf(children: readonly ChildView[]) {
|
|
|
267
189
|
}
|
|
268
190
|
}
|
|
269
191
|
|
|
270
|
-
/** Drop the call-out: the rules retract, the wash fades, and every series comes back to full. */
|
|
271
192
|
function clearing(timeline: AnyTimeline, state: LineState) {
|
|
272
193
|
const from = state.regionReveal
|
|
273
194
|
const region = state.region
|
|
274
|
-
// Nothing to retract, but the call still takes its slot
|
|
195
|
+
// Nothing to retract, but the call still takes its slot.
|
|
275
196
|
if (from === 0) return timeline.animate(() => ({ region: null, regionReveal: 0 }), { eased: false })
|
|
276
197
|
// `region` is held until the retraction lands, or there would be nothing left to retract.
|
|
277
198
|
return timeline.animate((t) => (t >= 1 ? { region: null, regionReveal: 0 } : { region, regionReveal: from * (1 - t) }))
|
|
278
199
|
}
|
|
279
200
|
|
|
280
|
-
/** Trace series `index`'s line in (`show`) or back out, in lockstep with its legend entry. */
|
|
281
201
|
function toggling(timeline: AnyTimeline, children: readonly ChildView[], index: number, show: boolean) {
|
|
282
202
|
const parts = partsOf(children)
|
|
283
203
|
const line = parts?.lines[index]
|
|
@@ -289,62 +209,35 @@ function toggling(timeline: AnyTimeline, children: readonly ChildView[], index:
|
|
|
289
209
|
}
|
|
290
210
|
|
|
291
211
|
/**
|
|
292
|
-
* A multi-series line chart
|
|
293
|
-
* one
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* - {@link VerticalAxis} / {@link HorizontalAxis} — columns/rows of real text
|
|
297
|
-
* markers that stagger in, each carrying its own axis caption.
|
|
298
|
-
* - {@link ChartRegion} — the optional call-out marking a spotlit slice of the
|
|
299
|
-
* plot: dashed rules on its bounded edges, and every series faded outside it.
|
|
300
|
-
* - {@link ChartLine} (one per series) — the traced polylines over the canvas.
|
|
301
|
-
* - {@link ChartLegend} — a row of entries below the chart, one per series.
|
|
212
|
+
* A multi-series line chart composed of {@link ChartCanvas}, {@link VerticalAxis} /
|
|
213
|
+
* {@link HorizontalAxis}, {@link ChartRegion}, one {@link ChartLine} per series and
|
|
214
|
+
* {@link ChartLegend}. Every part reads the chart's entries through bindings and
|
|
215
|
+
* maps against the plot panel's own laid-out box.
|
|
302
216
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* changes.
|
|
307
|
-
*
|
|
308
|
-
* **Resting state is the finished chart**: an editor paints a node's Initial
|
|
309
|
-
* state on a paused canvas. `enter` arms every part on its first frame.
|
|
310
|
-
*
|
|
311
|
-
* Past the entrance, `showSeries`/`hideSeries`/`spotlightSeries` pick out a
|
|
312
|
-
* *series*, `spotlightRegion` marks a *slice of the plot* without changing the
|
|
313
|
-
* scale, and `zoomTo` rescales the axes so a slice fills the plot. A spotlit
|
|
314
|
-
* region resolves against the live scales, so it travels with a zoom. A `to` of
|
|
315
|
-
* an axis `range` or `ticks` tweens from the domain as drawn (see
|
|
316
|
-
* {@link lerpAxisEntry}), so an auto axis rescales continuously.
|
|
217
|
+
* Resting state is the finished chart; `enter` arms every part on its first
|
|
218
|
+
* frame. A `to` of an axis `range` or `ticks` tweens from the domain as drawn
|
|
219
|
+
* (see {@link lerpAxisEntry}), so an auto axis rescales continuously.
|
|
317
220
|
*/
|
|
318
221
|
export const LineChart = component2d({
|
|
319
222
|
key: "lineChart",
|
|
320
223
|
state: {
|
|
321
224
|
data: fresh<DataRecord[]>(() => [], { kind: "dataset" }),
|
|
225
|
+
// Not `x`: every node already has an `x`, its position.
|
|
322
226
|
xField: "",
|
|
323
227
|
series: fresh<LineSeries[]>(() => [], { kind: "series" }),
|
|
324
228
|
xAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
325
229
|
yAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
326
230
|
theme: Types.custom<ChartTheme, ChartConfig>(DEFAULT_CHART_THEME, { resolve: resolveChartTheme, lerp: lerpChartTheme }),
|
|
327
|
-
/**
|
|
328
|
-
* The spotlit slice of the plot in data units, or `null` for none. The box
|
|
329
|
-
* snaps; the animation is {@link regionReveal} drawing the rules on.
|
|
330
|
-
*/
|
|
231
|
+
/** The spotlit slice in data units, or `null`. The box snaps; {@link regionReveal} animates it. */
|
|
331
232
|
region: Types.custom<PlotRegion | null>(null),
|
|
332
|
-
/**
|
|
333
|
-
* How far the `region` call-out is drawn in, `[0, 1]`: the rules grow out and
|
|
334
|
-
* the wash fades up as it climbs, while every line's out-of-region stretch
|
|
335
|
-
* fades to the theme's `region.dimOpacity`.
|
|
336
|
-
*/
|
|
233
|
+
/** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade to `region.dimOpacity`. */
|
|
337
234
|
regionReveal: 0,
|
|
338
235
|
/** Read as the chart is built: the legend is a part the chart builds or doesn't. */
|
|
339
236
|
showLegend: Types.boolean(true, { animatable: false }),
|
|
340
|
-
/**
|
|
341
|
-
* The domain `zoomTo` first left, so `resetZoom` restores the chart as it
|
|
342
|
-
* stood, auto-derived ticks included. Cleared by `resetZoom`, so the next zoom
|
|
343
|
-
* remembers wherever the chart rests then.
|
|
344
|
-
*/
|
|
237
|
+
/** The domain `zoomTo` first left, so `resetZoom` restores it, auto-derived ticks included. */
|
|
345
238
|
home: Types.custom<LineChartDomains | null>(null, { private: true }),
|
|
346
239
|
},
|
|
347
|
-
//
|
|
240
|
+
// A node made of parts that fill would otherwise hug them.
|
|
348
241
|
defaults: { width: "fill", height: "fill" },
|
|
349
242
|
allowedChildren: [],
|
|
350
243
|
children: (state) => {
|
|
@@ -359,7 +252,6 @@ export const LineChart = component2d({
|
|
|
359
252
|
const vAxis = new VerticalAxis({
|
|
360
253
|
range: () => domains().y.range,
|
|
361
254
|
markers: () => domains().y.ticks,
|
|
362
|
-
// The y axis is always numbers, so its format is simply the pattern.
|
|
363
255
|
captions: () => {
|
|
364
256
|
const markers = domains().y.ticks
|
|
365
257
|
const format = state.yAxis.format
|
|
@@ -374,8 +266,7 @@ export const LineChart = component2d({
|
|
|
374
266
|
const hAxis = new HorizontalAxis({
|
|
375
267
|
range: () => domains().x.range,
|
|
376
268
|
markers: () => domains().x.ticks,
|
|
377
|
-
// A
|
|
378
|
-
// out of — `Jan`, not the instant it parsed to.
|
|
269
|
+
// A month or category tick is captioned with its cell (`Jan`), not the instant it parsed to.
|
|
379
270
|
captions: () => {
|
|
380
271
|
const column = columnOf(state)
|
|
381
272
|
return domains().x.ticks.map((value) => column.label(value))
|
|
@@ -392,10 +283,8 @@ export const LineChart = component2d({
|
|
|
392
283
|
points: () => seriesPixels(state, domains(), i, plotSize()),
|
|
393
284
|
stroke: () => resolveSeriesStroke(theme().seriesStyles, s, i),
|
|
394
285
|
enabled: enabled[i],
|
|
395
|
-
//
|
|
396
|
-
// the dim/bright boundary can't drift from the dashes.
|
|
286
|
+
// The box the rules are drawn from, so the dim boundary cannot drift from the dashes.
|
|
397
287
|
spotlight: () => regionBox(state, domains(), plotSize()),
|
|
398
|
-
// Ramped by the reveal that draws the rules: one signal, both halves.
|
|
399
288
|
dimOpacity: () => 1 - (1 - theme().region.dimOpacity) * state.regionReveal,
|
|
400
289
|
}) as unknown as Node2D
|
|
401
290
|
)
|
|
@@ -405,12 +294,9 @@ export const LineChart = component2d({
|
|
|
405
294
|
below.push(
|
|
406
295
|
new ChartLegend({
|
|
407
296
|
series: state.series,
|
|
408
|
-
// The legend paints a swatch, not a line, so it is handed the colour each
|
|
409
|
-
// stroke resolves to rather than the stroke itself.
|
|
410
297
|
swatches: () => state.series.map((s, i) => resolveSeriesFill(theme().seriesStyles, s, i)),
|
|
411
298
|
legend: () => theme().legend,
|
|
412
|
-
//
|
|
413
|
-
// entries' type and chips: a theme tween re-paints the swatches only.
|
|
299
|
+
// Spacing is fixed as built; a theme tween re-paints the swatches only.
|
|
414
300
|
gap: state.theme.legend.itemGap,
|
|
415
301
|
padding: state.theme.legend.padding,
|
|
416
302
|
}) as unknown as Node2D
|
|
@@ -425,14 +311,12 @@ export const LineChart = component2d({
|
|
|
425
311
|
shadow: () => theme().plotArea.shadow,
|
|
426
312
|
xGridStroke: () => theme().xAxis.lineStyle,
|
|
427
313
|
yGridStroke: () => theme().yAxis.lineStyle,
|
|
428
|
-
// A grid line under every axis marker; the canvas skips the edges.
|
|
429
314
|
xMarkers: () => domains().x.ticks,
|
|
430
315
|
yMarkers: () => domains().y.ticks,
|
|
431
316
|
xExtent: () => domains().x.range,
|
|
432
317
|
yExtent: () => domains().y.range,
|
|
433
318
|
}),
|
|
434
|
-
//
|
|
435
|
-
// tinted by the highlight meant to pick it out.
|
|
319
|
+
// Under the lines, so the highlight does not tint the stretch it picks out.
|
|
436
320
|
new ChartRegion({
|
|
437
321
|
box: () => regionBox(state, domains(), plotSize()),
|
|
438
322
|
stroke: () => theme().region.stroke,
|
|
@@ -458,17 +342,13 @@ export const LineChart = component2d({
|
|
|
458
342
|
},
|
|
459
343
|
commands: {
|
|
460
344
|
/**
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* the legend dropped and transparent.
|
|
465
|
-
*
|
|
466
|
-
* `duration` is the **whole** entrance; the beats are shares of it. A chart
|
|
467
|
-
* authored with a `region` lands its call-out last, in the final quarter.
|
|
345
|
+
* Axes stagger in, lines trace left to right, the legend fades up last.
|
|
346
|
+
* `duration` is the whole entrance; a chart with a `region` lands its
|
|
347
|
+
* call-out in the final quarter.
|
|
468
348
|
*/
|
|
469
349
|
enter: {
|
|
470
350
|
args: {},
|
|
471
|
-
// Held from
|
|
351
|
+
// Held from arrival, so it never shows finished ahead of its draw.
|
|
472
352
|
entrance: true,
|
|
473
353
|
easing: easeOut(),
|
|
474
354
|
run: (timeline, state, children) => {
|
|
@@ -477,8 +357,8 @@ export const LineChart = component2d({
|
|
|
477
357
|
const duration = timeline.duration
|
|
478
358
|
const revealing = hasCallout(state) && state.regionReveal === 0
|
|
479
359
|
const body = revealing ? duration * 0.75 : duration
|
|
480
|
-
const axes = body *
|
|
481
|
-
const traced = body *
|
|
360
|
+
const axes = body * ENTER_SHARE.axes
|
|
361
|
+
const traced = body * ENTER_SHARE.marks
|
|
482
362
|
const { legend } = parts
|
|
483
363
|
const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
|
|
484
364
|
// The first frame arms what every later beat moves: a beat ahead of the
|
|
@@ -490,17 +370,15 @@ export const LineChart = component2d({
|
|
|
490
370
|
{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
|
|
491
371
|
{ at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) },
|
|
492
372
|
...parts.lines.map((line) => ({ at: axes, duration: traced, piece: line.run("enter") })),
|
|
493
|
-
...(legend ? [{ at: axes + traced, duration: body *
|
|
373
|
+
...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
|
|
494
374
|
...(revealing ? [{ at: body, duration: duration * 0.25, piece: timeline.animate((t) => ({ regionReveal: t })) }] : []),
|
|
495
375
|
])
|
|
496
376
|
},
|
|
497
377
|
},
|
|
498
378
|
/**
|
|
499
|
-
* Spotlight a
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* up, the old call-out retracts over the first half and the new one draws in
|
|
503
|
-
* over the second, each half eased on its own.
|
|
379
|
+
* Spotlight a slice of the plot in data units; either axis alone is a band.
|
|
380
|
+
* Over a region already up, the old one retracts in the first half and the
|
|
381
|
+
* new one draws in over the second, each half eased on its own.
|
|
504
382
|
*/
|
|
505
383
|
spotlightRegion: {
|
|
506
384
|
args: { region: REGION_ARG },
|
|
@@ -521,18 +399,13 @@ export const LineChart = component2d({
|
|
|
521
399
|
)
|
|
522
400
|
},
|
|
523
401
|
},
|
|
524
|
-
/** Drop the
|
|
402
|
+
/** Drop the call-out. A no-op when nothing is spotlit, which still takes its slot. */
|
|
525
403
|
clearRegion: {
|
|
526
404
|
args: {},
|
|
527
405
|
easing: easeOut(),
|
|
528
406
|
run: (timeline, state) => clearing(timeline, state),
|
|
529
407
|
},
|
|
530
|
-
/**
|
|
531
|
-
* Zoom the plot to a slice of itself, given in **data units**: the named axes
|
|
532
|
-
* rescale so that slice fills the plot, the grid, both marker rows and every
|
|
533
|
-
* series following. An omitted axis is left as it is. The first zoom
|
|
534
|
-
* remembers the domain it left, for `resetZoom`.
|
|
535
|
-
*/
|
|
408
|
+
/** Rescale the named axes so a slice (data units) fills the plot; an omitted axis is left. The first zoom remembers the domain for `resetZoom`. */
|
|
536
409
|
zoomTo: {
|
|
537
410
|
args: { region: REGION_ARG },
|
|
538
411
|
easing: easeOut(),
|
|
@@ -548,7 +421,7 @@ export const LineChart = component2d({
|
|
|
548
421
|
return timeline.animate((t) => ({ home, ...axesAt(from, to, t) }))
|
|
549
422
|
},
|
|
550
423
|
},
|
|
551
|
-
/**
|
|
424
|
+
/** Back to the domain the first zoom left; a hold if it never zoomed. */
|
|
552
425
|
resetZoom: {
|
|
553
426
|
args: {},
|
|
554
427
|
easing: easeOut(),
|
|
@@ -559,26 +432,19 @@ export const LineChart = component2d({
|
|
|
559
432
|
return timeline.animate((t) => ({ home: null, ...axesAt(from, home, t) }))
|
|
560
433
|
},
|
|
561
434
|
},
|
|
562
|
-
/**
|
|
563
|
-
* Show series `index`: **trace** its line in left to right, in lockstep with
|
|
564
|
-
* its legend entry growing in.
|
|
565
|
-
*/
|
|
435
|
+
/** Trace series `index` in, with its legend entry growing in. */
|
|
566
436
|
showSeries: {
|
|
567
437
|
args: { seriesIndex: SERIES_ARG },
|
|
568
438
|
easing: easeOut(),
|
|
569
439
|
run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, true),
|
|
570
440
|
},
|
|
571
|
-
/**
|
|
441
|
+
/** Retrace series `index` out as its legend entry shrinks away. */
|
|
572
442
|
hideSeries: {
|
|
573
443
|
args: { seriesIndex: SERIES_ARG },
|
|
574
444
|
easing: easeOut(),
|
|
575
445
|
run: (timeline, _state, children) => toggling(timeline, children, timeline.args.seriesIndex, false),
|
|
576
446
|
},
|
|
577
|
-
/**
|
|
578
|
-
* Fade every other series — each line **and its legend entry** — to focus on
|
|
579
|
-
* `index`, which comes back to full. The legend dims in place, so the row
|
|
580
|
-
* doesn't reflow.
|
|
581
|
-
*/
|
|
447
|
+
/** Fade every other series, line and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
|
|
582
448
|
spotlightSeries: {
|
|
583
449
|
args: { seriesIndex: SERIES_ARG },
|
|
584
450
|
easing: easeOut(),
|