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