@motionscript/charts 0.1.1-alpha.0 → 0.1.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +98 -28
- package/dist/bar-chart/bar-chart.d.ts +233 -39
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +215 -171
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +12 -7
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +56 -42
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +52 -52
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +80 -89
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +4 -4
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +2 -2
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +36 -65
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +68 -117
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-R3Q5EE6E.js +2 -0
- package/dist/browser/chunks/chunk-R3Q5EE6E.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/engine.d.ts +5 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +5 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/kit/cartesian.d.ts +49 -15
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +79 -29
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +32 -24
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +52 -46
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +8 -4
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +30 -20
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/{line-chart → kit}/horizontal-axis.d.ts +31 -6
- package/dist/kit/horizontal-axis.d.ts.map +1 -0
- package/dist/{line-chart → kit}/horizontal-axis.js +23 -24
- package/dist/kit/horizontal-axis.js.map +1 -0
- package/dist/kit/motion.d.ts +5 -0
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +8 -0
- package/dist/kit/motion.js.map +1 -1
- package/dist/{pie-chart → kit}/pie-legend.d.ts +3 -3
- package/dist/kit/pie-legend.d.ts.map +1 -0
- package/dist/{pie-chart → kit}/pie-legend.js +11 -10
- package/dist/kit/pie-legend.js.map +1 -0
- package/dist/kit/polar.d.ts +31 -0
- package/dist/kit/polar.d.ts.map +1 -0
- package/dist/kit/polar.js +33 -0
- package/dist/kit/polar.js.map +1 -0
- package/dist/kit/shared.d.ts +11 -50
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +58 -141
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/style.d.ts +254 -0
- package/dist/kit/style.d.ts.map +1 -0
- package/dist/kit/style.js +197 -0
- package/dist/kit/style.js.map +1 -0
- package/dist/kit/vertical-axis.d.ts +22 -2
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +56 -43
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +95 -3
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +165 -53
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +1 -1
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +7 -8
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/index.d.ts +5 -6
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +3 -4
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +257 -18
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +282 -134
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +51 -34
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +175 -83
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +2 -0
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +15 -0
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +3 -1
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +9 -1
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +2 -3
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +1 -2
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +114 -18
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +37 -24
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +16 -5
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +89 -55
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +53 -61
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +41 -121
- package/dist/pie-chart/shared.js.map +1 -1
- package/dist/radar-chart/index.d.ts +5 -0
- package/dist/radar-chart/index.d.ts.map +1 -0
- package/dist/radar-chart/index.js +5 -0
- package/dist/radar-chart/index.js.map +1 -0
- package/dist/radar-chart/radar-chart.d.ts +218 -0
- package/dist/radar-chart/radar-chart.d.ts.map +1 -0
- package/dist/radar-chart/radar-chart.js +224 -0
- package/dist/radar-chart/radar-chart.js.map +1 -0
- package/dist/radar-chart/radar-grid.d.ts +43 -0
- package/dist/radar-chart/radar-grid.d.ts.map +1 -0
- package/dist/radar-chart/radar-grid.js +61 -0
- package/dist/radar-chart/radar-grid.js.map +1 -0
- package/dist/radar-chart/radar-series.d.ts +59 -0
- package/dist/radar-chart/radar-series.d.ts.map +1 -0
- package/dist/radar-chart/radar-series.js +71 -0
- package/dist/radar-chart/radar-series.js.map +1 -0
- package/dist/radar-chart/shared.d.ts +73 -0
- package/dist/radar-chart/shared.d.ts.map +1 -0
- package/dist/radar-chart/shared.js +123 -0
- package/dist/radar-chart/shared.js.map +1 -0
- package/dist/radial-chart/index.d.ts +6 -0
- package/dist/radial-chart/index.d.ts.map +1 -0
- package/dist/radial-chart/index.js +4 -0
- package/dist/radial-chart/index.js.map +1 -0
- package/dist/radial-chart/radial-chart.d.ts +216 -0
- package/dist/radial-chart/radial-chart.d.ts.map +1 -0
- package/dist/radial-chart/radial-chart.js +174 -0
- package/dist/radial-chart/radial-chart.js.map +1 -0
- package/dist/radial-chart/radial-rings.d.ts +121 -0
- package/dist/radial-chart/radial-rings.d.ts.map +1 -0
- package/dist/radial-chart/radial-rings.js +273 -0
- package/dist/radial-chart/radial-rings.js.map +1 -0
- package/dist/radial-chart/shared.d.ts +73 -0
- package/dist/radial-chart/shared.d.ts.map +1 -0
- package/dist/radial-chart/shared.js +79 -0
- package/dist/radial-chart/shared.js.map +1 -0
- package/package.json +4 -4
- package/registry.json +21 -6
- package/src/bar-chart/bar-chart.ts +252 -179
- package/src/bar-chart/category-axis.ts +46 -40
- package/src/bar-chart/chart-bars.ts +97 -117
- package/src/bar-chart/index.ts +7 -10
- package/src/bar-chart/shared.ts +78 -238
- package/src/engine.ts +5 -2
- package/src/index.ts +2 -0
- package/src/kit/cartesian.ts +118 -40
- package/src/kit/chart-canvas.ts +49 -54
- package/src/kit/chart-legend.ts +28 -20
- package/src/kit/horizontal-axis.ts +114 -0
- package/src/kit/motion.ts +7 -0
- package/src/{pie-chart → kit}/pie-legend.ts +12 -11
- package/src/kit/polar.ts +54 -0
- package/src/kit/shared.ts +55 -277
- package/src/kit/style.ts +257 -0
- package/src/kit/vertical-axis.ts +67 -48
- package/src/line-chart/chart-line.ts +176 -54
- package/src/line-chart/chart-region.ts +6 -10
- package/src/line-chart/index.ts +14 -10
- package/src/line-chart/line-chart.ts +347 -151
- package/src/line-chart/shared.ts +193 -174
- package/src/line-chart/x-scale.ts +13 -0
- package/src/nodes.ts +9 -1
- package/src/pie-chart/index.ts +2 -23
- package/src/pie-chart/pie-chart.ts +52 -35
- package/src/pie-chart/pie-ring.ts +92 -71
- package/src/pie-chart/shared.ts +68 -266
- package/src/radar-chart/index.ts +4 -0
- package/src/radar-chart/radar-chart.ts +287 -0
- package/src/radar-chart/radar-grid.ts +78 -0
- package/src/radar-chart/radar-series.ts +88 -0
- package/src/radar-chart/shared.ts +146 -0
- package/src/radial-chart/index.ts +5 -0
- package/src/radial-chart/radial-chart.ts +210 -0
- package/src/radial-chart/radial-rings.ts +328 -0
- package/src/radial-chart/shared.ts +114 -0
- package/dist/browser/chunks/chunk-4SMRCU24.js +0 -2
- package/dist/browser/chunks/chunk-4SMRCU24.js.map +0 -7
- package/dist/kit/chart-border.d.ts +0 -18
- package/dist/kit/chart-border.d.ts.map +0 -1
- package/dist/kit/chart-border.js +0 -23
- package/dist/kit/chart-border.js.map +0 -1
- package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
- package/dist/line-chart/horizontal-axis.js.map +0 -1
- package/dist/pie-chart/pie-legend.d.ts.map +0 -1
- package/dist/pie-chart/pie-legend.js.map +0 -1
- package/src/kit/chart-border.ts +0 -36
- package/src/line-chart/horizontal-axis.ts +0 -122
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Rect,
|
|
3
|
+
Text,
|
|
3
4
|
Types,
|
|
5
|
+
clamp,
|
|
4
6
|
component2d,
|
|
5
7
|
easeOut,
|
|
6
8
|
type ChildView,
|
|
7
9
|
type DataRecord,
|
|
8
10
|
type NodeOf,
|
|
11
|
+
type OutOf,
|
|
9
12
|
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
10
13
|
type Node2D,
|
|
11
14
|
type Node2DProps as _Node2DProps,
|
|
15
|
+
type Fill as _Fill,
|
|
16
|
+
type FillResolved as _FillResolved,
|
|
17
|
+
type Stroke as _Stroke,
|
|
18
|
+
type StrokeResolved as _StrokeResolved,
|
|
12
19
|
type Size2D,
|
|
20
|
+
type StyleInput as _StyleInput,
|
|
21
|
+
type StyleValues as _StyleValues,
|
|
13
22
|
type Vector2,
|
|
14
23
|
} from "@motionscript/sdk"
|
|
15
24
|
import { formatNumber } from "@motionscript/sdk/component"
|
|
16
25
|
|
|
17
|
-
import { cartesianFrame,
|
|
18
|
-
import { ChartBorder } from "../kit/chart-border"
|
|
26
|
+
import { boundText, cartesianFrame, chartViews, columnGhost, fade, frameGap, headroom, laidOutSize, overhang } from "../kit/cartesian"
|
|
19
27
|
import { ChartCanvas } from "../kit/chart-canvas"
|
|
20
28
|
import { ChartLegend } from "../kit/chart-legend"
|
|
21
29
|
import { SERIES_ARG, asParts, fresh } from "../kit/entries"
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
30
|
+
import { HorizontalAxis } from "../kit/horizontal-axis"
|
|
31
|
+
import { DEFAULT_MARKERS, ENTER_SHARE, beats, legendAway, type AnyTimeline, type Beat } from "../kit/motion"
|
|
24
32
|
import {
|
|
25
33
|
SPOTLIGHT_DIM,
|
|
26
34
|
formatMarker,
|
|
@@ -28,32 +36,52 @@ import {
|
|
|
28
36
|
lerpAxisEntry,
|
|
29
37
|
markerStep,
|
|
30
38
|
markerValues,
|
|
39
|
+
niceRange,
|
|
40
|
+
niceTicks,
|
|
31
41
|
resolveAxisDomain,
|
|
32
|
-
resolveSeriesFill,
|
|
33
|
-
resolveSeriesStroke,
|
|
34
42
|
settleAxisDomain,
|
|
35
43
|
type AxisDomainConfig,
|
|
36
44
|
type Range,
|
|
37
45
|
type ResolvedAxisDomain,
|
|
38
46
|
} from "../kit/shared"
|
|
39
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
CARTESIAN_GRID,
|
|
49
|
+
CHART_COLORS,
|
|
50
|
+
CHART_LEGEND,
|
|
51
|
+
CHART_STYLE_FAMILY,
|
|
52
|
+
CHART_STYLE_PRESETS,
|
|
53
|
+
CHART_TEXT,
|
|
54
|
+
axisConfigOf,
|
|
55
|
+
legendConfigOf,
|
|
56
|
+
seriesFill,
|
|
57
|
+
valueText,
|
|
58
|
+
} from "../kit/style"
|
|
59
|
+
import { VerticalAxis, axisPads } from "../kit/vertical-axis"
|
|
60
|
+
import { ChartArea, ChartLine, traceX } from "./chart-line"
|
|
40
61
|
import { ChartRegion } from "./chart-region"
|
|
41
|
-
import { HorizontalAxis } from "./horizontal-axis"
|
|
42
62
|
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
resolveChartTheme,
|
|
63
|
+
LINE_LABELS,
|
|
64
|
+
LINE_MARKS,
|
|
46
65
|
resolveRegionBox,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
type ChartConfig,
|
|
50
|
-
type ChartTheme,
|
|
66
|
+
stackExtent,
|
|
67
|
+
stackSeries,
|
|
51
68
|
type LineSeries,
|
|
69
|
+
type LineStack,
|
|
52
70
|
type PlotRegion,
|
|
53
71
|
type RegionBox,
|
|
72
|
+
type StackedSeries,
|
|
54
73
|
} from "./shared"
|
|
55
74
|
import { resolveXScale, type XScale } from "./x-scale"
|
|
56
75
|
|
|
76
|
+
const LINE_STYLE = Types.style(
|
|
77
|
+
CHART_STYLE_FAMILY,
|
|
78
|
+
{ colors: CHART_COLORS, text: CHART_TEXT, grid: CARTESIAN_GRID, marks: LINE_MARKS, labels: LINE_LABELS, legend: CHART_LEGEND },
|
|
79
|
+
{ presets: CHART_STYLE_PRESETS }
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
/** A line chart's resolved style. */
|
|
83
|
+
export type LineChartStyle = OutOf<typeof LINE_STYLE>
|
|
84
|
+
|
|
57
85
|
/** Both axes' domains with their ranges and ticks resolved. */
|
|
58
86
|
export interface LineChartDomains {
|
|
59
87
|
readonly x: ResolvedAxisDomain
|
|
@@ -65,8 +93,13 @@ export interface LineChartValues {
|
|
|
65
93
|
readonly data: DataRecord[]
|
|
66
94
|
readonly xField: string
|
|
67
95
|
readonly series: LineSeries[]
|
|
96
|
+
readonly stack: LineStack
|
|
68
97
|
readonly xAxis: AxisDomainConfig
|
|
69
98
|
readonly yAxis: AxisDomainConfig
|
|
99
|
+
/** Each series' share of a stack, `1` when missing. */
|
|
100
|
+
readonly weights?: readonly number[]
|
|
101
|
+
/** An area must reach its baseline, so a chart with areas keeps zero on the scale. */
|
|
102
|
+
readonly style?: { readonly area: string }
|
|
70
103
|
}
|
|
71
104
|
|
|
72
105
|
/** One argument, two controls: the commands take a `PlotRegion`, so the x and y spans a host draws are declared inside it. */
|
|
@@ -78,19 +111,28 @@ const REGION_ARG = Types.custom<PlotRegion | undefined>(undefined, {
|
|
|
78
111
|
},
|
|
79
112
|
})
|
|
80
113
|
|
|
81
|
-
/** What the chart's own commands read: its entries, and the private
|
|
114
|
+
/** What the chart's own commands read: its entries, and the private state they write. */
|
|
82
115
|
interface LineState extends LineChartValues {
|
|
83
|
-
readonly
|
|
116
|
+
readonly style: LineChartStyle
|
|
84
117
|
readonly region: PlotRegion | null
|
|
85
118
|
readonly regionReveal: number
|
|
86
119
|
readonly home: LineChartDomains | null
|
|
120
|
+
readonly weights: number[]
|
|
87
121
|
}
|
|
88
122
|
|
|
123
|
+
/** The share of the y axis a percent stack reads to. */
|
|
124
|
+
const PERCENT: Range = [0, 100]
|
|
125
|
+
|
|
89
126
|
/** What the x column is (a number, an instant, or a place in the file), and so how rows are positioned, ticked and captioned. */
|
|
90
127
|
function columnOf(s: LineChartValues, format = s.xAxis.format): XScale {
|
|
91
128
|
return resolveXScale(s.data, s.xField, format)
|
|
92
129
|
}
|
|
93
130
|
|
|
131
|
+
/** Every series as drawn; `weighted` false reads each at full share, as the scale does so the ticks hold still while a series sinks out. */
|
|
132
|
+
function stacksOf(s: LineChartValues, weighted = true): StackedSeries[] {
|
|
133
|
+
return stackSeries(s.data, columnOf(s), s.series, s.stack, weighted ? s.weights : undefined)
|
|
134
|
+
}
|
|
135
|
+
|
|
94
136
|
/** The explicit range and ticks, else the column's extent and ticks: only a numeric column has round numbers between its samples. */
|
|
95
137
|
function drawnX(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
|
|
96
138
|
const column = columnOf(s, domain.format)
|
|
@@ -98,46 +140,83 @@ function drawnX(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig
|
|
|
98
140
|
return { ...domain, range, ticks: domain.ticks ?? column.ticks(range, DEFAULT_MARKERS - 1) }
|
|
99
141
|
}
|
|
100
142
|
|
|
143
|
+
/** The y range the data asks for: a percent stack reads to 100; areas and stacks keep zero on the scale. */
|
|
144
|
+
function autoY(s: LineChartValues, divisions: number): Range {
|
|
145
|
+
const [lo, hi] = stackExtent(stacksOf(s, false))
|
|
146
|
+
if (s.stack === "percent" && lo >= 0) return PERCENT
|
|
147
|
+
const zero = s.stack !== "none" || (s.style?.area ?? "none") !== "none"
|
|
148
|
+
return niceRange(zero ? [Math.min(0, lo), Math.max(0, hi)] : [lo, hi], divisions)
|
|
149
|
+
}
|
|
150
|
+
|
|
101
151
|
/** Auto-ranging steps into one fewer segment than the marker count, so the tidy bounds frame the ticks drawn. */
|
|
102
152
|
function drawnY(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig {
|
|
103
153
|
const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
|
|
104
|
-
const range =
|
|
105
|
-
|
|
154
|
+
const range = domain.range ?? autoY(s, divisions)
|
|
155
|
+
const even = domain.range !== undefined || (s.stack === "percent" && range === PERCENT)
|
|
156
|
+
return { ...domain, range, ticks: domain.ticks ?? (even ? markerValues(range, DEFAULT_MARKERS - 1) : niceTicks(range, DEFAULT_MARKERS - 1)) }
|
|
106
157
|
}
|
|
107
158
|
|
|
108
159
|
/** The last domains worked out from each data array, and what else they were read from. */
|
|
109
|
-
const domainsDrawn = new WeakMap<
|
|
110
|
-
DataRecord[],
|
|
111
|
-
Pick<LineChartValues, "xField" | "xAxis" | "yAxis" | "series"> & { readonly domains: LineChartDomains }
|
|
112
|
-
>()
|
|
160
|
+
const domainsDrawn = new WeakMap<DataRecord[], { readonly key: readonly unknown[]; readonly domains: LineChartDomains }>()
|
|
113
161
|
|
|
114
162
|
/** Both axes as drawn now. */
|
|
115
163
|
export function lineChartDomains(s: LineChartValues): LineChartDomains {
|
|
116
|
-
const
|
|
117
|
-
const kept = domainsDrawn.get(data)
|
|
118
|
-
if (kept && kept.
|
|
119
|
-
return kept.domains
|
|
120
|
-
}
|
|
164
|
+
const key = [s.xField, s.xAxis, s.yAxis, s.series, s.stack, s.style?.area]
|
|
165
|
+
const kept = domainsDrawn.get(s.data)
|
|
166
|
+
if (kept && kept.key.every((value, i) => value === key[i])) return kept.domains
|
|
121
167
|
const domains = {
|
|
122
|
-
x: settleAxisDomain(xAxis, (d) => drawnX(s, d)) as LineChartDomains["x"],
|
|
123
|
-
y: settleAxisDomain(yAxis, (d) => drawnY(s, d)) as LineChartDomains["y"],
|
|
168
|
+
x: settleAxisDomain(s.xAxis, (d) => drawnX(s, d)) as LineChartDomains["x"],
|
|
169
|
+
y: settleAxisDomain(s.yAxis, (d) => drawnY(s, d)) as LineChartDomains["y"],
|
|
124
170
|
}
|
|
125
|
-
domainsDrawn.set(data, {
|
|
171
|
+
domainsDrawn.set(s.data, { key, domains })
|
|
126
172
|
return domains
|
|
127
173
|
}
|
|
128
174
|
|
|
129
|
-
/**
|
|
130
|
-
function
|
|
131
|
-
const series = s.series[index]
|
|
132
|
-
if (!series) return []
|
|
133
|
-
const points = seriesPoints(s.data, columnOf(s), series)
|
|
134
|
-
if (points.length === 0) return []
|
|
135
|
-
const { width: w, height: h } = plot
|
|
175
|
+
/** Data to the plot's centred pixel space (origin at its centre, y-up). */
|
|
176
|
+
function mapper(domains: LineChartDomains, plot: Size2D) {
|
|
136
177
|
const [xMin, xMax] = domains.x.range
|
|
137
178
|
const [yMin, yMax] = domains.y.range
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
179
|
+
return {
|
|
180
|
+
x: (x: number) => (xMax <= xMin ? 0 : ((x - xMin) / (xMax - xMin) - 0.5) * plot.width),
|
|
181
|
+
y: (y: number) => (yMax === yMin ? 0 : ((y - yMin) / (yMax - yMin) - 0.5) * plot.height),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface SeriesPixels {
|
|
186
|
+
readonly points: Vector2[]
|
|
187
|
+
/** The line under the series in a stack; empty when it fills to the baseline. */
|
|
188
|
+
readonly base: Vector2[]
|
|
189
|
+
/** What each point reads: its own value, or its share in a percent stack. */
|
|
190
|
+
readonly values: number[]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const NO_PIXELS: SeriesPixels = { points: [], base: [], values: [] }
|
|
194
|
+
|
|
195
|
+
/** Series `index` in the plot's centred pixel space. */
|
|
196
|
+
function seriesPixels(s: LineChartValues, domains: LineChartDomains, index: number, plot: Size2D): SeriesPixels {
|
|
197
|
+
const stack = stacksOf(s)[index]
|
|
198
|
+
if (!stack) return NO_PIXELS
|
|
199
|
+
const map = mapper(domains, plot)
|
|
200
|
+
const toPx = ([x, y]: [number, number]): Vector2 => ({ x: map.x(x), y: map.y(y) })
|
|
201
|
+
return {
|
|
202
|
+
points: stack.points.map(toPx),
|
|
203
|
+
base: stack.base ? stack.base.map(toPx) : [],
|
|
204
|
+
values: stack.points.map(([, y], k) => y - (stack.base?.[k]?.[1] ?? 0)),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Where the areas fill down to: zero, held inside the scale. */
|
|
209
|
+
function baselineValue(domains: LineChartDomains): number {
|
|
210
|
+
const [lo, hi] = domains.y.range
|
|
211
|
+
return clamp(0, Math.min(lo, hi), Math.max(lo, hi))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Whether anything plotted runs past the scale, so the plot clips it: a zoom, or an author's range narrower than the data. */
|
|
215
|
+
function overflows(s: LineChartValues, domains: LineChartDomains): boolean {
|
|
216
|
+
const [ylo, yhi] = stackExtent(stacksOf(s, false))
|
|
217
|
+
const [xlo, xhi] = columnOf(s).extent
|
|
218
|
+
const within = (v: number, [a, b]: Range) => v >= Math.min(a, b) - 1e-9 && v <= Math.max(a, b) + 1e-9
|
|
219
|
+
return !(within(ylo, domains.y.range) && within(yhi, domains.y.range) && within(xlo, domains.x.range) && within(xhi, domains.x.range))
|
|
141
220
|
}
|
|
142
221
|
|
|
143
222
|
/** `region` in centred pixel space against the live scales, so it travels with a zoom. */
|
|
@@ -175,17 +254,26 @@ function axesAt(from: LineChartDomains, to: Partial<Record<"x" | "y", AxisDomain
|
|
|
175
254
|
}
|
|
176
255
|
}
|
|
177
256
|
|
|
257
|
+
/** A value as the y axis writes it: the author's pattern, a share in a percent stack, else the bare reading at the ticks' precision. */
|
|
258
|
+
function yLabel(s: LineChartValues, value: number, ticks: readonly number[]): string {
|
|
259
|
+
const format = s.yAxis.format
|
|
260
|
+
if (format !== undefined) return formatNumber(value, format)
|
|
261
|
+
const text = formatMarker(value, markerStep(ticks))
|
|
262
|
+
return s.stack === "percent" ? `${text}%` : text
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function weightOf(s: { readonly weights: readonly number[] }, index: number): number {
|
|
266
|
+
return Math.max(0, s.weights[index] ?? 1)
|
|
267
|
+
}
|
|
268
|
+
|
|
178
269
|
/** The parts a line chart's commands drive, found through its children. */
|
|
179
270
|
function partsOf(children: readonly ChildView[]) {
|
|
180
|
-
const
|
|
181
|
-
if (!frame) return null
|
|
182
|
-
const hAxis = frame.below.find(HorizontalAxis.is)
|
|
183
|
-
if (!VerticalAxis.is(frame.axis) || !hAxis) return null
|
|
271
|
+
const views = chartViews(children)
|
|
184
272
|
return {
|
|
185
|
-
vAxis:
|
|
186
|
-
hAxis,
|
|
187
|
-
lines:
|
|
188
|
-
legend:
|
|
273
|
+
vAxis: views.find(VerticalAxis.is) ?? null,
|
|
274
|
+
hAxis: views.find(HorizontalAxis.is) ?? null,
|
|
275
|
+
lines: views.filter(ChartLine.is),
|
|
276
|
+
legend: views.find(ChartLegend.is) ?? null,
|
|
189
277
|
}
|
|
190
278
|
}
|
|
191
279
|
|
|
@@ -198,21 +286,38 @@ function clearing(timeline: AnyTimeline, state: LineState) {
|
|
|
198
286
|
return timeline.animate((t) => (t >= 1 ? { region: null, regionReveal: 0 } : { region, regionReveal: from * (1 - t) }))
|
|
199
287
|
}
|
|
200
288
|
|
|
201
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Show or hide series `index` with its legend entry. Apart, the line retraces;
|
|
291
|
+
* in a stack it sinks into the series below as its share falls to nothing, so
|
|
292
|
+
* the ones above settle rather than leaving a gap.
|
|
293
|
+
*/
|
|
294
|
+
function toggling(timeline: AnyTimeline, state: LineState, children: readonly ChildView[], index: number, show: boolean) {
|
|
202
295
|
const parts = partsOf(children)
|
|
203
|
-
const line = parts
|
|
204
|
-
if (!
|
|
296
|
+
const line = parts.lines[index]
|
|
297
|
+
if (!line) return timeline.wait(timeline.duration)
|
|
298
|
+
const legend = parts.legend ? [parts.legend.run(show ? "showSeries" : "hideSeries", { index })] : []
|
|
299
|
+
if (state.stack === "none") {
|
|
300
|
+
return timeline.together([line.run("enter", show ? { from: 0, to: 1 } : { from: 1, to: 0 }), ...legend])
|
|
301
|
+
}
|
|
302
|
+
const from = state.series.map((_, i) => weightOf(state, i))
|
|
303
|
+
const start = from[index] ?? 1
|
|
304
|
+
const target = show ? 1 : 0
|
|
205
305
|
return timeline.together([
|
|
206
|
-
|
|
207
|
-
|
|
306
|
+
timeline.animate((t) => {
|
|
307
|
+
const weights = [...from]
|
|
308
|
+
weights[index] = start + (target - start) * timeline.ease(t)
|
|
309
|
+
return { weights }
|
|
310
|
+
}, { eased: false }),
|
|
311
|
+
line.run("hold", { shown: show }),
|
|
312
|
+
...legend,
|
|
208
313
|
])
|
|
209
314
|
}
|
|
210
315
|
|
|
211
316
|
/**
|
|
212
|
-
* A multi-series line chart composed of {@link ChartCanvas},
|
|
213
|
-
* {@link
|
|
214
|
-
* {@link ChartLegend}. Every part reads the chart's entries
|
|
215
|
-
* maps against the plot panel's own laid-out box.
|
|
317
|
+
* A multi-series line or area chart composed of {@link ChartCanvas}, the value
|
|
318
|
+
* and x axes, {@link ChartRegion}, a {@link ChartArea} and a {@link ChartLine}
|
|
319
|
+
* per series, and {@link ChartLegend}. Every part reads the chart's entries
|
|
320
|
+
* through bindings and maps against the plot panel's own laid-out box.
|
|
216
321
|
*
|
|
217
322
|
* Resting state is the finished chart; `enter` arms every part on its first
|
|
218
323
|
* frame. A `to` of an axis `range` or `ticks` tweens from the domain as drawn
|
|
@@ -225,126 +330,215 @@ export const LineChart = component2d({
|
|
|
225
330
|
// Not `x`: every node already has an `x`, its position.
|
|
226
331
|
xField: "",
|
|
227
332
|
series: fresh<LineSeries[]>(() => [], { kind: "series" }),
|
|
333
|
+
/** `stacked`: each series sits on the sum of those before it; `percent`: every row reads as shares of 100. */
|
|
334
|
+
stack: Types.oneOf(["none", "stacked", "percent"] as const),
|
|
228
335
|
xAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
229
336
|
yAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
230
|
-
|
|
337
|
+
style: LINE_STYLE,
|
|
231
338
|
/** The spotlit slice in data units, or `null`. The box snaps; {@link regionReveal} animates it. */
|
|
232
339
|
region: Types.custom<PlotRegion | null>(null),
|
|
233
|
-
/** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade
|
|
340
|
+
/** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade. */
|
|
234
341
|
regionReveal: 0,
|
|
235
|
-
/** Read as the chart is built: the legend is a part the chart builds or doesn't. */
|
|
236
|
-
showLegend: Types.boolean(true, { animatable: false }),
|
|
237
342
|
/** The domain `zoomTo` first left, so `resetZoom` restores it, auto-derived ticks included. */
|
|
238
343
|
home: Types.custom<LineChartDomains | null>(null, { private: true }),
|
|
344
|
+
/** Each series' share of a stack, seeded from `enabled`: a weight rather than a switch, so a hidden series sinks out. */
|
|
345
|
+
weights: Types.custom<number[]>([], { private: true }),
|
|
346
|
+
/** Whether the plot clips its marks, from a zoom's first frame until a reset lands. */
|
|
347
|
+
clipped: Types.boolean(false, { private: true }),
|
|
239
348
|
},
|
|
240
349
|
// A node made of parts that fill would otherwise hug them.
|
|
241
|
-
defaults: {
|
|
350
|
+
defaults: {
|
|
351
|
+
width: "fill",
|
|
352
|
+
height: "fill",
|
|
353
|
+
weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
|
|
354
|
+
},
|
|
242
355
|
allowedChildren: [],
|
|
243
356
|
children: (state) => {
|
|
244
|
-
const
|
|
357
|
+
const style = () => state.style
|
|
245
358
|
const domains = () => lineChartDomains(state)
|
|
246
359
|
let plot: Node2D | undefined
|
|
247
360
|
const plotSize = () => laidOutSize(plot)
|
|
361
|
+
const pixels = (i: number) => seriesPixels(state, domains(), i, plotSize())
|
|
248
362
|
const enabled = state.series.map((s) => s.enabled ?? true)
|
|
363
|
+
const built = state.style
|
|
364
|
+
const yConfig = () => axisConfigOf(style(), "y")
|
|
365
|
+
const xConfig = () => axisConfigOf(style(), "x")
|
|
366
|
+
const yCaptions = () => {
|
|
367
|
+
const ticks = domains().y.ticks
|
|
368
|
+
return ticks.map((value) => yLabel(state, value, ticks))
|
|
369
|
+
}
|
|
249
370
|
|
|
250
371
|
// Bound, not snapshotted, so a rescale re-labels and re-places the row as it
|
|
251
372
|
// tweens. The marker *count* is read once, which is why `zoomTo` keeps it.
|
|
252
|
-
const vAxis =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
373
|
+
const vAxis = built.showYAxis
|
|
374
|
+
? (new VerticalAxis({
|
|
375
|
+
range: () => domains().y.range,
|
|
376
|
+
markers: () => domains().y.ticks,
|
|
377
|
+
captions: yCaptions,
|
|
378
|
+
axis: yConfig,
|
|
379
|
+
label: state.yAxis.label ?? "",
|
|
380
|
+
width: "hug",
|
|
381
|
+
height: "fill",
|
|
382
|
+
}) as unknown as Node2D)
|
|
383
|
+
: undefined
|
|
384
|
+
|
|
385
|
+
const hAxis = built.showXAxis
|
|
386
|
+
? (new HorizontalAxis({
|
|
387
|
+
range: () => domains().x.range,
|
|
388
|
+
markers: () => domains().x.ticks,
|
|
389
|
+
// A month or category tick is captioned with its cell (`Jan`), not the instant it parsed to.
|
|
390
|
+
captions: () => {
|
|
391
|
+
const column = columnOf(state)
|
|
392
|
+
return domains().x.ticks.map((value) => column.label(value))
|
|
393
|
+
},
|
|
394
|
+
axis: xConfig,
|
|
395
|
+
label: state.xAxis.label ?? "",
|
|
396
|
+
overhang: () => ({ left: overhang(style()), right: overhang(style()) }),
|
|
397
|
+
width: "fill",
|
|
398
|
+
height: "hug",
|
|
399
|
+
}) as unknown as Node2D)
|
|
400
|
+
: undefined
|
|
401
|
+
|
|
402
|
+
const ghost = vAxis && hAxis
|
|
403
|
+
? columnGhost({
|
|
404
|
+
captions: yCaptions,
|
|
405
|
+
count: lineChartDomains(state).y.ticks.length,
|
|
406
|
+
text: () => yConfig().labelStyle,
|
|
407
|
+
title: state.yAxis.label ?? "",
|
|
408
|
+
titleText: () => yConfig().titleStyle,
|
|
409
|
+
tickPad: () => axisPads(yConfig(), "right").tick,
|
|
410
|
+
titlePad: () => axisPads(yConfig(), "right").title,
|
|
411
|
+
})
|
|
412
|
+
: undefined
|
|
413
|
+
|
|
414
|
+
const spotlight = () => regionBox(state, domains(), plotSize())
|
|
415
|
+
const dimOpacity = () => 1 - (1 - SPOTLIGHT_DIM) * state.regionReveal
|
|
416
|
+
const lines = state.series.map((s, i) =>
|
|
417
|
+
new ChartLine({
|
|
418
|
+
points: () => pixels(i).points,
|
|
419
|
+
curve: () => style().curve,
|
|
420
|
+
stroke: () => ({ weight: style().strokeWidth, fill: seriesFill(style(), s, i), align: "center", cap: "round", join: "round" }),
|
|
421
|
+
dotSize: () => (style().dots ? style().dotSize : 0),
|
|
422
|
+
dotFill: () => seriesFill(style(), s, i),
|
|
423
|
+
enabled: enabled[i],
|
|
424
|
+
spotlight,
|
|
425
|
+
dimOpacity,
|
|
426
|
+
})
|
|
427
|
+
)
|
|
428
|
+
const areas = state.series.map((s, i) =>
|
|
429
|
+
new ChartArea({
|
|
430
|
+
points: () => pixels(i).points,
|
|
431
|
+
base: () => pixels(i).base,
|
|
432
|
+
baseline: () => mapper(domains(), plotSize()).y(baselineValue(domains())),
|
|
433
|
+
curve: () => style().curve,
|
|
434
|
+
area: () => style().area,
|
|
435
|
+
fill: () => seriesFill(style(), s, i),
|
|
436
|
+
areaOpacity: () => style().areaOpacity,
|
|
437
|
+
// The line's own trace, switch and fade, so the two arrive, leave and dim as one.
|
|
438
|
+
progress: () => lines[i].progress,
|
|
439
|
+
enabled: () => lines[i].enabled,
|
|
440
|
+
opacity: () => lines[i].opacity,
|
|
441
|
+
spotlight,
|
|
442
|
+
dimOpacity,
|
|
443
|
+
})
|
|
290
444
|
)
|
|
291
445
|
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
below.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
446
|
+
const labels: Node2D[] = []
|
|
447
|
+
if (built.values !== "none") {
|
|
448
|
+
const below = built.values === "below"
|
|
449
|
+
state.series.forEach((_, i) => {
|
|
450
|
+
state.data.forEach((_row, k) => {
|
|
451
|
+
const line = lines[i]
|
|
452
|
+
const label: Node2D = new Text({
|
|
453
|
+
...boundText(() => ({ ...valueText(style()), fontSize: Math.round(style().fontSize * 0.8) })),
|
|
454
|
+
text: () => {
|
|
455
|
+
const value = pixels(i).values[k]
|
|
456
|
+
return value === undefined ? "" : yLabel(state, value, domains().y.ticks)
|
|
457
|
+
},
|
|
458
|
+
// Held inside the plot, so a figure on an end point does not run into the axis beside it.
|
|
459
|
+
x: () => {
|
|
460
|
+
const half = (plotSize().width - label.layoutBounds.width) / 2
|
|
461
|
+
return clamp(pixels(i).points[k]?.x ?? 0, -Math.max(0, half), Math.max(0, half))
|
|
462
|
+
},
|
|
463
|
+
y: () => {
|
|
464
|
+
const point = pixels(i).points[k]
|
|
465
|
+
const clear = label.layoutBounds.height / 2 + (style().dots ? style().dotSize / 2 : style().strokeWidth / 2) + Math.round(style().fontSize * 0.25)
|
|
466
|
+
return (point?.y ?? 0) + (below ? -clear : clear)
|
|
467
|
+
},
|
|
468
|
+
// In once the trace has passed its point.
|
|
469
|
+
opacity: () => {
|
|
470
|
+
const point = pixels(i).points[k]
|
|
471
|
+
if (!line.enabled || !point) return 0
|
|
472
|
+
const reach = traceX(pixels(i).points, line.progress)
|
|
473
|
+
return line.opacity * (reach === Infinity ? 1 : clamp((reach - point.x) / 24, 0, 1))
|
|
474
|
+
},
|
|
475
|
+
}) as unknown as Node2D
|
|
476
|
+
labels.push(label)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
304
479
|
}
|
|
305
480
|
|
|
481
|
+
const legendAt = built.legend
|
|
482
|
+
const legend = legendAt !== "none"
|
|
483
|
+
? (new ChartLegend({
|
|
484
|
+
series: state.series,
|
|
485
|
+
swatches: () => state.series.map((s, i) => seriesFill(style(), s, i)),
|
|
486
|
+
legend: () => legendConfigOf(style()),
|
|
487
|
+
flow: legendAt === "right" ? "vertical" : "horizontal",
|
|
488
|
+
}) as unknown as Node2D)
|
|
489
|
+
: undefined
|
|
490
|
+
|
|
306
491
|
plot = new Rect({
|
|
307
|
-
clip:
|
|
492
|
+
clip: () => state.clipped || overflows(state, domains()),
|
|
308
493
|
children: [
|
|
309
494
|
new ChartCanvas({
|
|
310
|
-
fill: () =>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
yGridStroke: () => theme().yAxis.lineStyle,
|
|
495
|
+
fill: () => style().background,
|
|
496
|
+
xGridStroke: () => style().verticalGrid,
|
|
497
|
+
yGridStroke: () => style().horizontalGrid,
|
|
314
498
|
xMarkers: () => domains().x.ticks,
|
|
315
499
|
yMarkers: () => domains().y.ticks,
|
|
316
500
|
xExtent: () => domains().x.range,
|
|
317
501
|
yExtent: () => domains().y.range,
|
|
502
|
+
axisStroke: () => style().axisStroke,
|
|
503
|
+
xAxisAt: () => baselineValue(domains()),
|
|
504
|
+
yAxisAt: () => domains().x.range[0],
|
|
318
505
|
}),
|
|
319
|
-
// Under the
|
|
506
|
+
// Under the marks, so the highlight does not tint the stretch it picks out.
|
|
320
507
|
new ChartRegion({
|
|
321
|
-
box:
|
|
322
|
-
stroke: () =>
|
|
323
|
-
fill: () =>
|
|
508
|
+
box: spotlight,
|
|
509
|
+
stroke: () => ({ weight: 2, fill: fade(style().foreground, 0.7), dash: [14, 12], cap: "butt", align: "center" }),
|
|
510
|
+
fill: () => fade(style().foreground, 0.06),
|
|
324
511
|
reveal: () => state.regionReveal,
|
|
325
512
|
}),
|
|
513
|
+
...areas,
|
|
326
514
|
...lines,
|
|
327
|
-
|
|
515
|
+
...labels,
|
|
328
516
|
],
|
|
329
517
|
}) as unknown as Node2D
|
|
330
518
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
519
|
+
return asParts([
|
|
520
|
+
cartesianFrame({
|
|
521
|
+
left: vAxis,
|
|
522
|
+
ghost,
|
|
523
|
+
plot,
|
|
524
|
+
bottom: hAxis,
|
|
525
|
+
legend,
|
|
526
|
+
legendAt,
|
|
527
|
+
gap: () => frameGap(style()),
|
|
528
|
+
padding: () => ({
|
|
529
|
+
top: headroom(style()),
|
|
530
|
+
right: hAxis ? overhang(style()) : 0,
|
|
531
|
+
left: hAxis && !vAxis ? overhang(style()) : 0,
|
|
532
|
+
bottom: 0,
|
|
533
|
+
}),
|
|
534
|
+
}),
|
|
535
|
+
])
|
|
342
536
|
},
|
|
343
537
|
commands: {
|
|
344
538
|
/**
|
|
345
|
-
* Axes stagger in, lines trace left to right, the legend
|
|
346
|
-
* `duration` is the whole entrance; a chart with a `region`
|
|
347
|
-
* call-out in the final quarter.
|
|
539
|
+
* Axes stagger in, lines (and their areas) trace left to right, the legend
|
|
540
|
+
* fades up last. `duration` is the whole entrance; a chart with a `region`
|
|
541
|
+
* lands its call-out in the final quarter.
|
|
348
542
|
*/
|
|
349
543
|
enter: {
|
|
350
544
|
args: {},
|
|
@@ -353,7 +547,6 @@ export const LineChart = component2d({
|
|
|
353
547
|
easing: easeOut(),
|
|
354
548
|
run: (timeline, state, children) => {
|
|
355
549
|
const parts = partsOf(children)
|
|
356
|
-
if (!parts) return timeline.wait(timeline.duration)
|
|
357
550
|
const duration = timeline.duration
|
|
358
551
|
const revealing = hasCallout(state) && state.regionReveal === 0
|
|
359
552
|
const body = revealing ? duration * 0.75 : duration
|
|
@@ -361,17 +554,20 @@ export const LineChart = component2d({
|
|
|
361
554
|
const traced = body * ENTER_SHARE.marks
|
|
362
555
|
const { legend } = parts
|
|
363
556
|
const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
|
|
557
|
+
const away = legendAway(state.style.legend)
|
|
364
558
|
// The first frame arms what every later beat moves: a beat ahead of the
|
|
365
559
|
// playhead writes nothing. A beat with nothing to do still takes its share.
|
|
366
560
|
return beats(timeline, [
|
|
367
561
|
...(revealing ? [at0(timeline.animate(() => ({ regionReveal: 0 })))] : []),
|
|
368
562
|
...parts.lines.map((line) => at0(line.to({ progress: 0 }))),
|
|
369
|
-
...(legend ? [at0(legend.to({ opacity: 0,
|
|
370
|
-
{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
|
|
371
|
-
{ at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) },
|
|
563
|
+
...(legend ? [at0(legend.to({ opacity: 0, ...away }))] : []),
|
|
564
|
+
...(parts.vAxis ? [{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) }] : []),
|
|
565
|
+
...(parts.hAxis ? [{ at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) }] : []),
|
|
372
566
|
...parts.lines.map((line) => ({ at: axes, duration: traced, piece: line.run("enter") })),
|
|
373
|
-
...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
|
|
567
|
+
...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
|
|
374
568
|
...(revealing ? [{ at: body, duration: duration * 0.25, piece: timeline.animate((t) => ({ regionReveal: t })) }] : []),
|
|
569
|
+
// Takes the whole slot even with no parts to move.
|
|
570
|
+
{ at: 0, duration, piece: timeline.wait(duration) },
|
|
375
571
|
])
|
|
376
572
|
},
|
|
377
573
|
},
|
|
@@ -418,7 +614,7 @@ export const LineChart = component2d({
|
|
|
418
614
|
...(region.x && { x: { ...from.x, ...rescale(region.x, from.x.ticks) } }),
|
|
419
615
|
...(region.y && { y: { ...from.y, ...rescale(region.y, from.y.ticks) } }),
|
|
420
616
|
}
|
|
421
|
-
return timeline.animate((t) => ({ home, ...axesAt(from, to, t) }))
|
|
617
|
+
return timeline.animate((t) => ({ home, clipped: true, ...axesAt(from, to, t) }))
|
|
422
618
|
},
|
|
423
619
|
},
|
|
424
620
|
/** Back to the domain the first zoom left; a hold if it never zoomed. */
|
|
@@ -429,32 +625,32 @@ export const LineChart = component2d({
|
|
|
429
625
|
const home = state.home
|
|
430
626
|
if (!home) return timeline.wait(timeline.duration)
|
|
431
627
|
const from = lineChartDomains(state)
|
|
432
|
-
return timeline.animate((t) => ({ home: null, ...axesAt(from, home, t) }))
|
|
628
|
+
return timeline.animate((t) => ({ home: null, clipped: t < 1, ...axesAt(from, home, t) }))
|
|
433
629
|
},
|
|
434
630
|
},
|
|
435
|
-
/**
|
|
631
|
+
/** Bring series `index` in, with its legend entry growing in: traced, or in a stack risen out of the series below. */
|
|
436
632
|
showSeries: {
|
|
437
633
|
args: { seriesIndex: SERIES_ARG },
|
|
438
634
|
easing: easeOut(),
|
|
439
|
-
run: (timeline,
|
|
635
|
+
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, true),
|
|
440
636
|
},
|
|
441
|
-
/**
|
|
637
|
+
/** The mirror of `showSeries`. */
|
|
442
638
|
hideSeries: {
|
|
443
639
|
args: { seriesIndex: SERIES_ARG },
|
|
444
640
|
easing: easeOut(),
|
|
445
|
-
run: (timeline,
|
|
641
|
+
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, false),
|
|
446
642
|
},
|
|
447
|
-
/** Fade every other series, line and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
|
|
643
|
+
/** Fade every other series, line, area and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
|
|
448
644
|
spotlightSeries: {
|
|
449
645
|
args: { seriesIndex: SERIES_ARG },
|
|
450
646
|
easing: easeOut(),
|
|
451
647
|
run: (timeline, _state, children) => {
|
|
452
648
|
const parts = partsOf(children)
|
|
453
|
-
if (!parts) return timeline.wait(timeline.duration)
|
|
454
649
|
const index = timeline.args.seriesIndex
|
|
455
650
|
return timeline.together([
|
|
456
651
|
...parts.lines.map((line, i) => line.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
|
|
457
652
|
...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
|
|
653
|
+
timeline.wait(timeline.duration),
|
|
458
654
|
])
|
|
459
655
|
},
|
|
460
656
|
},
|