@motionscript/charts 0.1.0-alpha.8 → 0.1.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 +6 -0
- package/LICENSE +201 -201
- package/NOTICE +49 -49
- package/README.md +99 -99
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/{chunk-XROBIRQK.js → chunk-TJNHLQGV.js} +1 -1
- package/dist/browser/chunks/chunk-TJNHLQGV.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-border.js.map +1 -1
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/kit/entries.js.map +1 -1
- package/dist/kit/motion.js.map +1 -1
- package/dist/kit/paints.js.map +1 -1
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/horizontal-axis.js.map +1 -1
- package/dist/line-chart/index.d.ts +1 -1
- package/dist/line-chart/index.js +1 -1
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-legend.js.map +1 -1
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.js.map +1 -1
- package/package.json +4 -4
- package/registry.json +23 -23
- package/src/bar-chart/bar-chart.ts +542 -542
- package/src/bar-chart/category-axis.ts +162 -162
- package/src/bar-chart/chart-bars.ts +287 -287
- package/src/bar-chart/index.ts +31 -31
- package/src/bar-chart/shared.ts +354 -354
- package/src/engine.ts +28 -28
- package/src/index.ts +5 -5
- package/src/kit/cartesian.ts +81 -81
- package/src/kit/chart-border.ts +50 -50
- package/src/kit/chart-canvas.ts +122 -122
- package/src/kit/chart-legend.ts +229 -229
- package/src/kit/entries.ts +20 -20
- package/src/kit/motion.ts +90 -90
- package/src/kit/paints.ts +15 -15
- package/src/kit/shared.ts +782 -782
- package/src/kit/vertical-axis.ts +194 -194
- package/src/line-chart/chart-line.ts +156 -156
- package/src/line-chart/chart-region.ts +90 -90
- package/src/line-chart/horizontal-axis.ts +151 -151
- package/src/line-chart/index.ts +39 -39
- package/src/line-chart/line-chart.ts +598 -598
- package/src/line-chart/shared.ts +473 -473
- package/src/line-chart/x-scale.ts +382 -382
- package/src/nodes.ts +18 -18
- package/src/pie-chart/index.ts +37 -37
- package/src/pie-chart/pie-chart.ts +296 -296
- package/src/pie-chart/pie-legend.ts +169 -169
- package/src/pie-chart/pie-ring.ts +848 -848
- package/src/pie-chart/shared.ts +496 -496
- package/dist/browser/chunks/chunk-XROBIRQK.js.map +0 -7
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Rect,
|
|
3
|
-
Text,
|
|
4
|
-
Types,
|
|
5
|
-
component2d,
|
|
6
|
-
insetsOps,
|
|
7
|
-
textOps,
|
|
8
|
-
type NodeOf,
|
|
9
|
-
type Node2D,
|
|
10
|
-
type Node2DProps,
|
|
11
|
-
} from "@motionscript/sdk"
|
|
12
|
-
|
|
13
|
-
import { asParts, fresh } from "../kit/entries"
|
|
14
|
-
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
|
|
15
|
-
import {
|
|
16
|
-
DEFAULT_X_AXIS,
|
|
17
|
-
clampUnit,
|
|
18
|
-
enterBudget,
|
|
19
|
-
type AxisConfig,
|
|
20
|
-
} from "../kit/shared"
|
|
21
|
-
|
|
22
|
-
export interface CategoryAxisProps extends Node2DProps {
|
|
23
|
-
/** One caption per category slot, left to right — the chart's rows, in order. */
|
|
24
|
-
categories: string[]
|
|
25
|
-
/** Caption/title text style and gutters — see {@link AxisConfig}. */
|
|
26
|
-
axis: AxisConfig
|
|
27
|
-
/**
|
|
28
|
-
* Caption drawn centred in the gutter beneath the category row. Empty string
|
|
29
|
-
* (the default) hides it.
|
|
30
|
-
*/
|
|
31
|
-
label: string
|
|
32
|
-
/**
|
|
33
|
-
* Per-category opacity multiplier — how the chart's category spotlight reaches
|
|
34
|
-
* the captions, so the name under a dimmed column dims with it. Missing
|
|
35
|
-
* entries read as 1.
|
|
36
|
-
*/
|
|
37
|
-
dims: number[]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** How far a caption starts above its slot when it enters, in px. */
|
|
41
|
-
const SLIDE = 24
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The **categorical** horizontal axis of a {@link BarChart}: a row of
|
|
45
|
-
* {@link Text} captions, one per row of the data, each centred on its slot
|
|
46
|
-
* beneath the baseline.
|
|
47
|
-
*
|
|
48
|
-
* Not a variant of the line chart's `HorizontalAxis` and deliberately not
|
|
49
|
-
* folded into it: that axis places markers by *value* across a numeric range,
|
|
50
|
-
* this one divides its width into equal slots and centres a name in each. The
|
|
51
|
-
* two look alike on screen and share nothing underneath — one reads a scale,
|
|
52
|
-
* the other counts rows.
|
|
53
|
-
*
|
|
54
|
-
* The slot arithmetic is the same as the plot's, because this node is laid out
|
|
55
|
-
* to exactly the plot's width (the chart reserves the axis gutter with an
|
|
56
|
-
* invisible twin of the value axis, as the line chart does), so a caption lands
|
|
57
|
-
* under the columns it names without either side being told about the other.
|
|
58
|
-
*
|
|
59
|
-
* `enter` staggers the captions in left-to-right, each lifted a little above
|
|
60
|
-
* its slot and transparent; the resting state is the landed one, so a chart
|
|
61
|
-
* that is never told to enter still reads as finished.
|
|
62
|
-
*/
|
|
63
|
-
export const CategoryAxis = component2d({
|
|
64
|
-
key: "categoryAxis",
|
|
65
|
-
state: {
|
|
66
|
-
categories: fresh<string[]>(() => []),
|
|
67
|
-
axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
|
|
68
|
-
label: "",
|
|
69
|
-
dims: fresh<number[]>(() => []),
|
|
70
|
-
arrivals: arrivalsEntry(),
|
|
71
|
-
},
|
|
72
|
-
// A plain node filled its parent; one made of its captions would hug them.
|
|
73
|
-
defaults: { width: "fill", height: "fill" },
|
|
74
|
-
children: (state) => {
|
|
75
|
-
const tickPad = insetsOps.resolve(state.axis.tickPadding).top
|
|
76
|
-
const titlePad = insetsOps.resolve(state.axis.titlePadding).top
|
|
77
|
-
|
|
78
|
-
const column: Node2D[] = []
|
|
79
|
-
// The column's parent is the axis: its box is what the slots divide.
|
|
80
|
-
const width = () => (column[0]?.parent as Node2D | null | undefined)?.layoutBounds.width ?? 0
|
|
81
|
-
/** Centre x of slot `i`, in the node's centred space. */
|
|
82
|
-
const slotCenter = (index: number) => {
|
|
83
|
-
const n = state.categories.length
|
|
84
|
-
if (n <= 0) return 0
|
|
85
|
-
return -width() / 2 + (width() / n) * (index + 0.5)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Each caption sits in a hug box carrying the spotlight dim, so the
|
|
89
|
-
// entrance can animate the text's own opacity without the two fighting over
|
|
90
|
-
// one value — opacity multiplies down the tree, which is exactly the
|
|
91
|
-
// composition wanted here.
|
|
92
|
-
const slots = state.categories.map((caption, i) =>
|
|
93
|
-
new Rect({
|
|
94
|
-
width: "hug",
|
|
95
|
-
height: "hug",
|
|
96
|
-
flow: "freeform",
|
|
97
|
-
opacity: () => clampUnit(state.dims[i] ?? 1),
|
|
98
|
-
x: () => slotCenter(i),
|
|
99
|
-
children: [
|
|
100
|
-
new Text({
|
|
101
|
-
...textOps.styleProps(state.axis.labelStyle),
|
|
102
|
-
text: caption,
|
|
103
|
-
opacity: () => arrivalOf(state.arrivals, i).opacity,
|
|
104
|
-
y: () => arrivalOf(state.arrivals, i).shift,
|
|
105
|
-
}),
|
|
106
|
-
],
|
|
107
|
-
})
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
const children: Node2D[] = [new Rect({ width: "fill", height: "hug", padding: { top: tickPad }, children: slots })]
|
|
111
|
-
if (state.label !== "") {
|
|
112
|
-
children.push(
|
|
113
|
-
new Text({
|
|
114
|
-
...textOps.styleProps(state.axis.titleStyle),
|
|
115
|
-
text: state.label,
|
|
116
|
-
opacity: () => arrivalOf(state.arrivals, "label").opacity,
|
|
117
|
-
y: () => arrivalOf(state.arrivals, "label").shift,
|
|
118
|
-
})
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
column.push(new Rect({ flow: "vertical", gap: titlePad, height: "hug", width: "fill", children }))
|
|
123
|
-
return asParts(column)
|
|
124
|
-
},
|
|
125
|
-
commands: {
|
|
126
|
-
/**
|
|
127
|
-
* Slide + fade every caption in from above, staggered left-to-right, then
|
|
128
|
-
* bring the title down behind them. Its first frame arms them all: lifted
|
|
129
|
-
* by `slide` and transparent.
|
|
130
|
-
*
|
|
131
|
-
* The timeline's duration is the **whole** entrance — the stagger and the
|
|
132
|
-
* title are carved out of it rather than added on top. See {@link enterBudget}.
|
|
133
|
-
*
|
|
134
|
-
* `stagger` is seconds between successive captions (derived when omitted);
|
|
135
|
-
* `slide` is the starting upward offset in px (node space is y-up, so a
|
|
136
|
-
* positive start slides *down* into place).
|
|
137
|
-
*/
|
|
138
|
-
enter: {
|
|
139
|
-
args: {
|
|
140
|
-
stagger: Types.custom<number | undefined>(undefined),
|
|
141
|
-
slide: Types.number(SLIDE),
|
|
142
|
-
},
|
|
143
|
-
run: (timeline, state) => {
|
|
144
|
-
const { stagger, slide } = timeline.args
|
|
145
|
-
const count = state.categories.length
|
|
146
|
-
const captioned = state.label !== ""
|
|
147
|
-
const from: Arrival = { opacity: 0, shift: slide }
|
|
148
|
-
const keys = [...state.categories.keys(), ...(captioned ? ["label"] : [])]
|
|
149
|
-
const arm = timeline.animate(() => ({ arrivals: armed(keys, from) }))
|
|
150
|
-
if (count === 0) return arm
|
|
151
|
-
const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
|
|
152
|
-
const ease = frameEase(timeline)
|
|
153
|
-
const lastCaptionEnds = (count - 1) * step + per
|
|
154
|
-
const captions = state.categories.map((_, i) => ({ at: i * step, duration: per, piece: arrival(timeline, i, from, ease) }))
|
|
155
|
-
const label = captioned ? [{ at: lastCaptionEnds, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
|
|
156
|
-
return timeline.together([arm, beats(timeline, [...captions, ...label])])
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
export type CategoryAxis = NodeOf<typeof CategoryAxis>
|
|
1
|
+
import {
|
|
2
|
+
Rect,
|
|
3
|
+
Text,
|
|
4
|
+
Types,
|
|
5
|
+
component2d,
|
|
6
|
+
insetsOps,
|
|
7
|
+
textOps,
|
|
8
|
+
type NodeOf,
|
|
9
|
+
type Node2D,
|
|
10
|
+
type Node2DProps,
|
|
11
|
+
} from "@motionscript/sdk"
|
|
12
|
+
|
|
13
|
+
import { asParts, fresh } from "../kit/entries"
|
|
14
|
+
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_X_AXIS,
|
|
17
|
+
clampUnit,
|
|
18
|
+
enterBudget,
|
|
19
|
+
type AxisConfig,
|
|
20
|
+
} from "../kit/shared"
|
|
21
|
+
|
|
22
|
+
export interface CategoryAxisProps extends Node2DProps {
|
|
23
|
+
/** One caption per category slot, left to right — the chart's rows, in order. */
|
|
24
|
+
categories: string[]
|
|
25
|
+
/** Caption/title text style and gutters — see {@link AxisConfig}. */
|
|
26
|
+
axis: AxisConfig
|
|
27
|
+
/**
|
|
28
|
+
* Caption drawn centred in the gutter beneath the category row. Empty string
|
|
29
|
+
* (the default) hides it.
|
|
30
|
+
*/
|
|
31
|
+
label: string
|
|
32
|
+
/**
|
|
33
|
+
* Per-category opacity multiplier — how the chart's category spotlight reaches
|
|
34
|
+
* the captions, so the name under a dimmed column dims with it. Missing
|
|
35
|
+
* entries read as 1.
|
|
36
|
+
*/
|
|
37
|
+
dims: number[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** How far a caption starts above its slot when it enters, in px. */
|
|
41
|
+
const SLIDE = 24
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The **categorical** horizontal axis of a {@link BarChart}: a row of
|
|
45
|
+
* {@link Text} captions, one per row of the data, each centred on its slot
|
|
46
|
+
* beneath the baseline.
|
|
47
|
+
*
|
|
48
|
+
* Not a variant of the line chart's `HorizontalAxis` and deliberately not
|
|
49
|
+
* folded into it: that axis places markers by *value* across a numeric range,
|
|
50
|
+
* this one divides its width into equal slots and centres a name in each. The
|
|
51
|
+
* two look alike on screen and share nothing underneath — one reads a scale,
|
|
52
|
+
* the other counts rows.
|
|
53
|
+
*
|
|
54
|
+
* The slot arithmetic is the same as the plot's, because this node is laid out
|
|
55
|
+
* to exactly the plot's width (the chart reserves the axis gutter with an
|
|
56
|
+
* invisible twin of the value axis, as the line chart does), so a caption lands
|
|
57
|
+
* under the columns it names without either side being told about the other.
|
|
58
|
+
*
|
|
59
|
+
* `enter` staggers the captions in left-to-right, each lifted a little above
|
|
60
|
+
* its slot and transparent; the resting state is the landed one, so a chart
|
|
61
|
+
* that is never told to enter still reads as finished.
|
|
62
|
+
*/
|
|
63
|
+
export const CategoryAxis = component2d({
|
|
64
|
+
key: "categoryAxis",
|
|
65
|
+
state: {
|
|
66
|
+
categories: fresh<string[]>(() => []),
|
|
67
|
+
axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
|
|
68
|
+
label: "",
|
|
69
|
+
dims: fresh<number[]>(() => []),
|
|
70
|
+
arrivals: arrivalsEntry(),
|
|
71
|
+
},
|
|
72
|
+
// A plain node filled its parent; one made of its captions would hug them.
|
|
73
|
+
defaults: { width: "fill", height: "fill" },
|
|
74
|
+
children: (state) => {
|
|
75
|
+
const tickPad = insetsOps.resolve(state.axis.tickPadding).top
|
|
76
|
+
const titlePad = insetsOps.resolve(state.axis.titlePadding).top
|
|
77
|
+
|
|
78
|
+
const column: Node2D[] = []
|
|
79
|
+
// The column's parent is the axis: its box is what the slots divide.
|
|
80
|
+
const width = () => (column[0]?.parent as Node2D | null | undefined)?.layoutBounds.width ?? 0
|
|
81
|
+
/** Centre x of slot `i`, in the node's centred space. */
|
|
82
|
+
const slotCenter = (index: number) => {
|
|
83
|
+
const n = state.categories.length
|
|
84
|
+
if (n <= 0) return 0
|
|
85
|
+
return -width() / 2 + (width() / n) * (index + 0.5)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Each caption sits in a hug box carrying the spotlight dim, so the
|
|
89
|
+
// entrance can animate the text's own opacity without the two fighting over
|
|
90
|
+
// one value — opacity multiplies down the tree, which is exactly the
|
|
91
|
+
// composition wanted here.
|
|
92
|
+
const slots = state.categories.map((caption, i) =>
|
|
93
|
+
new Rect({
|
|
94
|
+
width: "hug",
|
|
95
|
+
height: "hug",
|
|
96
|
+
flow: "freeform",
|
|
97
|
+
opacity: () => clampUnit(state.dims[i] ?? 1),
|
|
98
|
+
x: () => slotCenter(i),
|
|
99
|
+
children: [
|
|
100
|
+
new Text({
|
|
101
|
+
...textOps.styleProps(state.axis.labelStyle),
|
|
102
|
+
text: caption,
|
|
103
|
+
opacity: () => arrivalOf(state.arrivals, i).opacity,
|
|
104
|
+
y: () => arrivalOf(state.arrivals, i).shift,
|
|
105
|
+
}),
|
|
106
|
+
],
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const children: Node2D[] = [new Rect({ width: "fill", height: "hug", padding: { top: tickPad }, children: slots })]
|
|
111
|
+
if (state.label !== "") {
|
|
112
|
+
children.push(
|
|
113
|
+
new Text({
|
|
114
|
+
...textOps.styleProps(state.axis.titleStyle),
|
|
115
|
+
text: state.label,
|
|
116
|
+
opacity: () => arrivalOf(state.arrivals, "label").opacity,
|
|
117
|
+
y: () => arrivalOf(state.arrivals, "label").shift,
|
|
118
|
+
})
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
column.push(new Rect({ flow: "vertical", gap: titlePad, height: "hug", width: "fill", children }))
|
|
123
|
+
return asParts(column)
|
|
124
|
+
},
|
|
125
|
+
commands: {
|
|
126
|
+
/**
|
|
127
|
+
* Slide + fade every caption in from above, staggered left-to-right, then
|
|
128
|
+
* bring the title down behind them. Its first frame arms them all: lifted
|
|
129
|
+
* by `slide` and transparent.
|
|
130
|
+
*
|
|
131
|
+
* The timeline's duration is the **whole** entrance — the stagger and the
|
|
132
|
+
* title are carved out of it rather than added on top. See {@link enterBudget}.
|
|
133
|
+
*
|
|
134
|
+
* `stagger` is seconds between successive captions (derived when omitted);
|
|
135
|
+
* `slide` is the starting upward offset in px (node space is y-up, so a
|
|
136
|
+
* positive start slides *down* into place).
|
|
137
|
+
*/
|
|
138
|
+
enter: {
|
|
139
|
+
args: {
|
|
140
|
+
stagger: Types.custom<number | undefined>(undefined),
|
|
141
|
+
slide: Types.number(SLIDE),
|
|
142
|
+
},
|
|
143
|
+
run: (timeline, state) => {
|
|
144
|
+
const { stagger, slide } = timeline.args
|
|
145
|
+
const count = state.categories.length
|
|
146
|
+
const captioned = state.label !== ""
|
|
147
|
+
const from: Arrival = { opacity: 0, shift: slide }
|
|
148
|
+
const keys = [...state.categories.keys(), ...(captioned ? ["label"] : [])]
|
|
149
|
+
const arm = timeline.animate(() => ({ arrivals: armed(keys, from) }))
|
|
150
|
+
if (count === 0) return arm
|
|
151
|
+
const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
|
|
152
|
+
const ease = frameEase(timeline)
|
|
153
|
+
const lastCaptionEnds = (count - 1) * step + per
|
|
154
|
+
const captions = state.categories.map((_, i) => ({ at: i * step, duration: per, piece: arrival(timeline, i, from, ease) }))
|
|
155
|
+
const label = captioned ? [{ at: lastCaptionEnds, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
|
|
156
|
+
return timeline.together([arm, beats(timeline, [...captions, ...label])])
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export type CategoryAxis = NodeOf<typeof CategoryAxis>
|