@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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Rect,
|
|
3
|
+
Text,
|
|
4
|
+
Types,
|
|
5
|
+
component2d,
|
|
6
|
+
type NodeOf,
|
|
7
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
8
|
+
type Node2D,
|
|
9
|
+
type Node2DProps as _Node2DProps,
|
|
10
|
+
} from "@motionscript/sdk"
|
|
11
|
+
import { enterBudget } from "@motionscript/sdk/component"
|
|
12
|
+
|
|
13
|
+
import { boundText } from "./cartesian"
|
|
14
|
+
import { asParts, fresh } from "./entries"
|
|
15
|
+
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "./motion"
|
|
16
|
+
import { DEFAULT_X_AXIS, type AxisConfig, type Range } from "./shared"
|
|
17
|
+
import { axisPads, markerCaption } from "./vertical-axis"
|
|
18
|
+
|
|
19
|
+
const SLIDE = 24
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A row of {@link Text} markers under the plot, the mirror of `VerticalAxis`
|
|
23
|
+
* (bound text and positions, landed resting state). Markers are placed by value
|
|
24
|
+
* across the range, whatever they say: `2026-03` sits three months along, not
|
|
25
|
+
* three slots along, unlike a bar chart's category axis. An end marker may hang
|
|
26
|
+
* past the row by `overhang` px on either side, then is held inside it.
|
|
27
|
+
*/
|
|
28
|
+
export const HorizontalAxis = component2d({
|
|
29
|
+
key: "horizontalAxis",
|
|
30
|
+
state: {
|
|
31
|
+
range: fresh<Range>(() => [0, 1]),
|
|
32
|
+
/** Tick values in data units. */
|
|
33
|
+
markers: fresh<number[]>(() => []),
|
|
34
|
+
/** One caption per marker in its order; a missing one reads as the bare numeric value. */
|
|
35
|
+
captions: fresh<string[]>(() => []),
|
|
36
|
+
axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
|
|
37
|
+
/** Caption centred beneath the markers; empty hides it. */
|
|
38
|
+
label: "",
|
|
39
|
+
/** How far past the row's left and right ends a marker may reach, in px. */
|
|
40
|
+
overhang: Types.custom<{ left: number; right: number }>({ left: 0, right: 0 }),
|
|
41
|
+
arrivals: arrivalsEntry(),
|
|
42
|
+
},
|
|
43
|
+
// A node made of markers would otherwise hug them.
|
|
44
|
+
defaults: { width: "fill", height: "fill" },
|
|
45
|
+
children: (state) => {
|
|
46
|
+
const pads = () => axisPads(state.axis, "top")
|
|
47
|
+
const sorted = () => [...state.markers].sort((a, b) => a - b)
|
|
48
|
+
const fracAt = (value: number | undefined) => {
|
|
49
|
+
if (value === undefined) return 0
|
|
50
|
+
const [min, max] = state.range
|
|
51
|
+
return max <= min ? 0 : (value - min) / (max - min)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const markers = state.markers.map((_, i) => {
|
|
55
|
+
const width = () => (marker.parent as Node2D | null)?.layoutBounds.width ?? 0
|
|
56
|
+
const marker: Node2D = new Text({
|
|
57
|
+
...boundText(() => state.axis.labelStyle),
|
|
58
|
+
text: () => {
|
|
59
|
+
const value = sorted()[i]
|
|
60
|
+
return value === undefined ? "" : markerCaption(state, value)
|
|
61
|
+
},
|
|
62
|
+
x: () => {
|
|
63
|
+
const w = width()
|
|
64
|
+
const half = marker.layoutBounds.width / 2
|
|
65
|
+
const centred = (fracAt(sorted()[i]) - 0.5) * w
|
|
66
|
+
return Math.max(-w / 2 - state.overhang.left + half, Math.min(w / 2 + state.overhang.right - half, centred))
|
|
67
|
+
},
|
|
68
|
+
opacity: () => arrivalOf(state.arrivals, i).opacity,
|
|
69
|
+
y: () => arrivalOf(state.arrivals, i).shift,
|
|
70
|
+
})
|
|
71
|
+
return marker
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const children: Node2D[] = [
|
|
75
|
+
new Rect({ width: "fill", height: "hug", padding: () => ({ top: pads().tick }), children: markers }) as unknown as Node2D,
|
|
76
|
+
]
|
|
77
|
+
if (state.label !== "") {
|
|
78
|
+
children.push(
|
|
79
|
+
new Text({
|
|
80
|
+
...boundText(() => state.axis.titleStyle),
|
|
81
|
+
text: state.label,
|
|
82
|
+
opacity: () => arrivalOf(state.arrivals, "label").opacity,
|
|
83
|
+
y: () => arrivalOf(state.arrivals, "label").shift,
|
|
84
|
+
}) as unknown as Node2D
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
return asParts([new Rect({ flow: "vertical", gap: () => pads().title, height: "hug", width: "fill", children }) as unknown as Node2D])
|
|
88
|
+
},
|
|
89
|
+
commands: {
|
|
90
|
+
/** Stagger markers in left to right, then the caption. The duration is the whole entrance ({@link enterBudget}). */
|
|
91
|
+
enter: {
|
|
92
|
+
args: {
|
|
93
|
+
stagger: Types.custom<number | undefined>(undefined),
|
|
94
|
+
slide: Types.number(SLIDE),
|
|
95
|
+
},
|
|
96
|
+
run: (timeline, state) => {
|
|
97
|
+
const { stagger, slide } = timeline.args
|
|
98
|
+
const count = state.markers.length
|
|
99
|
+
const captioned = state.label !== ""
|
|
100
|
+
const from: Arrival = { opacity: 0, shift: slide }
|
|
101
|
+
const keys = [...state.markers.keys(), ...(captioned ? ["label"] : [])]
|
|
102
|
+
const arm = timeline.animate(() => ({ arrivals: armed(keys, from) }))
|
|
103
|
+
if (count === 0) return arm
|
|
104
|
+
const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
|
|
105
|
+
const ease = frameEase(timeline)
|
|
106
|
+
const markers = state.markers.map((_, i) => ({ at: i * step, duration: per, piece: arrival(timeline, i, from, ease) }))
|
|
107
|
+
const label = captioned ? [{ at: (count - 1) * step + per, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
|
|
108
|
+
return timeline.together([arm, beats(timeline, [...markers, ...label])])
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export type HorizontalAxis = NodeOf<typeof HorizontalAxis>
|
package/src/kit/motion.ts
CHANGED
|
@@ -3,7 +3,23 @@ import { Types, easeOut, type EasingFunction, type Piece, type Timeline } from "
|
|
|
3
3
|
const identity: EasingFunction = (t) => t
|
|
4
4
|
|
|
5
5
|
/** Any component's timeline: these helpers only build pieces, which every timeline builds alike. */
|
|
6
|
-
type AnyTimeline = Timeline<any, any>
|
|
6
|
+
export type AnyTimeline = Timeline<any, any>
|
|
7
|
+
|
|
8
|
+
/** How far a legend starts below its slot when `enter` brings it in, in px. */
|
|
9
|
+
export const LEGEND_SLIDE = 20
|
|
10
|
+
|
|
11
|
+
/** Where a legend waits before it comes up: below its place, above it, or to its right. */
|
|
12
|
+
export function legendAway(at: string): { x?: number; y?: number } {
|
|
13
|
+
if (at === "top") return { y: LEGEND_SLIDE }
|
|
14
|
+
if (at === "right") return { x: LEGEND_SLIDE }
|
|
15
|
+
return { y: -LEGEND_SLIDE }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Marker count when an axis states no ticks. */
|
|
19
|
+
export const DEFAULT_MARKERS = 5
|
|
20
|
+
|
|
21
|
+
/** How a cartesian `enter` splits its duration: axes arriving, marks drawing, legend coming up. Sums to 1. */
|
|
22
|
+
export const ENTER_SHARE = { axes: 0.35, marks: 0.5, legend: 0.15 } as const
|
|
7
23
|
|
|
8
24
|
/**
|
|
9
25
|
* The ease a part's own frames apply to `t`: none when its command was given
|
|
@@ -49,7 +65,7 @@ export interface Arrival {
|
|
|
49
65
|
/** Each piece's arrival, by key. An unlisted piece has landed. */
|
|
50
66
|
export type Arrivals = Readonly<Record<string, Arrival>>
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
const LANDED: Arrival = { opacity: 1, shift: 0 }
|
|
53
69
|
|
|
54
70
|
/**
|
|
55
71
|
* A part's private record of its pieces' arrivals. A write merges into it, so
|
|
@@ -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,
|
|
@@ -29,45 +29,18 @@ import {
|
|
|
29
29
|
type Vector2 as _Vector2,
|
|
30
30
|
} from "@motionscript/sdk"
|
|
31
31
|
|
|
32
|
-
import { fresh } from "
|
|
33
|
-
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
clampUnit,
|
|
37
|
-
type LegendConfig,
|
|
38
|
-
} from "../kit/shared"
|
|
39
|
-
import type { PieSlice } from "./shared"
|
|
40
|
-
|
|
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
|
-
}
|
|
32
|
+
import { fresh } from "./entries"
|
|
33
|
+
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "./motion"
|
|
34
|
+
import { DEFAULT_LEGEND, type LegendConfig } from "./shared"
|
|
35
|
+
import type { PieSlice } from "./polar"
|
|
52
36
|
|
|
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,22 +64,19 @@ 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
|
-
const marker = state.legend.markerStyle
|
|
69
|
+
const marker = () => state.legend.markerStyle
|
|
99
70
|
return state.slices.map((_, index) =>
|
|
100
71
|
Rect({
|
|
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",
|
|
108
|
-
gap: 12,
|
|
79
|
+
gap: () => Math.round(state.legend.textStyle.fontSize === undefined ? 12 : Number(state.legend.textStyle.fontSize) * 0.4),
|
|
109
80
|
align: "center",
|
|
110
81
|
width: "hug",
|
|
111
82
|
height: "hug",
|
|
@@ -113,15 +84,16 @@ export const PieLegend = component2d({
|
|
|
113
84
|
x: () => arrivalOf(state.arrivals, index).shift,
|
|
114
85
|
children: [
|
|
115
86
|
Rect({
|
|
116
|
-
width: marker.
|
|
117
|
-
height: marker.height,
|
|
118
|
-
cornerRadius: marker.borderRadius,
|
|
87
|
+
width: () => marker().width,
|
|
88
|
+
height: () => marker().height,
|
|
89
|
+
cornerRadius: () => marker().borderRadius,
|
|
119
90
|
fill: () => state.slices[index]?.fill ?? "white",
|
|
120
|
-
stroke: marker.stroke,
|
|
121
|
-
shadow: marker.shadow,
|
|
122
91
|
}),
|
|
123
92
|
Text({
|
|
124
93
|
...textOps.styleProps(state.legend.textStyle),
|
|
94
|
+
fill: () => state.legend.textStyle.fill ?? "white",
|
|
95
|
+
fontSize: () => state.legend.textStyle.fontSize ?? 28,
|
|
96
|
+
fontWeight: () => state.legend.textStyle.fontWeight ?? 500,
|
|
125
97
|
text: () => state.slices[index]?.category ?? "",
|
|
126
98
|
}),
|
|
127
99
|
],
|
|
@@ -132,28 +104,16 @@ export const PieLegend = component2d({
|
|
|
132
104
|
},
|
|
133
105
|
commands: {
|
|
134
106
|
...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
|
-
*/
|
|
107
|
+
/** 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
108
|
arm: (timeline, state) =>
|
|
141
109
|
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
|
-
*/
|
|
110
|
+
/** Slide and fade entries in from the right, top to bottom. The duration is the whole entrance, the stagger carved out of it. */
|
|
150
111
|
enter: (timeline, state) => {
|
|
151
112
|
const count = state.slices.length
|
|
152
113
|
if (count === 0) return timeline.wait(timeline.duration)
|
|
153
114
|
const from: Arrival = { opacity: 0, shift: SLIDE }
|
|
154
115
|
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.
|
|
116
|
+
// Each gap is a fifth of an entry's move, the axes' rhythm, solved from the budget.
|
|
157
117
|
const per = timeline.duration / (1 + Math.max(0, count - 1) / 5)
|
|
158
118
|
const step = per / 5
|
|
159
119
|
const ease = frameEase(timeline)
|
package/src/kit/polar.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** What the polar charts (pie, radial) share: slices, ring geometry, the hole's summary and the selection fade. */
|
|
2
|
+
|
|
3
|
+
import { clamp, fillOps, type Fill, type FillResolved, type TextStyle, type Vector2 } from "@motionscript/sdk"
|
|
4
|
+
|
|
5
|
+
/** One resolved slice, which the ring and legend consume; the chart resolves it from `data` and two field names. */
|
|
6
|
+
export interface PieSlice {
|
|
7
|
+
/** Label and legend caption. */
|
|
8
|
+
category: string
|
|
9
|
+
/** The wedge covers `value / (sum of all values)`. */
|
|
10
|
+
value: number
|
|
11
|
+
/** Wedge and swatch paint. */
|
|
12
|
+
fill: Fill
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const DEG = Math.PI / 180
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Opacity of a slice under the selection: 1 while nothing is out (`focus` is the
|
|
19
|
+
* most-grown pop) and for the selected slice itself, falling toward `dimOpacity`
|
|
20
|
+
* for the rest as the selection grows.
|
|
21
|
+
*/
|
|
22
|
+
export function selectionFade(focus: number, dimOpacity: number, pop: number): number {
|
|
23
|
+
if (focus <= 0) return 1
|
|
24
|
+
return 1 - (1 - clamp(dimOpacity, 0, 1)) * focus * (1 - pop)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The point at radius `r`, angle `a` on a ring about the origin in centred y-up space. Angles run clockwise from 12 o'clock. */
|
|
28
|
+
export function ringPoint(r: number, a: number): Vector2 {
|
|
29
|
+
return { x: r * Math.sin(a), y: r * Math.cos(a) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The summary set in the ring's hole. */
|
|
33
|
+
export interface CenterConfig {
|
|
34
|
+
labelStyle: TextStyle
|
|
35
|
+
captionStyle: TextStyle
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** How far everything outside the selection fades while any slice is selected; fainter than a gapless chart's, as a ring's slices touch. */
|
|
39
|
+
export const PIE_SPOTLIGHT_DIM = 0.15
|
|
40
|
+
|
|
41
|
+
const resolvedFills = new WeakMap<object, FillResolved[]>()
|
|
42
|
+
|
|
43
|
+
export function resolvedFill(fill: Fill): FillResolved[] {
|
|
44
|
+
if (typeof fill !== "object" || fill === null) return fillOps.resolve(fill)
|
|
45
|
+
let resolved = resolvedFills.get(fill)
|
|
46
|
+
if (resolved === undefined) resolvedFills.set(fill, resolved = fillOps.resolve(fill))
|
|
47
|
+
return resolved
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Each slice's paint, resolved once per fill object. */
|
|
51
|
+
export function ringFills(slices: readonly PieSlice[]): FillResolved[][] {
|
|
52
|
+
return slices.map((slice) => resolvedFill(slice.fill))
|
|
53
|
+
}
|
|
54
|
+
|