@motionscript/charts 0.1.0 → 0.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/bar-chart/bar-chart.d.ts +22 -93
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +48 -121
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +11 -38
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +15 -38
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +43 -74
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +31 -59
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -10
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +0 -7
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +27 -90
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +15 -54
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-4SMRCU24.js +2 -0
- package/dist/browser/chunks/chunk-4SMRCU24.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/engine.d.ts +4 -16
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -16
- package/dist/engine.js.map +1 -1
- package/dist/kit/cartesian.d.ts +8 -28
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +7 -19
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-border.d.ts +9 -18
- package/dist/kit/chart-border.d.ts.map +1 -1
- package/dist/kit/chart-border.js +4 -9
- package/dist/kit/chart-border.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +25 -36
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +13 -13
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +14 -61
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +17 -65
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/kit/entries.d.ts +2 -0
- package/dist/kit/entries.d.ts.map +1 -1
- package/dist/kit/entries.js +2 -0
- package/dist/kit/entries.js.map +1 -1
- package/dist/kit/motion.d.ts +11 -3
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +7 -1
- package/dist/kit/motion.js.map +1 -1
- package/dist/kit/shared.d.ts +60 -175
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +40 -157
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/vertical-axis.d.ts +16 -45
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +17 -42
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +16 -41
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +14 -43
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +14 -27
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +8 -22
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/horizontal-axis.d.ts +16 -43
- package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
- package/dist/line-chart/horizontal-axis.js +13 -42
- package/dist/line-chart/horizontal-axis.js.map +1 -1
- package/dist/line-chart/index.d.ts +1 -13
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +0 -12
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +23 -109
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +45 -141
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +33 -107
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +28 -81
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +14 -55
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +29 -111
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +0 -12
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +0 -12
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +1 -12
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +0 -9
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +37 -71
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +31 -65
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-legend.d.ts +11 -26
- package/dist/pie-chart/pie-legend.d.ts.map +1 -1
- package/dist/pie-chart/pie-legend.js +14 -39
- package/dist/pie-chart/pie-legend.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +50 -192
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +75 -247
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +45 -131
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +29 -72
- package/dist/pie-chart/shared.js.map +1 -1
- package/package.json +4 -4
- package/src/bar-chart/bar-chart.ts +51 -165
- package/src/bar-chart/category-axis.ts +16 -57
- package/src/bar-chart/chart-bars.ts +43 -120
- package/src/bar-chart/index.ts +2 -10
- package/src/bar-chart/shared.ts +29 -97
- package/src/engine.ts +4 -16
- package/src/kit/cartesian.ts +11 -26
- package/src/kit/chart-border.ts +6 -20
- package/src/kit/chart-canvas.ts +15 -40
- package/src/kit/chart-legend.ts +17 -86
- package/src/kit/entries.ts +4 -1
- package/src/kit/motion.ts +11 -2
- package/src/kit/shared.ts +58 -219
- package/src/kit/vertical-axis.ts +18 -71
- package/src/line-chart/chart-line.ts +16 -44
- package/src/line-chart/chart-region.ts +10 -23
- package/src/line-chart/horizontal-axis.ts +13 -42
- package/src/line-chart/index.ts +1 -13
- package/src/line-chart/line-chart.ts +50 -184
- package/src/line-chart/shared.ts +51 -145
- package/src/line-chart/x-scale.ts +31 -121
- package/src/nodes.ts +0 -12
- package/src/pie-chart/index.ts +1 -12
- package/src/pie-chart/pie-chart.ts +34 -102
- package/src/pie-chart/pie-legend.ts +14 -55
- package/src/pie-chart/pie-ring.ts +80 -307
- package/src/pie-chart/shared.ts +50 -139
- package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
- package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
package/src/pie-chart/index.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The pie chart: the one in the family that draws a mark per **row** rather
|
|
3
|
-
* than per column.
|
|
4
|
-
*
|
|
5
|
-
* So it shares the legend and the theme machinery with its siblings and none of
|
|
6
|
-
* the cartesian geometry. Set the theme's `ring.holeRadius` to 0 for a solid
|
|
7
|
-
* pie; it defaults to a donut, because the hole is the only place a chart has
|
|
8
|
-
* to put its conclusion.
|
|
9
|
-
*/
|
|
10
1
|
export { PieChart, isSliceSelected } from "./pie-chart"
|
|
11
|
-
export type {
|
|
2
|
+
export type { PieChartValues } from "./pie-chart"
|
|
12
3
|
export { PieRing } from "./pie-ring"
|
|
13
|
-
export type { PieRingProps } from "./pie-ring"
|
|
14
4
|
export { PieLegend } from "./pie-legend"
|
|
15
|
-
export type { PieLegendProps } from "./pie-legend"
|
|
16
5
|
export {
|
|
17
6
|
DEFAULT_PIE_CHART_THEME,
|
|
18
7
|
lerpPieChartTheme,
|
|
@@ -1,20 +1,19 @@
|
|
|
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
9
|
type NodeOf,
|
|
9
|
-
// Named so this component's emitted type reaches `Node2D` through the sdk, not core.
|
|
10
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
10
11
|
type Node2D,
|
|
11
|
-
type Node2DProps,
|
|
12
|
-
type Timeline,
|
|
12
|
+
type Node2DProps as _Node2DProps,
|
|
13
13
|
} from "@motionscript/sdk"
|
|
14
14
|
|
|
15
15
|
import { asParts, fresh } from "../kit/entries"
|
|
16
|
-
import { beats, type Beat } from "../kit/motion"
|
|
17
|
-
import { clampUnit } from "../kit/shared"
|
|
16
|
+
import { beats, type AnyTimeline, type Beat } from "../kit/motion"
|
|
18
17
|
import { PieLegend } from "./pie-legend"
|
|
19
18
|
import { PieRing } from "./pie-ring"
|
|
20
19
|
import {
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
lerpPieChartTheme,
|
|
23
22
|
resolvePieChartTheme,
|
|
24
23
|
resolvePieSlices,
|
|
24
|
+
selectionFade,
|
|
25
25
|
type PieChartConfig,
|
|
26
26
|
type PieChartTheme,
|
|
27
27
|
type PieLabelMode,
|
|
@@ -29,40 +29,6 @@ import {
|
|
|
29
29
|
type PieSlice,
|
|
30
30
|
} from "./shared"
|
|
31
31
|
|
|
32
|
-
export interface PieChartProps extends Node2DProps {
|
|
33
|
-
/** One row per slice — a plain record of field name to value. */
|
|
34
|
-
data: DataRecord[]
|
|
35
|
-
/** Key into each row for its slice caption, used on the labels and the legend. */
|
|
36
|
-
categoryField: string
|
|
37
|
-
/** Key into each row for its slice magnitude — the wedge's share of the ring. */
|
|
38
|
-
valueField: string
|
|
39
|
-
/**
|
|
40
|
-
* The chart's visual theme — slice palette, ring geometry, label and centre
|
|
41
|
-
* type, legend. Every branch is optional and deep-merged onto
|
|
42
|
-
* {@link DEFAULT_PIE_CHART_THEME}.
|
|
43
|
-
*/
|
|
44
|
-
theme?: PieChartTheme
|
|
45
|
-
/** What the ring's labels say. */
|
|
46
|
-
labelMode?: PieLabelMode
|
|
47
|
-
/** Where they sit: inside their wedge, outside on a leader, or nowhere. */
|
|
48
|
-
labelPlacement?: PieLabelPlacement
|
|
49
|
-
/** Whether the legend column beside the ring is drawn at all. */
|
|
50
|
-
showLegend?: boolean
|
|
51
|
-
/** Gap between the ring and the legend, in px. */
|
|
52
|
-
gap?: number
|
|
53
|
-
/** Text set in the ring's hole — see `revealCenter`. */
|
|
54
|
-
centerLabel?: string
|
|
55
|
-
/** Smaller second line under {@link centerLabel}. */
|
|
56
|
-
centerCaption?: string
|
|
57
|
-
/**
|
|
58
|
-
* Whether the hole's summary is drawn at rest. `true` (the default) is the
|
|
59
|
-
* plain answer; a scene that wants the conclusion to land on a later beat sets
|
|
60
|
-
* this `false` and runs `revealCenter` when it gets there.
|
|
61
|
-
*/
|
|
62
|
-
showCenter?: boolean
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** What the selection reads: the rows, how they are cut, and the pops the commands move. */
|
|
66
32
|
export interface PieChartValues {
|
|
67
33
|
readonly data: DataRecord[]
|
|
68
34
|
readonly categoryField: string
|
|
@@ -71,57 +37,42 @@ export interface PieChartValues {
|
|
|
71
37
|
readonly pops: number[]
|
|
72
38
|
}
|
|
73
39
|
|
|
74
|
-
/**
|
|
75
|
-
* How `enter` divides its duration: the wedges sweeping in, then the labels and
|
|
76
|
-
* the legend arriving together. Shares of the whole, so they sum to 1.
|
|
77
|
-
*/
|
|
40
|
+
/** How `enter` divides its duration: wedges sweeping in, then labels and legend together. Sums to 1. */
|
|
78
41
|
const SWEEP_SHARE = 0.65
|
|
79
42
|
const LABEL_SHARE = 0.35
|
|
80
43
|
|
|
81
44
|
const SLICE_ARG = Types.number(0, { min: 0, step: 1 })
|
|
82
45
|
|
|
83
|
-
/** The slices the ring and the legend draw, resolved from the rows and the palette. */
|
|
84
46
|
function slicesOf(s: PieChartValues): PieSlice[] {
|
|
85
47
|
return resolvePieSlices(s.data, s.categoryField, s.valueField, s.theme.sliceFills)
|
|
86
48
|
}
|
|
87
49
|
|
|
88
|
-
/** How far slice `index` is pushed out, in `[0, 1]`. */
|
|
89
50
|
function popAt(s: PieChartValues, index: number): number {
|
|
90
|
-
return
|
|
51
|
+
return clamp(s.pops[index] ?? 0, 0, 1)
|
|
91
52
|
}
|
|
92
53
|
|
|
93
|
-
/** Whether slice `index` is
|
|
54
|
+
/** Whether slice `index` is past half way out, so a slice mid-swell is not yet counted. */
|
|
94
55
|
export function isSliceSelected(s: PieChartValues, index: number): boolean {
|
|
95
56
|
return popAt(s, index) > 0.5
|
|
96
57
|
}
|
|
97
58
|
|
|
98
|
-
/**
|
|
99
|
-
* How far each slice is dimmed by the selection, one entry per slice: 1 while
|
|
100
|
-
* nothing is out, and for the selected slice itself; falling toward the theme's
|
|
101
|
-
* `dimOpacity` for the rest in proportion to how far the selection has grown.
|
|
102
|
-
*/
|
|
59
|
+
/** Per-slice opacity: 1 while nothing is out and for the selected slice, falling toward `dimOpacity` for the rest as the selection grows. */
|
|
103
60
|
function sliceDims(s: PieChartValues): number[] {
|
|
104
61
|
let focus = 0
|
|
105
62
|
for (let i = 0; i < s.data.length; i++) focus = Math.max(focus, popAt(s, i))
|
|
106
63
|
const dim = s.theme.spotlight.dimOpacity
|
|
107
|
-
|
|
108
|
-
return s.data.map((_, i) => 1 - (1 - clampUnit(dim)) * focus * (1 - popAt(s, i)))
|
|
64
|
+
return s.data.map((_, i) => selectionFade(focus, dim, popAt(s, i)))
|
|
109
65
|
}
|
|
110
66
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Every slice's pop eased from where it is to what `pop` asks for. A move that
|
|
115
|
-
* changes nothing still takes its slot, writing nothing.
|
|
116
|
-
*/
|
|
67
|
+
/** Every slice's pop eased to what `pop` asks for. A move that changes nothing still takes its slot. */
|
|
117
68
|
function popping(timeline: AnyTimeline, s: PieChartValues, pop: (index: number) => number) {
|
|
118
69
|
const from = s.data.map((_, i) => popAt(s, i))
|
|
119
|
-
const targets = s.data.map((_, i) =>
|
|
70
|
+
const targets = s.data.map((_, i) => clamp(pop(i), 0, 1))
|
|
120
71
|
if (from.every((value, i) => value === targets[i])) return timeline.wait(timeline.duration)
|
|
121
72
|
return timeline.animate((t) => ({ pops: from.map((value, i) => value + (targets[i] - value) * t) }))
|
|
122
73
|
}
|
|
123
74
|
|
|
124
|
-
/** The ring and
|
|
75
|
+
/** The ring and legend among the children; the last row is the one the chart built. */
|
|
125
76
|
function partsOf(children: readonly ChildView[]) {
|
|
126
77
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
127
78
|
const row = children[i]
|
|
@@ -132,57 +83,48 @@ function partsOf(children: readonly ChildView[]) {
|
|
|
132
83
|
}
|
|
133
84
|
|
|
134
85
|
/**
|
|
135
|
-
* A pie or donut chart
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* - {@link PieRing} — the wedges, each sized by its share of the total, with its
|
|
140
|
-
* label inside its own band or pinned outside on a leader line, and the
|
|
141
|
-
* summary in the hole.
|
|
142
|
-
* - {@link PieLegend} — a column of swatch-and-caption rows naming each slice.
|
|
143
|
-
*
|
|
144
|
-
* **One mark per row, not per column**: a pie's palette is indexed by row and
|
|
145
|
-
* there is nothing to hide, since dropping a slice would re-cut every other
|
|
146
|
-
* wedge. `selectSlice` and its siblings are what a pie has instead — a slice
|
|
147
|
-
* swells out of the ring and everything else dims behind it.
|
|
86
|
+
* A pie or donut chart: a {@link PieRing} filling what the hugging {@link PieLegend}
|
|
87
|
+
* leaves. One mark per row, not per column, and nothing to hide, since dropping
|
|
88
|
+
* a slice would re-cut every other wedge; `selectSlice` and its siblings swell
|
|
89
|
+
* a slice and dim the rest instead.
|
|
148
90
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* with `showCenter: false`, because it is usually the chart's *conclusion*.
|
|
91
|
+
* Resting state is the finished chart, except the hole's summary, which
|
|
92
|
+
* `showCenter: false` holds back for `revealCenter`.
|
|
152
93
|
*/
|
|
153
94
|
export const PieChart = component2d({
|
|
154
95
|
key: "pieChart",
|
|
155
96
|
state: {
|
|
156
97
|
data: fresh<DataRecord[]>(() => []),
|
|
98
|
+
/** Slice caption, used on the labels and the legend. */
|
|
157
99
|
categoryField: "",
|
|
100
|
+
/** Slice magnitude: the wedge's share of the ring. */
|
|
158
101
|
valueField: "",
|
|
159
102
|
theme: Types.custom<PieChartTheme, PieChartConfig>(DEFAULT_PIE_CHART_THEME, { resolve: resolvePieChartTheme, lerp: lerpPieChartTheme }),
|
|
103
|
+
/** What the ring's labels say. */
|
|
160
104
|
labelMode: Types.custom<PieLabelMode>("percent"),
|
|
105
|
+
/** Where they sit: inside their wedge, outside on a leader, or nowhere. */
|
|
161
106
|
labelPlacement: Types.custom<PieLabelPlacement>("leader"),
|
|
107
|
+
/** Gap between the ring and the legend, in px. */
|
|
162
108
|
gap: 40,
|
|
109
|
+
/** Text set in the ring's hole. */
|
|
163
110
|
centerLabel: "",
|
|
111
|
+
/** Smaller second line under `centerLabel`. */
|
|
164
112
|
centerCaption: "",
|
|
165
|
-
/**
|
|
166
|
-
* How far each slice is pushed out of the ring, `0`–`1` per index. Held here
|
|
167
|
-
* rather than on the ring because two parts read it: the wedges grow out by
|
|
168
|
-
* it, and the legend entries dim by it. Seeded flush, one to a row.
|
|
169
|
-
*/
|
|
113
|
+
/** How far each slice is pushed out, `0`-`1` per index. Here, not on the ring, because the wedges and the legend both read it. */
|
|
170
114
|
pops: fresh<number[]>(() => []),
|
|
171
115
|
/** Read as the chart is built: the legend is a part the chart builds or doesn't. */
|
|
172
116
|
showLegend: Types.boolean(true, { animatable: false }),
|
|
173
|
-
/** Read as the chart is built: whether the hole's summary is drawn at rest. */
|
|
117
|
+
/** Read as the chart is built: whether the hole's summary is drawn at rest. A scene that wants it to land later sets `false` and runs `revealCenter`. */
|
|
174
118
|
showCenter: Types.boolean(true, { animatable: false }),
|
|
175
119
|
},
|
|
176
120
|
defaults: {
|
|
177
|
-
//
|
|
121
|
+
// A node made of parts that fill would otherwise hug them.
|
|
178
122
|
width: "fill",
|
|
179
123
|
height: "fill",
|
|
180
124
|
pops: (state) => slicesOf(state).map(() => 0),
|
|
181
125
|
},
|
|
182
126
|
allowedChildren: [],
|
|
183
|
-
// The
|
|
184
|
-
// the slices themselves, their paint and the selection are bound, so a theme
|
|
185
|
-
// tween re-paints and a `set` of `data` re-cuts the wedges.
|
|
127
|
+
// The structure (a label and legend row per row) is read once; slices, paint and selection are bound, so a theme tween re-paints and a `set` of `data` re-cuts.
|
|
186
128
|
children: (state) => {
|
|
187
129
|
const theme = () => state.theme
|
|
188
130
|
const slices = () => slicesOf(state)
|
|
@@ -209,8 +151,7 @@ export const PieChart = component2d({
|
|
|
209
151
|
slices,
|
|
210
152
|
legend: () => theme().legend,
|
|
211
153
|
dims: () => sliceDims(state),
|
|
212
|
-
//
|
|
213
|
-
// entries' type and chips.
|
|
154
|
+
// Spacing is fixed as built.
|
|
214
155
|
gap: state.theme.legend.itemGap / 3,
|
|
215
156
|
padding: state.theme.legend.padding,
|
|
216
157
|
}) as unknown as Node2D
|
|
@@ -220,15 +161,10 @@ export const PieChart = component2d({
|
|
|
220
161
|
return asParts([row as unknown as Node2D])
|
|
221
162
|
},
|
|
222
163
|
commands: {
|
|
223
|
-
/**
|
|
224
|
-
* Animate the whole chart in: the wedges sweep in clockwise from the theme's
|
|
225
|
-
* `startAngle`, then the labels fade up and the legend staggers in together.
|
|
226
|
-
* Its first frame arms the wedges, the labels and the legend; the hole's
|
|
227
|
-
* summary keeps its own signal. `duration` is the **whole** entrance.
|
|
228
|
-
*/
|
|
164
|
+
/** Wedges sweep in clockwise from `startAngle`, then labels fade up and the legend staggers in. `duration` is the whole entrance; the hole's summary keeps its own signal. */
|
|
229
165
|
enter: {
|
|
230
166
|
args: {},
|
|
231
|
-
// Held from
|
|
167
|
+
// Held from arrival, so it never shows finished ahead of its draw.
|
|
232
168
|
entrance: true,
|
|
233
169
|
easing: easeOut(),
|
|
234
170
|
run: (timeline, _state, children) => {
|
|
@@ -247,11 +183,7 @@ export const PieChart = component2d({
|
|
|
247
183
|
])
|
|
248
184
|
},
|
|
249
185
|
},
|
|
250
|
-
/**
|
|
251
|
-
* Fade the hole's summary up. Held out of `enter` on purpose: what sits in a
|
|
252
|
-
* donut's hole is usually the chart's conclusion, so the scene lands it on
|
|
253
|
-
* its own beat. Pair it with `showCenter: false`.
|
|
254
|
-
*/
|
|
186
|
+
/** Fade the hole's summary up. Not part of `enter`: it is usually the conclusion, which a scene lands on its own beat. */
|
|
255
187
|
revealCenter: {
|
|
256
188
|
args: {},
|
|
257
189
|
easing: easeOut(),
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
Shapes,
|
|
6
6
|
Text,
|
|
7
7
|
Types,
|
|
8
|
+
clamp,
|
|
8
9
|
component2d,
|
|
9
10
|
textOps,
|
|
10
11
|
type NodeOf,
|
|
11
|
-
type RectProps,
|
|
12
12
|
// Named so the column's emitted type reaches the Rect surface it spreads through the sdk, not core.
|
|
13
13
|
type Alignment as _Alignment,
|
|
14
14
|
type CornerRadiusResolved as _CornerRadiusResolved,
|
|
@@ -31,43 +31,16 @@ import {
|
|
|
31
31
|
|
|
32
32
|
import { fresh } from "../kit/entries"
|
|
33
33
|
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
|
|
34
|
-
import {
|
|
35
|
-
DEFAULT_LEGEND,
|
|
36
|
-
clampUnit,
|
|
37
|
-
type LegendConfig,
|
|
38
|
-
} from "../kit/shared"
|
|
34
|
+
import { DEFAULT_LEGEND, type LegendConfig } from "../kit/shared"
|
|
39
35
|
import type { PieSlice } from "./shared"
|
|
40
36
|
|
|
41
|
-
export interface PieLegendProps extends RectProps {
|
|
42
|
-
/** One entry per slice, in ring order. */
|
|
43
|
-
slices: PieSlice[]
|
|
44
|
-
/** Padding, caption style, gaps, and swatch geometry — see {@link LegendConfig}. */
|
|
45
|
-
legend: LegendConfig
|
|
46
|
-
/**
|
|
47
|
-
* Per-slice opacity multiplier, by index — the same signal that dims the ring,
|
|
48
|
-
* so an entry fades in step with the wedge it names. Missing entries read as 1.
|
|
49
|
-
*/
|
|
50
|
-
dims: number[]
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** How far an entry starts to the right of its slot when it enters, in px. */
|
|
54
37
|
const SLIDE = 16
|
|
55
38
|
|
|
56
39
|
/**
|
|
57
|
-
* The legend of a {@link PieChart}: a
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* ring rather than beneath the plot — a pie is as tall as it is wide, so the
|
|
62
|
-
* space it leaves over is a vertical strip. It is otherwise the same entry: the
|
|
63
|
-
* swatch carries the colour, the caption reads in the legend's neutral ink, and
|
|
64
|
-
* both take the shared {@link LegendConfig}.
|
|
65
|
-
*
|
|
66
|
-
* There is no show/hide here. A pie's marks are its *rows*, and a row that
|
|
67
|
-
* shouldn't be in the chart shouldn't be in the table — removing one would
|
|
68
|
-
* silently re-cut every other wedge's share, which is a different chart rather
|
|
69
|
-
* than the same chart with something hidden. Selection is what a pie has
|
|
70
|
-
* instead, and it arrives through {@link PieLegendProps.dims}.
|
|
40
|
+
* The legend of a {@link PieChart}: a column (it sits beside the ring, in the
|
|
41
|
+
* vertical strip it leaves) of caption-and-swatch entries, one per slice. No
|
|
42
|
+
* show/hide: removing a row would re-cut every other wedge's share. Selection
|
|
43
|
+
* arrives through `dims`.
|
|
71
44
|
*/
|
|
72
45
|
export const PieLegend = component2d({
|
|
73
46
|
key: "pieLegend",
|
|
@@ -75,13 +48,14 @@ export const PieLegend = component2d({
|
|
|
75
48
|
...Paint.state,
|
|
76
49
|
...Flow.state,
|
|
77
50
|
...Shapes.corners,
|
|
51
|
+
/** One entry per slice, in ring order. */
|
|
78
52
|
slices: fresh<PieSlice[]>(() => []),
|
|
79
53
|
legend: Types.custom<LegendConfig>(DEFAULT_LEGEND),
|
|
54
|
+
/** Per-slice opacity multiplier, the signal that dims the ring, so an entry fades in step with its wedge. Missing reads as 1. */
|
|
80
55
|
dims: fresh<number[]>(() => []),
|
|
81
56
|
arrivals: arrivalsEntry(),
|
|
82
57
|
},
|
|
83
|
-
//
|
|
84
|
-
// flow lays its children out `freeform`, every caption on top of the last.
|
|
58
|
+
// The flow must be stated: a `Rect` with none lays children out `freeform`, every caption on top of the last.
|
|
85
59
|
defaults: {
|
|
86
60
|
flow: "vertical",
|
|
87
61
|
align: "centerLeft",
|
|
@@ -90,10 +64,7 @@ export const PieLegend = component2d({
|
|
|
90
64
|
gap: (state) => state.legend.itemGap / 3,
|
|
91
65
|
padding: (state) => state.legend.padding,
|
|
92
66
|
},
|
|
93
|
-
//
|
|
94
|
-
// rather than the entry itself, so the entrance can animate the entry's own
|
|
95
|
-
// opacity without the two fighting over one value — opacity multiplies down
|
|
96
|
-
// the tree, which is exactly the composition wanted here.
|
|
67
|
+
// The selection dim rides a wrapper, so the entrance animates the entry's own opacity without the two fighting over one value.
|
|
97
68
|
children: (state) => {
|
|
98
69
|
const marker = state.legend.markerStyle
|
|
99
70
|
return state.slices.map((_, index) =>
|
|
@@ -101,7 +72,7 @@ export const PieLegend = component2d({
|
|
|
101
72
|
width: "hug",
|
|
102
73
|
height: "hug",
|
|
103
74
|
flow: "freeform",
|
|
104
|
-
opacity: () =>
|
|
75
|
+
opacity: () => clamp(state.dims[index] ?? 1, 0, 1),
|
|
105
76
|
children: [
|
|
106
77
|
Rect({
|
|
107
78
|
flow: "horizontal",
|
|
@@ -132,28 +103,16 @@ export const PieLegend = component2d({
|
|
|
132
103
|
},
|
|
133
104
|
commands: {
|
|
134
105
|
...Paint.commands,
|
|
135
|
-
/**
|
|
136
|
-
* Every entry at its pre-entrance state, shifted right and transparent: what
|
|
137
|
-
* a chart shows ahead of this legend's own beat in its entrance, since
|
|
138
|
-
* `enter` arms the entries only on its first frame.
|
|
139
|
-
*/
|
|
106
|
+
/** Every entry shifted right and transparent: what a chart shows ahead of this legend's beat, since `enter` arms only on its first frame. */
|
|
140
107
|
arm: (timeline, state) =>
|
|
141
108
|
timeline.animate(() => ({ arrivals: armed([...state.slices.keys()], { opacity: 0, shift: SLIDE }) })),
|
|
142
|
-
/**
|
|
143
|
-
* Slide + fade every entry in from the right, staggered top-to-bottom. Its
|
|
144
|
-
* first frame arms them all: shifted right and transparent.
|
|
145
|
-
*
|
|
146
|
-
* The timeline's duration is the **whole** entrance: the stagger is carved
|
|
147
|
-
* out of it rather than added on top, so the command takes exactly the time
|
|
148
|
-
* the timeline laid out for it.
|
|
149
|
-
*/
|
|
109
|
+
/** Slide and fade entries in from the right, top to bottom. The duration is the whole entrance, the stagger carved out of it. */
|
|
150
110
|
enter: (timeline, state) => {
|
|
151
111
|
const count = state.slices.length
|
|
152
112
|
if (count === 0) return timeline.wait(timeline.duration)
|
|
153
113
|
const from: Arrival = { opacity: 0, shift: SLIDE }
|
|
154
114
|
const arm = timeline.animate(() => ({ arrivals: armed([...state.slices.keys()], from) }))
|
|
155
|
-
// Each gap is a fifth of an entry's
|
|
156
|
-
// solved for from the budget rather than added to it.
|
|
115
|
+
// Each gap is a fifth of an entry's move, the axes' rhythm, solved from the budget.
|
|
157
116
|
const per = timeline.duration / (1 + Math.max(0, count - 1) / 5)
|
|
158
117
|
const step = per / 5
|
|
159
118
|
const ease = frameEase(timeline)
|