@motionscript/charts 0.1.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 +38 -0
- package/README.md +98 -28
- package/dist/bar-chart/bar-chart.d.ts +242 -119
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +231 -260
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +16 -38
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +59 -68
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +60 -91
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +88 -125
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +5 -13
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +2 -9
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +40 -132
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +70 -158
- 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 +8 -17
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +8 -17
- 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 -35
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +80 -42
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +37 -40
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +53 -47
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +16 -59
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +38 -76
- 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/horizontal-axis.d.ts +77 -0
- package/dist/kit/horizontal-axis.d.ts.map +1 -0
- package/dist/kit/horizontal-axis.js +99 -0
- package/dist/kit/horizontal-axis.js.map +1 -0
- package/dist/kit/motion.d.ts +16 -3
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +15 -1
- package/dist/kit/motion.js.map +1 -1
- package/dist/{pie-chart → kit}/pie-legend.d.ts +14 -29
- package/dist/kit/pie-legend.d.ts.map +1 -0
- package/dist/kit/pie-legend.js +93 -0
- 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 +60 -214
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +91 -291
- 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 +36 -45
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +69 -81
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +107 -40
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +172 -89
- 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 +13 -28
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/index.d.ts +5 -18
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +3 -16
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +272 -119
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +309 -257
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +73 -130
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +195 -156
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +16 -55
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +44 -111
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +3 -13
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +9 -13
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +3 -15
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +1 -11
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +147 -85
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +65 -86
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +65 -196
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +154 -292
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +78 -172
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +54 -177
- 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 +274 -315
- package/src/bar-chart/category-axis.ts +53 -88
- package/src/bar-chart/chart-bars.ts +108 -205
- package/src/bar-chart/index.ts +8 -19
- package/src/bar-chart/shared.ts +81 -309
- package/src/engine.ts +8 -17
- package/src/index.ts +2 -0
- package/src/kit/cartesian.ts +118 -55
- package/src/kit/chart-canvas.ts +50 -80
- package/src/kit/chart-legend.ts +36 -97
- package/src/kit/entries.ts +4 -1
- package/src/kit/horizontal-axis.ts +114 -0
- package/src/kit/motion.ts +18 -2
- package/src/{pie-chart → kit}/pie-legend.ts +25 -65
- package/src/kit/polar.ts +54 -0
- package/src/kit/shared.ts +104 -487
- package/src/kit/style.ts +257 -0
- package/src/kit/vertical-axis.ts +81 -115
- package/src/line-chart/chart-line.ts +185 -91
- package/src/line-chart/chart-region.ts +14 -31
- package/src/line-chart/index.ts +14 -22
- package/src/line-chart/line-chart.ts +377 -315
- package/src/line-chart/shared.ts +229 -304
- package/src/line-chart/x-scale.ts +44 -121
- package/src/nodes.ts +9 -13
- package/src/pie-chart/index.ts +3 -35
- package/src/pie-chart/pie-chart.ts +83 -134
- package/src/pie-chart/pie-ring.ts +162 -368
- package/src/pie-chart/shared.ts +92 -379
- 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-TJNHLQGV.js +0 -2
- package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
- package/dist/kit/chart-border.d.ts +0 -27
- package/dist/kit/chart-border.d.ts.map +0 -1
- package/dist/kit/chart-border.js +0 -28
- package/dist/kit/chart-border.js.map +0 -1
- package/dist/line-chart/horizontal-axis.d.ts +0 -79
- package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
- package/dist/line-chart/horizontal-axis.js +0 -129
- 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 +0 -117
- package/dist/pie-chart/pie-legend.js.map +0 -1
- package/src/kit/chart-border.ts +0 -50
- package/src/line-chart/horizontal-axis.ts +0 -151
|
@@ -1,271 +1,231 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Rect,
|
|
3
3
|
Types,
|
|
4
|
+
clamp,
|
|
4
5
|
component2d,
|
|
5
6
|
easeOut,
|
|
6
7
|
type ChildView,
|
|
7
8
|
type DataRecord,
|
|
8
|
-
type EntryOptions,
|
|
9
9
|
type NodeOf,
|
|
10
|
-
|
|
10
|
+
type OutOf,
|
|
11
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
11
12
|
type Node2D,
|
|
12
|
-
type Node2DProps,
|
|
13
|
+
type Node2DProps as _Node2DProps,
|
|
14
|
+
type Fill as _Fill,
|
|
15
|
+
type FillResolved as _FillResolved,
|
|
16
|
+
type Stroke as _Stroke,
|
|
17
|
+
type StrokeResolved as _StrokeResolved,
|
|
13
18
|
type Size2D,
|
|
14
|
-
type
|
|
19
|
+
type StyleInput as _StyleInput,
|
|
20
|
+
type StyleValues as _StyleValues,
|
|
15
21
|
} from "@motionscript/sdk"
|
|
16
22
|
import { formatNumber } from "@motionscript/sdk/component"
|
|
17
23
|
|
|
18
|
-
import { cartesianFrame,
|
|
19
|
-
import { ChartBorder } from "../kit/chart-border"
|
|
24
|
+
import { cartesianFrame, chartViews, columnGhost, frameGap, headroom, laidOutSize, overhang } from "../kit/cartesian"
|
|
20
25
|
import { ChartCanvas } from "../kit/chart-canvas"
|
|
21
26
|
import { ChartLegend } from "../kit/chart-legend"
|
|
22
|
-
import { asParts, fresh } from "../kit/entries"
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
27
|
+
import { SERIES_ARG, asParts, fresh } from "../kit/entries"
|
|
28
|
+
import { HorizontalAxis } from "../kit/horizontal-axis"
|
|
29
|
+
import { DEFAULT_MARKERS, ENTER_SHARE, beats, legendAway, type AnyTimeline, type Beat } from "../kit/motion"
|
|
25
30
|
import {
|
|
26
|
-
|
|
31
|
+
SPOTLIGHT_DIM,
|
|
27
32
|
formatMarker,
|
|
28
33
|
lerpAxisDomain,
|
|
29
34
|
lerpAxisEntry,
|
|
30
35
|
markerStep,
|
|
31
36
|
markerValues,
|
|
37
|
+
niceTicks,
|
|
32
38
|
num,
|
|
33
39
|
resolveAxisDomain,
|
|
34
40
|
settleAxisDomain,
|
|
35
41
|
type AxisDomainConfig,
|
|
36
42
|
type Range,
|
|
43
|
+
type ResolvedAxisDomain,
|
|
37
44
|
} from "../kit/shared"
|
|
38
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
CARTESIAN_GRID,
|
|
47
|
+
CHART_COLORS,
|
|
48
|
+
CHART_LEGEND,
|
|
49
|
+
CHART_STYLE_FAMILY,
|
|
50
|
+
CHART_STYLE_PRESETS,
|
|
51
|
+
CHART_TEXT,
|
|
52
|
+
axisConfigOf,
|
|
53
|
+
legendConfigOf,
|
|
54
|
+
seriesFill,
|
|
55
|
+
valueText,
|
|
56
|
+
} from "../kit/style"
|
|
57
|
+
import { VerticalAxis, axisPads } from "../kit/vertical-axis"
|
|
58
|
+
import { CategoryAxis, slotCentre } from "./category-axis"
|
|
39
59
|
import { ChartBars, type BarGeometry } from "./chart-bars"
|
|
40
60
|
import {
|
|
41
|
-
|
|
61
|
+
BAR_LABELS,
|
|
62
|
+
BAR_MARKS,
|
|
42
63
|
barCategories,
|
|
43
64
|
barSpans,
|
|
44
|
-
lerpBarChartTheme,
|
|
45
|
-
resolveBarChartTheme,
|
|
46
|
-
resolveBarFill,
|
|
47
65
|
resolveBarScale,
|
|
48
|
-
|
|
49
|
-
type BarChartConfig,
|
|
50
|
-
type BarChartTheme,
|
|
66
|
+
type BarOrientation,
|
|
51
67
|
type BarSeries,
|
|
68
|
+
type BarStack,
|
|
52
69
|
} from "./shared"
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
categoryField: string
|
|
63
|
-
/** One entry per bar series: which column it plots, its `label`, its `fill`. */
|
|
64
|
-
series: BarSeries[]
|
|
65
|
-
/**
|
|
66
|
-
* When `true` every category's series stack into one column; when `false`
|
|
67
|
-
* (the default) each series gets its own bar, grouped side by side within the
|
|
68
|
-
* category's slot.
|
|
69
|
-
*/
|
|
70
|
-
stacked?: boolean
|
|
71
|
-
/** The value axis's domain: caption, scale range, and tick placement. */
|
|
72
|
-
valueAxis?: AxisDomainConfig
|
|
73
|
-
/** The category axis's domain. Only its `label` is read — the slots are the rows. */
|
|
74
|
-
categoryAxis?: AxisDomainConfig
|
|
75
|
-
/**
|
|
76
|
-
* The chart's visual theme — bar palette and geometry, axis furniture, plot
|
|
77
|
-
* panel paint, legend, value figures. Every branch is optional and deep-merged
|
|
78
|
-
* onto {@link DEFAULT_BAR_CHART_THEME}.
|
|
79
|
-
*/
|
|
80
|
-
theme?: BarChartTheme
|
|
81
|
-
/** Whether the legend row below the plot is drawn at all. */
|
|
82
|
-
showLegend?: boolean
|
|
83
|
-
/** Whether each column prints the figure it plots. */
|
|
84
|
-
showValues?: boolean
|
|
85
|
-
}
|
|
71
|
+
const BAR_STYLE = Types.style(
|
|
72
|
+
CHART_STYLE_FAMILY,
|
|
73
|
+
{ colors: CHART_COLORS, text: CHART_TEXT, grid: CARTESIAN_GRID, marks: BAR_MARKS, labels: BAR_LABELS, legend: CHART_LEGEND },
|
|
74
|
+
{ presets: CHART_STYLE_PRESETS }
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/** A bar chart's resolved style. */
|
|
78
|
+
export type BarChartStyle = OutOf<typeof BAR_STYLE>
|
|
86
79
|
|
|
87
80
|
/** What a bar chart plots, as its entries hold it. */
|
|
88
81
|
export interface BarChartValues {
|
|
89
82
|
readonly data: DataRecord[]
|
|
90
83
|
readonly series: BarSeries[]
|
|
91
|
-
readonly
|
|
84
|
+
readonly stack: BarStack
|
|
92
85
|
readonly valueAxis: AxisDomainConfig
|
|
93
86
|
}
|
|
94
87
|
|
|
95
88
|
/** The value axis's domain with its range and ticks resolved. */
|
|
96
|
-
export type BarChartDomain =
|
|
97
|
-
|
|
98
|
-
/** The argument every per-series command takes: an index into `series`, offered by name. */
|
|
99
|
-
const SERIES_ARG = Types.custom<number, number, EntryOptions & { readonly min: number; readonly step: number }>(0, { kind: "series", min: 0, step: 1 })
|
|
89
|
+
export type BarChartDomain = ResolvedAxisDomain
|
|
100
90
|
|
|
101
|
-
/** How far the legend starts below its slot when it enters, in px. */
|
|
102
|
-
const LEGEND_SLIDE = 20
|
|
103
|
-
|
|
104
|
-
/** Fallback marker count when the value axis states no ticks. */
|
|
105
|
-
const DEFAULT_MARKERS = 5
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* How `enter` divides its duration between its three beats: the axes arriving,
|
|
109
|
-
* the columns rising, and the legend coming up. Shares of the whole, so they
|
|
110
|
-
* must sum to 1.
|
|
111
|
-
*/
|
|
112
|
-
const AXES_SHARE = 0.35
|
|
113
|
-
const BARS_SHARE = 0.5
|
|
114
|
-
const LEGEND_SHARE = 0.15
|
|
115
|
-
|
|
116
|
-
/** What the geometry reads: what the chart plots, its theme and the signals its commands move. */
|
|
117
91
|
interface BarState extends BarChartValues {
|
|
118
|
-
readonly
|
|
92
|
+
readonly categoryField: string
|
|
93
|
+
readonly orientation: BarOrientation
|
|
94
|
+
readonly style: BarChartStyle
|
|
119
95
|
readonly weights: number[]
|
|
96
|
+
readonly showing: boolean[]
|
|
120
97
|
readonly spotlight: number | null
|
|
121
98
|
readonly spotlightReveal: number
|
|
122
99
|
}
|
|
123
100
|
|
|
124
|
-
/** The value axis with its range and ticks resolved: the explicit ones, else five across the scale. */
|
|
125
101
|
function drawnValue(s: BarChartValues, domain: AxisDomainConfig): AxisDomainConfig {
|
|
126
102
|
const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
|
|
127
|
-
const range = resolveBarScale(s.data, s.series, s.
|
|
128
|
-
|
|
103
|
+
const range = resolveBarScale(s.data, s.series, s.stack, domain.range, divisions)
|
|
104
|
+
const even = domain.range !== undefined || s.stack === "percent"
|
|
105
|
+
return { ...domain, range, ticks: domain.ticks ?? (even ? markerValues(range, DEFAULT_MARKERS - 1) : niceTicks(range, DEFAULT_MARKERS - 1)) }
|
|
129
106
|
}
|
|
130
107
|
|
|
131
|
-
/** The last domain worked out from each data array, and what
|
|
132
|
-
const domainsDrawn = new WeakMap<DataRecord[],
|
|
108
|
+
/** The last domain worked out from each data array, and what it was read from. */
|
|
109
|
+
const domainsDrawn = new WeakMap<DataRecord[], { readonly key: readonly unknown[]; readonly domain: BarChartDomain }>()
|
|
133
110
|
|
|
134
|
-
/** The value axis as
|
|
111
|
+
/** The value axis as drawn now. A series being hidden keeps its room, so the ticks hold still while it goes. */
|
|
135
112
|
export function barChartDomain(s: BarChartValues): BarChartDomain {
|
|
136
|
-
const
|
|
137
|
-
const kept = domainsDrawn.get(data)
|
|
138
|
-
if (kept && kept.
|
|
139
|
-
const domain = settleAxisDomain(valueAxis, (d) => drawnValue(s, d)) as BarChartDomain
|
|
140
|
-
domainsDrawn.set(data, {
|
|
113
|
+
const key = [s.series, s.stack, s.valueAxis]
|
|
114
|
+
const kept = domainsDrawn.get(s.data)
|
|
115
|
+
if (kept && kept.key.every((value, i) => value === key[i])) return kept.domain
|
|
116
|
+
const domain = settleAxisDomain(s.valueAxis, (d) => drawnValue(s, d)) as BarChartDomain
|
|
117
|
+
domainsDrawn.set(s.data, { key, domain })
|
|
141
118
|
return domain
|
|
142
119
|
}
|
|
143
120
|
|
|
144
|
-
/**
|
|
145
|
-
* A value written down as this chart's value axis says: the ticks and the
|
|
146
|
-
* figures printed on the columns both read through here, so the two cannot
|
|
147
|
-
* disagree about what a number looks like.
|
|
148
|
-
*/
|
|
121
|
+
/** Ticks and the figures printed by bars both read through here, so they cannot disagree about how a number looks. */
|
|
149
122
|
function valueLabel(s: BarChartValues, domain: BarChartDomain, value: number): string {
|
|
150
123
|
const pattern = s.valueAxis.format
|
|
151
|
-
|
|
124
|
+
if (pattern !== undefined) return formatNumber(value, pattern)
|
|
125
|
+
const text = formatMarker(value, markerStep(domain.ticks))
|
|
126
|
+
return s.stack === "percent" ? `${text}%` : text
|
|
152
127
|
}
|
|
153
128
|
|
|
154
|
-
/** A value
|
|
155
|
-
function
|
|
129
|
+
/** A value along the value axis, a run of `length` px centred on 0. */
|
|
130
|
+
function valueToPx(domain: BarChartDomain, length: number, value: number): number {
|
|
156
131
|
const [min, max] = domain.range
|
|
157
|
-
if (max
|
|
158
|
-
return ((value - min) / (max - min) - 0.5) *
|
|
132
|
+
if (max === min) return -length / 2
|
|
133
|
+
return ((value - min) / (max - min) - 0.5) * length
|
|
159
134
|
}
|
|
160
135
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
function baselinePx(domain: BarChartDomain, plot: Size2D): number {
|
|
166
|
-
const [min, max] = domain.range
|
|
167
|
-
return valueToY(domain, plot, Math.min(Math.max(0, min), max))
|
|
136
|
+
/** Where bars grow from: zero held on the scale, so a cropped axis grows them from the plot's edge. */
|
|
137
|
+
function baselineValue(domain: BarChartDomain): number {
|
|
138
|
+
const [lo, hi] = domain.range
|
|
139
|
+
return clamp(0, Math.min(lo, hi), Math.max(lo, hi))
|
|
168
140
|
}
|
|
169
141
|
|
|
170
|
-
|
|
171
|
-
function weightAt(s: BarState, index: number): number {
|
|
142
|
+
function weightAt(s: { readonly weights: readonly number[] }, index: number): number {
|
|
172
143
|
return Math.max(0, s.weights[index] ?? 1)
|
|
173
144
|
}
|
|
174
145
|
|
|
175
|
-
/**
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
146
|
+
/** The plot's run along the value axis and across the categories. */
|
|
147
|
+
function runs(s: BarState, plot: Size2D): { along: number; across: number } {
|
|
148
|
+
return s.orientation === "horizontal" ? { along: plot.width, across: plot.height } : { along: plot.height, across: plot.width }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Category `row`'s slot centre across the plot: left to right, or top to bottom. */
|
|
152
|
+
function slotAt(s: BarState, row: number, across: number): number {
|
|
153
|
+
const centre = slotCentre(row, s.data.length, across)
|
|
154
|
+
return s.orientation === "horizontal" ? -centre : centre
|
|
183
155
|
}
|
|
184
156
|
|
|
185
157
|
/**
|
|
186
|
-
* Series `index`'s
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
158
|
+
* Series `index`'s bars in centred pixel space. Side by side, a series divides
|
|
159
|
+
* the slot with its siblings by live weight; stacked, it takes the whole slot
|
|
160
|
+
* and its weight scales its length, so hiding a series reflows rather than
|
|
161
|
+
* punching a hole.
|
|
190
162
|
*/
|
|
191
|
-
function seriesBars(s: BarState, index: number, plot: Size2D
|
|
163
|
+
function seriesBars(s: BarState, index: number, plot: Size2D): BarGeometry[] {
|
|
192
164
|
const series = s.series[index]
|
|
193
165
|
if (!series) return []
|
|
194
166
|
const domain = barChartDomain(s)
|
|
167
|
+
const { along, across } = runs(s, plot)
|
|
195
168
|
const n = s.data.length
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const gap = s.theme.bar.groupGap
|
|
169
|
+
const slot = n > 0 ? across / n : 0
|
|
170
|
+
const inner = slot * clamp(s.style.barWidth, 0, 1)
|
|
171
|
+
const stacked = s.stack !== "none"
|
|
200
172
|
let total = 0
|
|
201
173
|
for (let i = 0; i < s.series.length; i++) total += weightAt(s, i)
|
|
202
174
|
const unit = inner / (total > 0 ? total : 1)
|
|
203
175
|
const weight = weightAt(s, index)
|
|
204
176
|
let before = 0
|
|
205
177
|
for (let i = 0; i < index; i++) before += weightAt(s, i)
|
|
178
|
+
const gap = s.series.length > 1 ? s.style.barGap : 0
|
|
179
|
+
const values = s.style.values
|
|
206
180
|
|
|
207
181
|
return s.data.map((row, rowIndex) => {
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
182
|
+
// Built from live contributions, so a fading series lowers the segments beyond it.
|
|
183
|
+
const spans = barSpans(row, s.series, s.stack, s.weights)
|
|
184
|
+
const span = spans[index] ?? { from: 0, to: 0, value: 0, outer: false }
|
|
185
|
+
const centre = slotAt(s, rowIndex, across)
|
|
186
|
+
const offset = -inner / 2 + unit * before + (unit * weight) / 2
|
|
187
|
+
let caption = ""
|
|
188
|
+
if (values !== "none" && span.value !== 0) {
|
|
189
|
+
if (!stacked || values === "inside" || s.stack === "percent") caption = valueLabel(s, domain, stacked ? span.value : num(row, series.field))
|
|
190
|
+
else if (span.outer) caption = valueLabel(s, domain, span.to)
|
|
191
|
+
}
|
|
218
192
|
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
from:
|
|
222
|
-
to:
|
|
223
|
-
round:
|
|
224
|
-
|
|
225
|
-
...(captioned && { caption: valueLabel(s, domain, value) }),
|
|
193
|
+
center: stacked ? centre : centre + (s.orientation === "horizontal" ? -offset : offset),
|
|
194
|
+
thickness: stacked ? inner : Math.max(0, unit * weight - gap),
|
|
195
|
+
from: valueToPx(domain, along, span.from),
|
|
196
|
+
to: valueToPx(domain, along, span.to),
|
|
197
|
+
round: span.outer,
|
|
198
|
+
caption,
|
|
226
199
|
}
|
|
227
200
|
})
|
|
228
201
|
}
|
|
229
202
|
|
|
230
|
-
/**
|
|
231
|
-
* How far each category is dimmed by the spotlight, one entry per row: 1 while
|
|
232
|
-
* nothing is spotlit, falling to the theme's `dimOpacity` for every category but
|
|
233
|
-
* the chosen one as the reveal climbs.
|
|
234
|
-
*/
|
|
203
|
+
/** Per-row opacity: 1 while nothing is spotlit, falling to {@link SPOTLIGHT_DIM} for all but the chosen category. */
|
|
235
204
|
function categoryDims(s: BarState): number[] {
|
|
236
|
-
const focus =
|
|
237
|
-
const dim = s.theme.spotlight.dimOpacity
|
|
205
|
+
const focus = clamp(s.spotlightReveal, 0, 1)
|
|
238
206
|
const chosen = s.spotlight
|
|
239
|
-
return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 -
|
|
207
|
+
return s.data.map((_, i) => (focus <= 0 || chosen === i ? 1 : 1 - (1 - SPOTLIGHT_DIM) * focus))
|
|
240
208
|
}
|
|
241
209
|
|
|
242
|
-
type AnyTimeline = Timeline<any, any>
|
|
243
|
-
|
|
244
210
|
/** The parts a bar chart's commands drive, found through its children. */
|
|
245
211
|
function partsOf(children: readonly ChildView[]) {
|
|
246
|
-
const
|
|
247
|
-
if (!frame) return null
|
|
248
|
-
const cAxis = frame.below.find(CategoryAxis.is)
|
|
249
|
-
if (!VerticalAxis.is(frame.axis) || !cAxis) return null
|
|
212
|
+
const views = chartViews(children)
|
|
250
213
|
return {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
bars:
|
|
254
|
-
legend:
|
|
214
|
+
valueAxis: views.find((view) => VerticalAxis.is(view) || HorizontalAxis.is(view)) ?? null,
|
|
215
|
+
categoryAxis: views.find(CategoryAxis.is) ?? null,
|
|
216
|
+
bars: views.filter(ChartBars.is),
|
|
217
|
+
legend: views.find(ChartLegend.is) ?? null,
|
|
255
218
|
}
|
|
256
219
|
}
|
|
257
220
|
|
|
258
221
|
/**
|
|
259
|
-
* Move series `index`'s
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* arriving forwards does.
|
|
222
|
+
* Move series `index`'s share to `target` with its legend entry. Its bars stay
|
|
223
|
+
* drawn for every frame the move covers and switch off only once a hide has
|
|
224
|
+
* landed, so arriving at a time backwards matches arriving forwards.
|
|
263
225
|
*/
|
|
264
|
-
function toggling(timeline: AnyTimeline, state: BarState
|
|
226
|
+
function toggling(timeline: AnyTimeline, state: BarState, children: readonly ChildView[], index: number, target: number) {
|
|
265
227
|
const parts = partsOf(children)
|
|
266
|
-
if (!parts
|
|
267
|
-
// Read through `weightAt`, so a chart whose stored weights are short still
|
|
268
|
-
// tweens from the 1 it is drawing at.
|
|
228
|
+
if (!parts.bars[index]) return timeline.wait(timeline.duration)
|
|
269
229
|
const from = state.series.map((_, i) => weightAt(state, i))
|
|
270
230
|
const start = from[index] ?? 1
|
|
271
231
|
const showing = state.series.map((s, i) => state.showing[i] ?? (s.enabled ?? true))
|
|
@@ -282,61 +242,42 @@ function toggling(timeline: AnyTimeline, state: BarState & { readonly showing: b
|
|
|
282
242
|
}
|
|
283
243
|
|
|
284
244
|
/**
|
|
285
|
-
* A
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* - {@link CategoryAxis} — the row of names beneath the baseline, one per row.
|
|
291
|
-
* - {@link ChartBars} (one per series) — that series' column in every category,
|
|
292
|
-
* plus the figures printed on them.
|
|
293
|
-
* - `ChartLegend` — a row of entries below the chart, one per series.
|
|
245
|
+
* A bar chart, vertical or horizontal, its series side by side, stacked, or
|
|
246
|
+
* stacked as shares of 100. Built like the line chart from `ChartCanvas`, a
|
|
247
|
+
* value axis, a {@link CategoryAxis}, one {@link ChartBars} per series and
|
|
248
|
+
* `ChartLegend`. Zero is always on the scale and bars grow about it, so a
|
|
249
|
+
* negative value draws the other way from the same `growth` signal.
|
|
294
250
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
* a negative value draws downward out of the same `growth` signal.
|
|
299
|
-
*
|
|
300
|
-
* **Resting state is the finished chart**; `enter` arms every part on its first
|
|
301
|
-
* frame. `showSeries`/`hideSeries`/`spotlightSeries` pick out a *series* (hiding
|
|
302
|
-
* one reflows the group), `spotlightCategory`/`clearCategory` a *column of the
|
|
303
|
-
* table*. A `to` of the value axis's `range` or `ticks` tweens from the domain
|
|
304
|
-
* as drawn, so an auto axis rescales the columns continuously.
|
|
251
|
+
* Resting state is the finished chart; `enter` arms every part on its first
|
|
252
|
+
* frame. A `to` of the value axis's `range` or `ticks` tweens from the domain as
|
|
253
|
+
* drawn, so an auto axis rescales the bars continuously.
|
|
305
254
|
*/
|
|
306
255
|
export const BarChart = component2d({
|
|
307
256
|
key: "barChart",
|
|
308
257
|
state: {
|
|
309
|
-
data: fresh<DataRecord[]>(() => []),
|
|
258
|
+
data: fresh<DataRecord[]>(() => [], { kind: "dataset" }),
|
|
259
|
+
// Names a column, not a value, like the line chart's `xField`.
|
|
310
260
|
categoryField: "",
|
|
311
|
-
series: fresh<BarSeries[]>(() => []),
|
|
312
|
-
|
|
261
|
+
series: fresh<BarSeries[]>(() => [], { kind: "series" }),
|
|
262
|
+
/** `vertical`: columns rise from a row of categories; `horizontal`: bars grow right from a column of them. */
|
|
263
|
+
orientation: Types.oneOf(["vertical", "horizontal"] as const, { animatable: false }),
|
|
264
|
+
/** `stacked`: each series sits on the sum of those before it; `percent`: every category reads as shares of 100. */
|
|
265
|
+
stack: Types.oneOf(["none", "stacked", "percent"] as const),
|
|
313
266
|
valueAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
314
267
|
categoryAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisDomain }),
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Global rise of every column in `[0, 1]`, measured about the baseline: 0
|
|
318
|
-
* collapses them all onto it, 1 draws them at full length.
|
|
319
|
-
*/
|
|
268
|
+
style: BAR_STYLE,
|
|
269
|
+
/** Rise of every bar about the baseline, `[0, 1]`: 0 collapses them onto it. */
|
|
320
270
|
growth: 1,
|
|
321
|
-
/**
|
|
322
|
-
* How much of the slot each series claims, one entry per series: a weight
|
|
323
|
-
* rather than a switch, so hiding one is the others widening into the space.
|
|
324
|
-
* Seeded from each series' `enabled`.
|
|
325
|
-
*/
|
|
326
|
-
weights: fresh<number[]>(() => []),
|
|
327
|
-
/** The category spotlit, or `null` for none. The choice snaps; `spotlightReveal` is the animation. */
|
|
271
|
+
/** The category spotlit, or `null`. The choice snaps; `spotlightReveal` animates it. */
|
|
328
272
|
spotlight: Types.custom<number | null>(null),
|
|
329
|
-
/** How far the category spotlight is drawn in, `[0, 1]`. */
|
|
330
273
|
spotlightReveal: 0,
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
showValues: Types.boolean(false, { animatable: false }),
|
|
335
|
-
/** Whether each series' columns are drawn: on for every frame a show or hide covers, off once a hide lands. */
|
|
274
|
+
/** Each series' share of the slot, seeded from `enabled`: a weight rather than a switch, so hiding one widens the others. */
|
|
275
|
+
weights: Types.custom<number[]>([], { private: true }),
|
|
276
|
+
/** Whether each series' bars are drawn: on for every frame a show or hide covers, off once a hide lands. */
|
|
336
277
|
showing: Types.custom<boolean[]>([], { private: true }),
|
|
337
278
|
},
|
|
338
279
|
defaults: {
|
|
339
|
-
//
|
|
280
|
+
// A node made of parts that fill would otherwise hug them.
|
|
340
281
|
width: "fill",
|
|
341
282
|
height: "fill",
|
|
342
283
|
weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
|
|
@@ -344,172 +285,194 @@ export const BarChart = component2d({
|
|
|
344
285
|
},
|
|
345
286
|
allowedChildren: [],
|
|
346
287
|
children: (state) => {
|
|
347
|
-
const
|
|
288
|
+
const style = () => state.style
|
|
348
289
|
const domain = () => barChartDomain(state)
|
|
349
290
|
let plot: Node2D | undefined
|
|
350
|
-
// Before the panel is built or laid out it has no size, and the columns map onto nothing.
|
|
351
291
|
const plotSize = () => laidOutSize(plot)
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
292
|
+
const built = state.style
|
|
293
|
+
const horizontal = state.orientation === "horizontal"
|
|
294
|
+
const categories = barCategories(state.data, state.categoryField)
|
|
295
|
+
const valueConfig = () => axisConfigOf(style(), horizontal ? "x" : "y")
|
|
296
|
+
const categoryConfig = () => axisConfigOf(style(), horizontal ? "y" : "x")
|
|
297
|
+
const valueCaptions = () => {
|
|
298
|
+
const d = domain()
|
|
299
|
+
return d.ticks.map((value) => valueLabel(state, d, value))
|
|
300
|
+
}
|
|
301
|
+
const showValue = horizontal ? built.showXAxis : built.showYAxis
|
|
302
|
+
const showCategory = horizontal ? built.showYAxis : built.showXAxis
|
|
303
|
+
|
|
304
|
+
const valueAxis = showValue
|
|
305
|
+
? (horizontal
|
|
306
|
+
? new HorizontalAxis({
|
|
307
|
+
range: () => domain().range,
|
|
308
|
+
markers: () => domain().ticks,
|
|
309
|
+
captions: valueCaptions,
|
|
310
|
+
axis: valueConfig,
|
|
311
|
+
label: state.valueAxis.label ?? "",
|
|
312
|
+
overhang: () => ({ left: overhang(style()), right: overhang(style()) }),
|
|
313
|
+
width: "fill",
|
|
314
|
+
height: "hug",
|
|
315
|
+
})
|
|
316
|
+
: new VerticalAxis({
|
|
317
|
+
range: () => domain().range,
|
|
318
|
+
markers: () => domain().ticks,
|
|
319
|
+
captions: valueCaptions,
|
|
320
|
+
axis: valueConfig,
|
|
321
|
+
label: state.valueAxis.label ?? "",
|
|
322
|
+
width: "hug",
|
|
323
|
+
height: "fill",
|
|
324
|
+
})) as unknown as Node2D
|
|
325
|
+
: undefined
|
|
326
|
+
|
|
327
|
+
const categoryAxis = showCategory
|
|
328
|
+
? (new CategoryAxis({
|
|
329
|
+
categories,
|
|
330
|
+
axis: categoryConfig,
|
|
331
|
+
label: state.categoryAxis.label ?? "",
|
|
332
|
+
orientation: horizontal ? "vertical" : "horizontal",
|
|
333
|
+
dims: () => categoryDims(state),
|
|
334
|
+
width: horizontal ? "hug" : "fill",
|
|
335
|
+
height: horizontal ? "fill" : "hug",
|
|
336
|
+
}) as unknown as Node2D)
|
|
337
|
+
: undefined
|
|
338
|
+
|
|
339
|
+
const left = horizontal ? categoryAxis : valueAxis
|
|
340
|
+
const bottom = horizontal ? valueAxis : categoryAxis
|
|
341
|
+
const leftConfig = horizontal ? categoryConfig : valueConfig
|
|
342
|
+
const ghost = left && bottom
|
|
343
|
+
? columnGhost({
|
|
344
|
+
captions: horizontal ? () => categories : valueCaptions,
|
|
345
|
+
count: horizontal ? categories.length : barChartDomain(state).ticks.length,
|
|
346
|
+
text: () => leftConfig().labelStyle,
|
|
347
|
+
title: (horizontal ? state.categoryAxis.label : state.valueAxis.label) ?? "",
|
|
348
|
+
titleText: () => leftConfig().titleStyle,
|
|
349
|
+
tickPad: () => axisPads(leftConfig(), "right").tick,
|
|
350
|
+
titlePad: () => axisPads(leftConfig(), "right").title,
|
|
351
|
+
})
|
|
352
|
+
: undefined
|
|
375
353
|
|
|
376
354
|
const barSets = state.series.map(
|
|
377
355
|
(s, i) =>
|
|
378
356
|
new ChartBars({
|
|
379
|
-
bars: () => seriesBars(state, i, plotSize()
|
|
380
|
-
baseline: () =>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
cornerRadius: () => theme().bar.cornerRadius,
|
|
357
|
+
bars: () => seriesBars(state, i, plotSize()),
|
|
358
|
+
baseline: () => valueToPx(domain(), runs(state, plotSize()).along, baselineValue(domain())),
|
|
359
|
+
horizontal,
|
|
360
|
+
fill: () => seriesFill(style(), s, i),
|
|
361
|
+
cornerRadius: () => style().barRadius,
|
|
385
362
|
growth: () => state.growth,
|
|
386
363
|
dims: () => categoryDims(state),
|
|
387
|
-
|
|
388
|
-
|
|
364
|
+
// A figure past a stack's end prints the stack's total; a percent stack has none worth printing, so prints its shares inside.
|
|
365
|
+
values: () => (built.values === "end" && state.stack === "percent" ? "inside" : built.values),
|
|
366
|
+
valueStyle: () => ({ ...valueText(style()), fontSize: Math.round(style().fontSize * 0.85) }),
|
|
389
367
|
enabled: () => state.showing[i] ?? (s.enabled ?? true),
|
|
390
368
|
}) as unknown as Node2D
|
|
391
369
|
)
|
|
392
370
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
371
|
+
const legendAt = built.legend
|
|
372
|
+
const legend = legendAt !== "none"
|
|
373
|
+
? (new ChartLegend({
|
|
374
|
+
series: state.series,
|
|
375
|
+
swatches: () => state.series.map((s, i) => seriesFill(style(), s, i)),
|
|
376
|
+
legend: () => legendConfigOf(style()),
|
|
377
|
+
flow: legendAt === "right" ? "vertical" : "horizontal",
|
|
378
|
+
}) as unknown as Node2D)
|
|
379
|
+
: undefined
|
|
380
|
+
|
|
381
|
+
const n = state.data.length
|
|
382
|
+
// Category lines fall between the slots, never through a bar.
|
|
383
|
+
const boundaries = () => Array.from({ length: Math.max(0, n - 1) }, (_, i) => i + 1)
|
|
384
|
+
const slots = (): Range => [0, Math.max(1, n)]
|
|
408
385
|
plot = new Rect({
|
|
409
|
-
clip: true,
|
|
410
386
|
children: [
|
|
411
387
|
new ChartCanvas({
|
|
412
|
-
fill: () =>
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
388
|
+
fill: () => style().background,
|
|
389
|
+
xGridStroke: () => style().verticalGrid,
|
|
390
|
+
yGridStroke: () => style().horizontalGrid,
|
|
391
|
+
xMarkers: horizontal ? () => domain().ticks : boundaries,
|
|
392
|
+
yMarkers: horizontal ? boundaries : () => domain().ticks,
|
|
393
|
+
xExtent: horizontal ? () => domain().range : slots,
|
|
394
|
+
yExtent: horizontal ? slots : () => domain().range,
|
|
395
|
+
axisStroke: () => style().axisStroke,
|
|
396
|
+
xAxisAt: () => (horizontal ? 0 : baselineValue(domain())),
|
|
397
|
+
yAxisAt: () => (horizontal ? baselineValue(domain()) : 0),
|
|
422
398
|
}),
|
|
423
399
|
...barSets,
|
|
424
|
-
new ChartBorder({ stroke: () => theme().plotArea.stroke }),
|
|
425
400
|
],
|
|
426
401
|
}) as unknown as Node2D
|
|
427
402
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
403
|
+
return asParts([
|
|
404
|
+
cartesianFrame({
|
|
405
|
+
left,
|
|
406
|
+
ghost,
|
|
407
|
+
plot,
|
|
408
|
+
bottom,
|
|
409
|
+
legend,
|
|
410
|
+
legendAt,
|
|
411
|
+
gap: () => frameGap(style()),
|
|
412
|
+
padding: () => ({
|
|
413
|
+
top: headroom(style()),
|
|
414
|
+
right: horizontal && bottom ? overhang(style()) : 0,
|
|
415
|
+
left: horizontal && bottom && !left ? overhang(style()) : 0,
|
|
416
|
+
bottom: 0,
|
|
417
|
+
}),
|
|
418
|
+
}),
|
|
419
|
+
])
|
|
439
420
|
},
|
|
440
421
|
commands: {
|
|
441
|
-
/**
|
|
442
|
-
* Animate the whole chart in: the two axes stagger their markers and
|
|
443
|
-
* captions in, the columns rise from the baseline, and the legend fades up
|
|
444
|
-
* last. Its first frame arms every part: markers and captions shifted and
|
|
445
|
-
* transparent, columns collapsed, the legend dropped and transparent.
|
|
446
|
-
* `duration` is the **whole** entrance; the beats are shares of it.
|
|
447
|
-
*/
|
|
422
|
+
/** Axes stagger in, bars rise from the baseline, the legend fades up last. `duration` is the whole entrance. */
|
|
448
423
|
enter: {
|
|
449
424
|
args: {},
|
|
450
|
-
// Held from
|
|
425
|
+
// Held from arrival, so it never shows finished ahead of its draw.
|
|
451
426
|
entrance: true,
|
|
452
427
|
easing: easeOut(),
|
|
453
|
-
run: (timeline,
|
|
428
|
+
run: (timeline, state, children) => {
|
|
454
429
|
const parts = partsOf(children)
|
|
455
|
-
if (!parts) return timeline.wait(timeline.duration)
|
|
456
430
|
const duration = timeline.duration
|
|
457
|
-
const axes = duration *
|
|
458
|
-
const rise = duration *
|
|
431
|
+
const axes = duration * ENTER_SHARE.axes
|
|
432
|
+
const rise = duration * ENTER_SHARE.marks
|
|
459
433
|
const { legend } = parts
|
|
460
434
|
const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
|
|
461
|
-
// Without a legend the beat is still its share of the whole.
|
|
462
435
|
return beats(timeline, [
|
|
463
436
|
at0(timeline.animate(() => ({ growth: 0 }))),
|
|
464
|
-
...(legend ? [at0(legend.to({ opacity: 0,
|
|
465
|
-
{ at: 0, duration: axes, piece: parts.
|
|
466
|
-
{ at: 0, duration: axes, piece: parts.
|
|
437
|
+
...(legend ? [at0(legend.to({ opacity: 0, ...legendAway(state.style.legend) }))] : []),
|
|
438
|
+
...(parts.valueAxis ? [{ at: 0, duration: axes, piece: parts.valueAxis.run("enter", { slide: 24 }) }] : []),
|
|
439
|
+
...(parts.categoryAxis ? [{ at: 0, duration: axes, piece: parts.categoryAxis.run("enter", { slide: 24 }) }] : []),
|
|
467
440
|
// No priming to 0 first: the value at `t` is a function of `t`.
|
|
468
441
|
{ at: axes, duration: rise, piece: timeline.animate((t) => ({ growth: t })) },
|
|
469
|
-
...(legend ? [{ at: axes + rise, duration: duration *
|
|
442
|
+
...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
|
|
470
443
|
])
|
|
471
444
|
},
|
|
472
445
|
},
|
|
473
|
-
/**
|
|
446
|
+
/** Bring series `index` back, the group reflowing to make room, with its legend entry. */
|
|
474
447
|
showSeries: {
|
|
475
448
|
args: { seriesIndex: SERIES_ARG },
|
|
476
449
|
easing: easeOut(),
|
|
477
450
|
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 1),
|
|
478
451
|
},
|
|
479
|
-
/**
|
|
480
|
-
* Hide series `index`: its columns narrow away (or, stacked, sink out of the
|
|
481
|
-
* stack) while the rest of the group widens into the space — the mirror of
|
|
482
|
-
* `showSeries`.
|
|
483
|
-
*/
|
|
452
|
+
/** The mirror of `showSeries`: bars narrow away (stacked, sink out) while the rest widen into the space. */
|
|
484
453
|
hideSeries: {
|
|
485
454
|
args: { seriesIndex: SERIES_ARG },
|
|
486
455
|
easing: easeOut(),
|
|
487
456
|
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 0),
|
|
488
457
|
},
|
|
489
|
-
/**
|
|
490
|
-
* Fade every other series — its columns **and its legend entry** — to focus
|
|
491
|
-
* on `index`, which comes back to full. Nothing moves, so the group doesn't
|
|
492
|
-
* reflow.
|
|
493
|
-
*/
|
|
458
|
+
/** Fade every other series, bars and legend entry, to focus on `index`. Nothing moves, so the group does not reflow. */
|
|
494
459
|
spotlightSeries: {
|
|
495
460
|
args: { seriesIndex: SERIES_ARG },
|
|
496
461
|
easing: easeOut(),
|
|
497
|
-
run: (timeline,
|
|
462
|
+
run: (timeline, _state, children) => {
|
|
498
463
|
const parts = partsOf(children)
|
|
499
|
-
if (!parts) return timeline.wait(timeline.duration)
|
|
500
464
|
const index = timeline.args.seriesIndex
|
|
501
|
-
const dim = state.theme.spotlight.dimOpacity
|
|
502
465
|
return timeline.together([
|
|
503
|
-
...parts.bars.map((bars, i) => bars.to({ opacity: i === index ? 1 :
|
|
466
|
+
...parts.bars.map((bars, i) => bars.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
|
|
504
467
|
...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
|
|
468
|
+
timeline.wait(timeline.duration),
|
|
505
469
|
])
|
|
506
470
|
},
|
|
507
471
|
},
|
|
508
472
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
* category that is not there takes its slot and changes nothing.
|
|
473
|
+
* Fade every other category's bars and caption to {@link SPOTLIGHT_DIM}. While
|
|
474
|
+
* another is lit the focus moves directly. A missing category takes its slot
|
|
475
|
+
* and changes nothing.
|
|
513
476
|
*/
|
|
514
477
|
spotlightCategory: {
|
|
515
478
|
args: { categoryIndex: Types.number(0, { min: 0, step: 1 }) },
|
|
@@ -521,11 +484,7 @@ export const BarChart = component2d({
|
|
|
521
484
|
return timeline.animate((t) => ({ spotlight: index, spotlightReveal: from + (1 - from) * t }))
|
|
522
485
|
},
|
|
523
486
|
},
|
|
524
|
-
/**
|
|
525
|
-
* Drop the category spotlight, every category back to full. The lit
|
|
526
|
-
* category is held until the fade lands; a no-op when nothing is lit, which
|
|
527
|
-
* still takes its slot.
|
|
528
|
-
*/
|
|
487
|
+
/** Every category back to full; the lit one is held until the fade lands. A no-op when nothing is lit, which still takes its slot. */
|
|
529
488
|
clearCategory: {
|
|
530
489
|
args: {},
|
|
531
490
|
easing: easeOut(),
|