@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
|
@@ -7,26 +7,34 @@ import {
|
|
|
7
7
|
type ChildView,
|
|
8
8
|
type DataRecord,
|
|
9
9
|
type NodeOf,
|
|
10
|
+
type OutOf,
|
|
10
11
|
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
11
12
|
type Node2D,
|
|
12
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,
|
|
19
|
+
type StyleInput as _StyleInput,
|
|
20
|
+
type StyleValues as _StyleValues,
|
|
14
21
|
} from "@motionscript/sdk"
|
|
15
22
|
import { formatNumber } from "@motionscript/sdk/component"
|
|
16
23
|
|
|
17
|
-
import { cartesianFrame,
|
|
18
|
-
import { ChartBorder } from "../kit/chart-border"
|
|
24
|
+
import { cartesianFrame, chartViews, columnGhost, frameGap, headroom, laidOutSize, overhang } from "../kit/cartesian"
|
|
19
25
|
import { ChartCanvas } from "../kit/chart-canvas"
|
|
20
26
|
import { ChartLegend } from "../kit/chart-legend"
|
|
21
27
|
import { SERIES_ARG, asParts, fresh } from "../kit/entries"
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
28
|
+
import { HorizontalAxis } from "../kit/horizontal-axis"
|
|
29
|
+
import { DEFAULT_MARKERS, ENTER_SHARE, beats, legendAway, type AnyTimeline, type Beat } from "../kit/motion"
|
|
24
30
|
import {
|
|
31
|
+
SPOTLIGHT_DIM,
|
|
25
32
|
formatMarker,
|
|
26
33
|
lerpAxisDomain,
|
|
27
34
|
lerpAxisEntry,
|
|
28
35
|
markerStep,
|
|
29
36
|
markerValues,
|
|
37
|
+
niceTicks,
|
|
30
38
|
num,
|
|
31
39
|
resolveAxisDomain,
|
|
32
40
|
settleAxisDomain,
|
|
@@ -34,27 +42,46 @@ import {
|
|
|
34
42
|
type Range,
|
|
35
43
|
type ResolvedAxisDomain,
|
|
36
44
|
} from "../kit/shared"
|
|
37
|
-
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"
|
|
38
59
|
import { ChartBars, type BarGeometry } from "./chart-bars"
|
|
39
60
|
import {
|
|
40
|
-
|
|
61
|
+
BAR_LABELS,
|
|
62
|
+
BAR_MARKS,
|
|
41
63
|
barCategories,
|
|
42
64
|
barSpans,
|
|
43
|
-
lerpBarChartTheme,
|
|
44
|
-
resolveBarChartTheme,
|
|
45
|
-
resolveBarFill,
|
|
46
65
|
resolveBarScale,
|
|
47
|
-
|
|
48
|
-
type BarChartConfig,
|
|
49
|
-
type BarChartTheme,
|
|
66
|
+
type BarOrientation,
|
|
50
67
|
type BarSeries,
|
|
68
|
+
type BarStack,
|
|
51
69
|
} from "./shared"
|
|
52
70
|
|
|
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>
|
|
79
|
+
|
|
53
80
|
/** What a bar chart plots, as its entries hold it. */
|
|
54
81
|
export interface BarChartValues {
|
|
55
82
|
readonly data: DataRecord[]
|
|
56
83
|
readonly series: BarSeries[]
|
|
57
|
-
readonly
|
|
84
|
+
readonly stack: BarStack
|
|
58
85
|
readonly valueAxis: AxisDomainConfig
|
|
59
86
|
}
|
|
60
87
|
|
|
@@ -62,138 +89,143 @@ export interface BarChartValues {
|
|
|
62
89
|
export type BarChartDomain = ResolvedAxisDomain
|
|
63
90
|
|
|
64
91
|
interface BarState extends BarChartValues {
|
|
65
|
-
readonly
|
|
92
|
+
readonly categoryField: string
|
|
93
|
+
readonly orientation: BarOrientation
|
|
94
|
+
readonly style: BarChartStyle
|
|
66
95
|
readonly weights: number[]
|
|
96
|
+
readonly showing: boolean[]
|
|
67
97
|
readonly spotlight: number | null
|
|
68
98
|
readonly spotlightReveal: number
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
function drawnValue(s: BarChartValues, domain: AxisDomainConfig): AxisDomainConfig {
|
|
72
102
|
const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1
|
|
73
|
-
const range = resolveBarScale(s.data, s.series, s.
|
|
74
|
-
|
|
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)) }
|
|
75
106
|
}
|
|
76
107
|
|
|
77
108
|
/** The last domain worked out from each data array, and what it was read from. */
|
|
78
|
-
const domainsDrawn = new WeakMap<DataRecord[],
|
|
109
|
+
const domainsDrawn = new WeakMap<DataRecord[], { readonly key: readonly unknown[]; readonly domain: BarChartDomain }>()
|
|
79
110
|
|
|
80
|
-
/** The value axis as drawn now. */
|
|
111
|
+
/** The value axis as drawn now. A series being hidden keeps its room, so the ticks hold still while it goes. */
|
|
81
112
|
export function barChartDomain(s: BarChartValues): BarChartDomain {
|
|
82
|
-
const
|
|
83
|
-
const kept = domainsDrawn.get(data)
|
|
84
|
-
if (kept && kept.
|
|
85
|
-
const domain = settleAxisDomain(valueAxis, (d) => drawnValue(s, d)) as BarChartDomain
|
|
86
|
-
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 })
|
|
87
118
|
return domain
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
/** Ticks and the figures printed
|
|
121
|
+
/** Ticks and the figures printed by bars both read through here, so they cannot disagree about how a number looks. */
|
|
91
122
|
function valueLabel(s: BarChartValues, domain: BarChartDomain, value: number): string {
|
|
92
123
|
const pattern = s.valueAxis.format
|
|
93
|
-
|
|
124
|
+
if (pattern !== undefined) return formatNumber(value, pattern)
|
|
125
|
+
const text = formatMarker(value, markerStep(domain.ticks))
|
|
126
|
+
return s.stack === "percent" ? `${text}%` : text
|
|
94
127
|
}
|
|
95
128
|
|
|
96
|
-
/** A value
|
|
97
|
-
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 {
|
|
98
131
|
const [min, max] = domain.range
|
|
99
|
-
if (max
|
|
100
|
-
return ((value - min) / (max - min) - 0.5) *
|
|
132
|
+
if (max === min) return -length / 2
|
|
133
|
+
return ((value - min) / (max - min) - 0.5) * length
|
|
101
134
|
}
|
|
102
135
|
|
|
103
|
-
/**
|
|
104
|
-
function
|
|
105
|
-
const [
|
|
106
|
-
return
|
|
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))
|
|
107
140
|
}
|
|
108
141
|
|
|
109
|
-
function weightAt(s:
|
|
142
|
+
function weightAt(s: { readonly weights: readonly number[] }, index: number): number {
|
|
110
143
|
return Math.max(0, s.weights[index] ?? 1)
|
|
111
144
|
}
|
|
112
145
|
|
|
113
|
-
/**
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
121
155
|
}
|
|
122
156
|
|
|
123
157
|
/**
|
|
124
|
-
* Series `index`'s
|
|
125
|
-
* slot with its siblings by live weight
|
|
126
|
-
* hiding a series reflows rather than
|
|
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.
|
|
127
162
|
*/
|
|
128
|
-
function seriesBars(s: BarState, index: number, plot: Size2D
|
|
163
|
+
function seriesBars(s: BarState, index: number, plot: Size2D): BarGeometry[] {
|
|
129
164
|
const series = s.series[index]
|
|
130
165
|
if (!series) return []
|
|
131
166
|
const domain = barChartDomain(s)
|
|
167
|
+
const { along, across } = runs(s, plot)
|
|
132
168
|
const n = s.data.length
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
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"
|
|
137
172
|
let total = 0
|
|
138
173
|
for (let i = 0; i < s.series.length; i++) total += weightAt(s, i)
|
|
139
174
|
const unit = inner / (total > 0 ? total : 1)
|
|
140
175
|
const weight = weightAt(s, index)
|
|
141
176
|
let before = 0
|
|
142
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
|
|
143
180
|
|
|
144
181
|
return s.data.map((row, rowIndex) => {
|
|
145
|
-
// Built from live contributions, so a fading series lowers the segments
|
|
146
|
-
const spans = barSpans(
|
|
147
|
-
const span = spans[index] ?? { from: 0, to: 0, value: 0 }
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
192
|
return {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
from:
|
|
158
|
-
to:
|
|
159
|
-
round:
|
|
160
|
-
|
|
161
|
-
...(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,
|
|
162
199
|
}
|
|
163
200
|
})
|
|
164
201
|
}
|
|
165
202
|
|
|
166
|
-
/** Per-row opacity: 1 while nothing is spotlit, falling to
|
|
203
|
+
/** Per-row opacity: 1 while nothing is spotlit, falling to {@link SPOTLIGHT_DIM} for all but the chosen category. */
|
|
167
204
|
function categoryDims(s: BarState): number[] {
|
|
168
205
|
const focus = clamp(s.spotlightReveal, 0, 1)
|
|
169
|
-
const dim = s.theme.spotlight.dimOpacity
|
|
170
206
|
const chosen = s.spotlight
|
|
171
|
-
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))
|
|
172
208
|
}
|
|
173
209
|
|
|
174
210
|
/** The parts a bar chart's commands drive, found through its children. */
|
|
175
211
|
function partsOf(children: readonly ChildView[]) {
|
|
176
|
-
const
|
|
177
|
-
if (!frame) return null
|
|
178
|
-
const cAxis = frame.below.find(CategoryAxis.is)
|
|
179
|
-
if (!VerticalAxis.is(frame.axis) || !cAxis) return null
|
|
212
|
+
const views = chartViews(children)
|
|
180
213
|
return {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
bars:
|
|
184
|
-
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,
|
|
185
218
|
}
|
|
186
219
|
}
|
|
187
220
|
|
|
188
221
|
/**
|
|
189
|
-
* Move series `index`'s
|
|
190
|
-
*
|
|
191
|
-
*
|
|
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.
|
|
192
225
|
*/
|
|
193
|
-
function toggling(timeline: AnyTimeline, state: BarState
|
|
226
|
+
function toggling(timeline: AnyTimeline, state: BarState, children: readonly ChildView[], index: number, target: number) {
|
|
194
227
|
const parts = partsOf(children)
|
|
195
|
-
if (!parts
|
|
196
|
-
// Through `weightAt`, so short stored weights still tween from the 1 drawn.
|
|
228
|
+
if (!parts.bars[index]) return timeline.wait(timeline.duration)
|
|
197
229
|
const from = state.series.map((_, i) => weightAt(state, i))
|
|
198
230
|
const start = from[index] ?? 1
|
|
199
231
|
const showing = state.series.map((s, i) => state.showing[i] ?? (s.enabled ?? true))
|
|
@@ -210,38 +242,38 @@ function toggling(timeline: AnyTimeline, state: BarState & { readonly showing: b
|
|
|
210
242
|
}
|
|
211
243
|
|
|
212
244
|
/**
|
|
213
|
-
* A
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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.
|
|
217
250
|
*
|
|
218
251
|
* Resting state is the finished chart; `enter` arms every part on its first
|
|
219
252
|
* frame. A `to` of the value axis's `range` or `ticks` tweens from the domain as
|
|
220
|
-
* drawn, so an auto axis rescales the
|
|
253
|
+
* drawn, so an auto axis rescales the bars continuously.
|
|
221
254
|
*/
|
|
222
255
|
export const BarChart = component2d({
|
|
223
256
|
key: "barChart",
|
|
224
257
|
state: {
|
|
225
|
-
data: fresh<DataRecord[]>(() => []),
|
|
258
|
+
data: fresh<DataRecord[]>(() => [], { kind: "dataset" }),
|
|
226
259
|
// Names a column, not a value, like the line chart's `xField`.
|
|
227
260
|
categoryField: "",
|
|
228
|
-
series: fresh<BarSeries[]>(() => []),
|
|
229
|
-
|
|
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),
|
|
230
266
|
valueAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
231
267
|
categoryAxis: Types.custom<AxisDomainConfig>({}, { resolve: resolveAxisDomain, lerp: lerpAxisDomain }),
|
|
232
|
-
|
|
233
|
-
/** Rise of every
|
|
268
|
+
style: BAR_STYLE,
|
|
269
|
+
/** Rise of every bar about the baseline, `[0, 1]`: 0 collapses them onto it. */
|
|
234
270
|
growth: 1,
|
|
235
|
-
/** Each series' share of the slot, seeded from `enabled`: a weight rather than a switch, so hiding one widens the others. */
|
|
236
|
-
weights: fresh<number[]>(() => []),
|
|
237
271
|
/** The category spotlit, or `null`. The choice snaps; `spotlightReveal` animates it. */
|
|
238
272
|
spotlight: Types.custom<number | null>(null),
|
|
239
273
|
spotlightReveal: 0,
|
|
240
|
-
/**
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
showValues: Types.boolean(false, { animatable: false }),
|
|
244
|
-
/** 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. */
|
|
245
277
|
showing: Types.custom<boolean[]>([], { private: true }),
|
|
246
278
|
},
|
|
247
279
|
defaults: {
|
|
@@ -253,106 +285,148 @@ export const BarChart = component2d({
|
|
|
253
285
|
},
|
|
254
286
|
allowedChildren: [],
|
|
255
287
|
children: (state) => {
|
|
256
|
-
const
|
|
288
|
+
const style = () => state.style
|
|
257
289
|
const domain = () => barChartDomain(state)
|
|
258
290
|
let plot: Node2D | undefined
|
|
259
291
|
const plotSize = () => laidOutSize(plot)
|
|
260
|
-
const
|
|
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
|
|
261
303
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
274
326
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
283
353
|
|
|
284
354
|
const barSets = state.series.map(
|
|
285
355
|
(s, i) =>
|
|
286
356
|
new ChartBars({
|
|
287
|
-
bars: () => seriesBars(state, i, plotSize()
|
|
288
|
-
baseline: () =>
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
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,
|
|
293
362
|
growth: () => state.growth,
|
|
294
363
|
dims: () => categoryDims(state),
|
|
295
|
-
|
|
296
|
-
|
|
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) }),
|
|
297
367
|
enabled: () => state.showing[i] ?? (s.enabled ?? true),
|
|
298
368
|
}) as unknown as Node2D
|
|
299
369
|
)
|
|
300
370
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
padding: state.theme.legend.padding,
|
|
311
|
-
}) as unknown as Node2D
|
|
312
|
-
)
|
|
313
|
-
}
|
|
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
|
|
314
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)]
|
|
315
385
|
plot = new Rect({
|
|
316
|
-
clip: true,
|
|
317
386
|
children: [
|
|
318
387
|
new ChartCanvas({
|
|
319
|
-
fill: () =>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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),
|
|
328
398
|
}),
|
|
329
399
|
...barSets,
|
|
330
|
-
new ChartBorder({ stroke: () => theme().plotArea.stroke }),
|
|
331
400
|
],
|
|
332
401
|
}) as unknown as Node2D
|
|
333
402
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
])
|
|
345
420
|
},
|
|
346
421
|
commands: {
|
|
347
|
-
/** Axes stagger in,
|
|
422
|
+
/** Axes stagger in, bars rise from the baseline, the legend fades up last. `duration` is the whole entrance. */
|
|
348
423
|
enter: {
|
|
349
424
|
args: {},
|
|
350
425
|
// Held from arrival, so it never shows finished ahead of its draw.
|
|
351
426
|
entrance: true,
|
|
352
427
|
easing: easeOut(),
|
|
353
|
-
run: (timeline,
|
|
428
|
+
run: (timeline, state, children) => {
|
|
354
429
|
const parts = partsOf(children)
|
|
355
|
-
if (!parts) return timeline.wait(timeline.duration)
|
|
356
430
|
const duration = timeline.duration
|
|
357
431
|
const axes = duration * ENTER_SHARE.axes
|
|
358
432
|
const rise = duration * ENTER_SHARE.marks
|
|
@@ -360,12 +434,12 @@ export const BarChart = component2d({
|
|
|
360
434
|
const at0 = (piece: Beat["piece"]): Beat => ({ at: 0, duration: 0, piece })
|
|
361
435
|
return beats(timeline, [
|
|
362
436
|
at0(timeline.animate(() => ({ growth: 0 }))),
|
|
363
|
-
...(legend ? [at0(legend.to({ opacity: 0,
|
|
364
|
-
{ at: 0, duration: axes, piece: parts.
|
|
365
|
-
{ 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 }) }] : []),
|
|
366
440
|
// No priming to 0 first: the value at `t` is a function of `t`.
|
|
367
441
|
{ at: axes, duration: rise, piece: timeline.animate((t) => ({ growth: t })) },
|
|
368
|
-
...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
|
|
442
|
+
...(legend ? [{ at: axes + rise, duration: duration * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
|
|
369
443
|
])
|
|
370
444
|
},
|
|
371
445
|
},
|
|
@@ -375,29 +449,28 @@ export const BarChart = component2d({
|
|
|
375
449
|
easing: easeOut(),
|
|
376
450
|
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 1),
|
|
377
451
|
},
|
|
378
|
-
/** The mirror of `showSeries`:
|
|
452
|
+
/** The mirror of `showSeries`: bars narrow away (stacked, sink out) while the rest widen into the space. */
|
|
379
453
|
hideSeries: {
|
|
380
454
|
args: { seriesIndex: SERIES_ARG },
|
|
381
455
|
easing: easeOut(),
|
|
382
456
|
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, 0),
|
|
383
457
|
},
|
|
384
|
-
/** Fade every other series,
|
|
458
|
+
/** Fade every other series, bars and legend entry, to focus on `index`. Nothing moves, so the group does not reflow. */
|
|
385
459
|
spotlightSeries: {
|
|
386
460
|
args: { seriesIndex: SERIES_ARG },
|
|
387
461
|
easing: easeOut(),
|
|
388
|
-
run: (timeline,
|
|
462
|
+
run: (timeline, _state, children) => {
|
|
389
463
|
const parts = partsOf(children)
|
|
390
|
-
if (!parts) return timeline.wait(timeline.duration)
|
|
391
464
|
const index = timeline.args.seriesIndex
|
|
392
|
-
const dim = state.theme.spotlight.dimOpacity
|
|
393
465
|
return timeline.together([
|
|
394
|
-
...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 })),
|
|
395
467
|
...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
|
|
468
|
+
timeline.wait(timeline.duration),
|
|
396
469
|
])
|
|
397
470
|
},
|
|
398
471
|
},
|
|
399
472
|
/**
|
|
400
|
-
* Fade every other category's
|
|
473
|
+
* Fade every other category's bars and caption to {@link SPOTLIGHT_DIM}. While
|
|
401
474
|
* another is lit the focus moves directly. A missing category takes its slot
|
|
402
475
|
* and changes nothing.
|
|
403
476
|
*/
|