@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
|
@@ -2,63 +2,34 @@ import {
|
|
|
2
2
|
Rect,
|
|
3
3
|
Text,
|
|
4
4
|
Types,
|
|
5
|
+
clamp,
|
|
5
6
|
component2d,
|
|
6
|
-
insetsOps,
|
|
7
|
-
textOps,
|
|
8
7
|
type NodeOf,
|
|
8
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
9
9
|
type Node2D,
|
|
10
|
-
type Node2DProps,
|
|
10
|
+
type Node2DProps as _Node2DProps,
|
|
11
11
|
} from "@motionscript/sdk"
|
|
12
|
+
import { enterBudget } from "@motionscript/sdk/component"
|
|
12
13
|
|
|
14
|
+
import { boundText } from "../kit/cartesian"
|
|
13
15
|
import { asParts, fresh } from "../kit/entries"
|
|
14
16
|
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
|
|
15
|
-
import {
|
|
16
|
-
|
|
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
|
-
}
|
|
17
|
+
import { DEFAULT_X_AXIS, type AxisConfig } from "../kit/shared"
|
|
18
|
+
import { axisPads, captionColumn } from "../kit/vertical-axis"
|
|
39
19
|
|
|
40
|
-
/** How far a caption starts above its slot when it enters, in px. */
|
|
41
20
|
const SLIDE = 24
|
|
42
21
|
|
|
22
|
+
/** Slot `index`'s centre along a run of `length` px divided into `count` equal slots, first slot at `-length / 2`. */
|
|
23
|
+
export function slotCentre(index: number, count: number, length: number): number {
|
|
24
|
+
return count <= 0 ? 0 : -length / 2 + (length / count) * (index + 0.5)
|
|
25
|
+
}
|
|
26
|
+
|
|
43
27
|
/**
|
|
44
|
-
* The
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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.
|
|
28
|
+
* The categorical axis of a {@link BarChart}: one {@link Text} caption per row,
|
|
29
|
+
* centred on its slot. `horizontal` is a row under the plot, first category on
|
|
30
|
+
* the left; `vertical` a right-aligned column beside it, first category at the
|
|
31
|
+
* top. It is laid out to exactly the plot's length, so each caption lands by its
|
|
32
|
+
* bars with neither side told about the other. The resting state is landed.
|
|
62
33
|
*/
|
|
63
34
|
export const CategoryAxis = component2d({
|
|
64
35
|
key: "categoryAxis",
|
|
@@ -66,74 +37,69 @@ export const CategoryAxis = component2d({
|
|
|
66
37
|
categories: fresh<string[]>(() => []),
|
|
67
38
|
axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
|
|
68
39
|
label: "",
|
|
40
|
+
orientation: Types.oneOf(["horizontal", "vertical"] as const),
|
|
41
|
+
/** Per-category opacity multiplier, so the caption by a dimmed bar dims with it. Missing reads as 1. */
|
|
69
42
|
dims: fresh<number[]>(() => []),
|
|
70
43
|
arrivals: arrivalsEntry(),
|
|
71
44
|
},
|
|
72
|
-
// A
|
|
45
|
+
// A node made of captions would otherwise hug them.
|
|
73
46
|
defaults: { width: "fill", height: "fill" },
|
|
74
47
|
children: (state) => {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
48
|
+
const n = state.categories.length
|
|
49
|
+
const dim = (i: number) => clamp(state.dims[i] ?? 1, 0, 1)
|
|
50
|
+
if (state.orientation === "vertical") {
|
|
51
|
+
return asParts([
|
|
52
|
+
captionColumn({
|
|
53
|
+
count: n,
|
|
54
|
+
axis: () => state.axis,
|
|
55
|
+
text: (i) => state.categories[i] ?? "",
|
|
56
|
+
// y-up: the first category takes the top slot.
|
|
57
|
+
place: (i, height) => -slotCentre(i, state.categories.length, height),
|
|
58
|
+
arrivals: () => state.arrivals,
|
|
59
|
+
opacity: dim,
|
|
60
|
+
label: state.label,
|
|
61
|
+
}),
|
|
62
|
+
])
|
|
86
63
|
}
|
|
87
64
|
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const slots = state.categories.map((caption, i) =>
|
|
93
|
-
new Rect({
|
|
65
|
+
const pads = () => axisPads(state.axis, "top")
|
|
66
|
+
// The spotlight dim sits on a hug box around each caption, so the entrance animates the text's own opacity without the two fighting over one value.
|
|
67
|
+
const captions = state.categories.map((caption, i) => {
|
|
68
|
+
const slot: Node2D = new Rect({
|
|
94
69
|
width: "hug",
|
|
95
70
|
height: "hug",
|
|
96
71
|
flow: "freeform",
|
|
97
|
-
opacity: () =>
|
|
98
|
-
x: () =>
|
|
72
|
+
opacity: () => dim(i),
|
|
73
|
+
x: () => slotCentre(i, state.categories.length, (slot.parent as Node2D | null)?.layoutBounds.width ?? 0),
|
|
99
74
|
children: [
|
|
100
75
|
new Text({
|
|
101
|
-
...
|
|
76
|
+
...boundText(() => state.axis.labelStyle),
|
|
102
77
|
text: caption,
|
|
103
78
|
opacity: () => arrivalOf(state.arrivals, i).opacity,
|
|
104
79
|
y: () => arrivalOf(state.arrivals, i).shift,
|
|
105
80
|
}),
|
|
106
81
|
],
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const children: Node2D[] = [new Rect({ width: "fill", height: "hug", padding: { top:
|
|
82
|
+
}) as unknown as Node2D
|
|
83
|
+
return slot
|
|
84
|
+
})
|
|
85
|
+
const children: Node2D[] = [new Rect({ width: "fill", height: "hug", padding: () => ({ top: pads().tick }), children: captions }) as unknown as Node2D]
|
|
111
86
|
if (state.label !== "") {
|
|
112
87
|
children.push(
|
|
113
88
|
new Text({
|
|
114
|
-
...
|
|
89
|
+
...boundText(() => state.axis.titleStyle),
|
|
115
90
|
text: state.label,
|
|
116
91
|
opacity: () => arrivalOf(state.arrivals, "label").opacity,
|
|
117
92
|
y: () => arrivalOf(state.arrivals, "label").shift,
|
|
118
|
-
})
|
|
93
|
+
}) as unknown as Node2D
|
|
119
94
|
)
|
|
120
95
|
}
|
|
121
|
-
|
|
122
|
-
column.push(new Rect({ flow: "vertical", gap: titlePad, height: "hug", width: "fill", children }))
|
|
123
|
-
return asParts(column)
|
|
96
|
+
return asParts([new Rect({ flow: "vertical", gap: () => pads().title, height: "hug", width: "fill", children }) as unknown as Node2D])
|
|
124
97
|
},
|
|
125
98
|
commands: {
|
|
126
99
|
/**
|
|
127
|
-
* Slide
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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).
|
|
100
|
+
* Slide and fade captions in, first category first, then the title. The
|
|
101
|
+
* duration is the whole entrance ({@link enterBudget}). `stagger` is seconds
|
|
102
|
+
* between captions (derived when omitted); `slide` is the starting offset in px.
|
|
137
103
|
*/
|
|
138
104
|
enter: {
|
|
139
105
|
args: {
|
|
@@ -150,9 +116,8 @@ export const CategoryAxis = component2d({
|
|
|
150
116
|
if (count === 0) return arm
|
|
151
117
|
const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
|
|
152
118
|
const ease = frameEase(timeline)
|
|
153
|
-
const lastCaptionEnds = (count - 1) * step + per
|
|
154
119
|
const captions = state.categories.map((_, i) => ({ at: i * step, duration: per, piece: arrival(timeline, i, from, ease) }))
|
|
155
|
-
const label = captioned ? [{ at:
|
|
120
|
+
const label = captioned ? [{ at: (count - 1) * step + per, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
|
|
156
121
|
return timeline.together([arm, beats(timeline, [...captions, ...label])])
|
|
157
122
|
},
|
|
158
123
|
},
|
|
@@ -2,243 +2,152 @@ import {
|
|
|
2
2
|
Graphics2D,
|
|
3
3
|
Text,
|
|
4
4
|
Types,
|
|
5
|
+
clamp,
|
|
5
6
|
component2d,
|
|
6
7
|
fillOps,
|
|
7
|
-
shadowOps,
|
|
8
|
-
strokeOps,
|
|
9
|
-
textOps,
|
|
10
8
|
type Fill,
|
|
11
9
|
type FillResolved,
|
|
12
10
|
type NodeOf,
|
|
13
|
-
// Named so this component's emitted type reaches `Node2D` through the sdk, not core.
|
|
14
|
-
type Node2D
|
|
15
|
-
type Node2DProps,
|
|
16
|
-
type Shadow,
|
|
17
|
-
type ShadowResolved,
|
|
18
|
-
type Stroke,
|
|
19
|
-
type StrokeResolved,
|
|
11
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
12
|
+
type Node2D,
|
|
13
|
+
type Node2DProps as _Node2DProps,
|
|
20
14
|
type TextStyle,
|
|
21
15
|
} from "@motionscript/sdk"
|
|
22
16
|
|
|
23
|
-
import {
|
|
17
|
+
import { boundText } from "../kit/cartesian"
|
|
18
|
+
import { asParts, fresh } from "../kit/entries"
|
|
24
19
|
import { frameEase } from "../kit/motion"
|
|
25
20
|
import { loadPaints } from "../kit/paints"
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
21
|
+
import { formatMarker } from "../kit/shared"
|
|
22
|
+
import { inkOn } from "./shared"
|
|
28
23
|
|
|
29
24
|
/**
|
|
30
|
-
* One drawn
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* grouped bar, a stacked segment and a negative value without a special case.
|
|
25
|
+
* One drawn bar in the plot's centred pixel space (origin at the centre, y-up),
|
|
26
|
+
* mapped by the parent {@link BarChart}: `center` and `thickness` across the
|
|
27
|
+
* category slot, `from` and `to` along the value axis (y for a column, x for a
|
|
28
|
+
* horizontal bar). Two ends rather than a base and a length, like `BarSpan`:
|
|
29
|
+
* one shape fits grouped, stacked and negative.
|
|
36
30
|
*/
|
|
37
31
|
export interface BarGeometry {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
width: number
|
|
42
|
-
/** Pixel y of the column's base — the baseline when grouped, the running stack top when stacked. */
|
|
32
|
+
center: number
|
|
33
|
+
thickness: number
|
|
34
|
+
/** The base: the baseline when grouped, the running stack end when stacked. */
|
|
43
35
|
from: number
|
|
44
|
-
/**
|
|
36
|
+
/** The far end at full growth. */
|
|
45
37
|
to: number
|
|
46
|
-
/** Whether the far end takes the
|
|
38
|
+
/** Whether the far end takes the corner radius (the stack's last segment on its side). */
|
|
47
39
|
round: boolean
|
|
48
|
-
/** The
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The figure printed on it, written as the chart's value axis writes its
|
|
52
|
-
* ticks, so the two cannot disagree. Absent, the bare marker reading of
|
|
53
|
-
* {@link value}.
|
|
54
|
-
*/
|
|
55
|
-
caption?: string
|
|
40
|
+
/** The figure printed by the bar; empty for none. */
|
|
41
|
+
caption: string
|
|
56
42
|
}
|
|
57
43
|
|
|
58
|
-
/**
|
|
59
|
-
const
|
|
60
|
-
/** A segment shorter than this many multiples of the type size prints no figure. */
|
|
61
|
-
const VALUE_FIT = 1.6
|
|
62
|
-
/** Fallback type size when the theme's value style leaves it unset or autofit. */
|
|
63
|
-
const VALUE_SIZE = 24
|
|
44
|
+
/** A figure inside a bar needs this much room around it, in multiples of the type size. */
|
|
45
|
+
const INSIDE_ROOM = 0.6
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
/** This series' columns, one per row of the chart's data. */
|
|
67
|
-
bars: BarGeometry[]
|
|
68
|
-
/** Pixel y of value 0 — where every column grows *from*. */
|
|
69
|
-
baseline: number
|
|
70
|
-
/** Column paint — this series' own, or the theme palette's entry for its index. */
|
|
71
|
-
fill: Fill
|
|
72
|
-
/** Optional outline on every column. */
|
|
73
|
-
barStroke: Stroke
|
|
74
|
-
/** Optional drop shadow on every column. */
|
|
75
|
-
barShadow: Shadow
|
|
76
|
-
/** Radius of the far-end corners, in px. */
|
|
77
|
-
cornerRadius: number
|
|
78
|
-
/**
|
|
79
|
-
* Rise of every column in `[0, 1]`, measured **about the baseline**: 0
|
|
80
|
-
* collapses them all onto it, 1 draws them at full length.
|
|
81
|
-
*
|
|
82
|
-
* Scaling about the baseline rather than about each column's own base is what
|
|
83
|
-
* keeps a stack flush mid-animation — every segment's base and end travel by
|
|
84
|
-
* the same factor, so the segments stay stacked instead of the upper ones
|
|
85
|
-
* floating above the still-growing lower ones. It also means a negative
|
|
86
|
-
* column grows *down* out of the same signal.
|
|
87
|
-
*/
|
|
88
|
-
growth: number
|
|
89
|
-
/**
|
|
90
|
-
* Per-column opacity multiplier, by row index — how the chart's category
|
|
91
|
-
* spotlight reaches a single column without touching the series it belongs
|
|
92
|
-
* to. Missing entries read as 1.
|
|
93
|
-
*/
|
|
94
|
-
dims: number[]
|
|
95
|
-
/** Whether the figure each column plots is printed on it. */
|
|
96
|
-
showValues: boolean
|
|
97
|
-
/** Type style for those figures. */
|
|
98
|
-
valueStyle: TextStyle
|
|
99
|
-
/**
|
|
100
|
-
* Whether this series draws at all — the wholesale switch behind
|
|
101
|
-
* `BarChart.showSeries`/`hideSeries`, alongside the slot weight that animates
|
|
102
|
-
* the reflow. Off skips the columns *and* their figures.
|
|
103
|
-
*/
|
|
104
|
-
enabled: boolean
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** What the column geometry reads. */
|
|
108
|
-
interface Columns {
|
|
47
|
+
interface Bars {
|
|
109
48
|
readonly bars: readonly BarGeometry[]
|
|
110
49
|
readonly baseline: number
|
|
50
|
+
readonly horizontal: boolean
|
|
111
51
|
readonly growth: number
|
|
112
52
|
readonly dims: readonly number[]
|
|
113
53
|
readonly enabled: boolean
|
|
54
|
+
readonly values: "none" | "end" | "inside"
|
|
114
55
|
readonly valueStyle: TextStyle
|
|
115
56
|
}
|
|
116
57
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
* switch on top of it.
|
|
120
|
-
*
|
|
121
|
-
* The captions read this rather than being hidden by `render`'s guard: they are
|
|
122
|
-
* real child nodes, so a `return` in the draw would leave them on screen over a
|
|
123
|
-
* series that isn't there.
|
|
124
|
-
*/
|
|
125
|
-
function dimAt(s: Columns, index: number): number {
|
|
58
|
+
/** Bar `i`'s dimming: the category spotlight, then the series switch. */
|
|
59
|
+
function dimAt(s: Bars, index: number): number {
|
|
126
60
|
if (!s.enabled) return 0
|
|
127
|
-
return
|
|
61
|
+
return clamp(s.dims[index] ?? 1, 0, 1)
|
|
128
62
|
}
|
|
129
63
|
|
|
130
|
-
/**
|
|
131
|
-
function endsAt(s:
|
|
64
|
+
/** Bar `i`'s two ends as grown so far, along the value axis. */
|
|
65
|
+
function endsAt(s: Bars, index: number): { base: number; end: number } | null {
|
|
132
66
|
const bar = s.bars[index]
|
|
133
67
|
if (!bar) return null
|
|
134
|
-
const g =
|
|
68
|
+
const g = clamp(s.growth, 0, 1)
|
|
135
69
|
return { base: s.baseline + (bar.from - s.baseline) * g, end: s.baseline + (bar.to - s.baseline) * g }
|
|
136
70
|
}
|
|
137
71
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const size = s.valueStyle.fontSize
|
|
141
|
-
return typeof size === "number" ? size : VALUE_SIZE
|
|
72
|
+
function sizeOf(style: TextStyle): number {
|
|
73
|
+
return typeof style.fontSize === "number" ? style.fontSize : 24
|
|
142
74
|
}
|
|
143
75
|
|
|
144
|
-
/**
|
|
145
|
-
function
|
|
146
|
-
const
|
|
147
|
-
|
|
76
|
+
/** Where bar `i`'s figure sits along the value axis: past its end, or in its middle. */
|
|
77
|
+
function captionAlong(s: Bars, index: number, extent: number): number {
|
|
78
|
+
const ends = endsAt(s, index)
|
|
79
|
+
if (!ends) return 0
|
|
80
|
+
if (s.values === "inside") return (ends.base + ends.end) / 2
|
|
81
|
+
const direction = ends.end >= ends.base ? 1 : -1
|
|
82
|
+
return ends.end + direction * (sizeOf(s.valueStyle) * 0.4 + extent / 2)
|
|
148
83
|
}
|
|
149
84
|
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
* Blanking the text rather than dropping the node keeps the caption list
|
|
154
|
-
* index-aligned with the columns, so a chart that rescales into more room simply
|
|
155
|
-
* starts printing again.
|
|
156
|
-
*/
|
|
157
|
-
function captionAt(s: Columns, index: number): string {
|
|
85
|
+
/** Whether bar `i` has room for a figure `extent` long inside it. */
|
|
86
|
+
function fitsInside(s: Bars, index: number, extent: number, across: number): boolean {
|
|
158
87
|
const bar = s.bars[index]
|
|
159
|
-
if (!bar || bar.value === 0) return ""
|
|
160
|
-
// An interior segment prints inside itself, so it has to be tall enough to
|
|
161
|
-
// hold a line of type; a grouped column prints in the headroom above its end,
|
|
162
|
-
// where the only constraint is the plot's own top.
|
|
163
|
-
if (insideAt(s, index)) {
|
|
164
|
-
const ends = endsAt(s, index)
|
|
165
|
-
if (!ends) return ""
|
|
166
|
-
if (Math.abs(ends.end - ends.base) < valueSize(s) * VALUE_FIT) return ""
|
|
167
|
-
}
|
|
168
|
-
return bar.caption ?? formatMarker(bar.value)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Where column `i`'s figure sits, in the node's centred y-up space. */
|
|
172
|
-
function captionY(s: Columns, index: number): number {
|
|
173
88
|
const ends = endsAt(s, index)
|
|
174
|
-
if (!ends) return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// line — on whichever side of the baseline the column actually reached.
|
|
178
|
-
const direction = ends.end >= ends.base ? 1 : -1
|
|
179
|
-
return ends.end + direction * (VALUE_GAP + valueSize(s) / 2)
|
|
89
|
+
if (!bar || !ends) return false
|
|
90
|
+
const room = sizeOf(s.valueStyle) * INSIDE_ROOM
|
|
91
|
+
return Math.abs(ends.end - ends.base) >= extent + room && bar.thickness >= across
|
|
180
92
|
}
|
|
181
93
|
|
|
182
94
|
/**
|
|
183
|
-
* One
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* unit `ChartLine` is, which is what lets `showSeries`/`hideSeries`/
|
|
188
|
-
* `spotlightSeries` mean exactly the same thing on a bar chart as on a line
|
|
189
|
-
* chart: enable the node, or tween its opacity. The columns' *positions* come
|
|
190
|
-
* from the parent (it owns the slot arithmetic both modes share), so this part
|
|
191
|
-
* only has to know how to draw a span and grow it.
|
|
192
|
-
*
|
|
193
|
-
* The figures are real {@link Text} children rather than glyphs in the command
|
|
194
|
-
* list, so they take the theme's type style, are measured by the layout engine,
|
|
195
|
-
* and ride the node's own opacity — which is why hiding the series takes its
|
|
196
|
-
* figures with it for free.
|
|
95
|
+
* One series of a {@link BarChart}: its bar in every category plus the printed
|
|
96
|
+
* figures. A node per series, like `ChartLine`, so `showSeries` and
|
|
97
|
+
* `spotlightSeries` mean the same on both: enable the node or tween its opacity.
|
|
98
|
+
* Positions come from the parent, which owns the slot arithmetic.
|
|
197
99
|
*/
|
|
198
100
|
export const ChartBars = component2d({
|
|
199
101
|
key: "chartBars",
|
|
200
102
|
state: {
|
|
201
103
|
bars: fresh<BarGeometry[]>(() => []),
|
|
104
|
+
/** Pixel position of value 0 along the value axis, where every bar grows from. */
|
|
202
105
|
baseline: 0,
|
|
106
|
+
/** Bars grow along x from a category column rather than along y from a row. */
|
|
107
|
+
horizontal: false,
|
|
203
108
|
fill: Types.custom<Fill, FillResolved[]>([], { resolve: fillOps.resolve, lerp: fillOps.lerp }),
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
109
|
+
cornerRadius: 8,
|
|
110
|
+
/**
|
|
111
|
+
* Rise of every bar in `[0, 1]` about the baseline, not each bar's own base:
|
|
112
|
+
* every segment travels by the same factor, so a stack stays flush
|
|
113
|
+
* mid-animation and a negative bar grows the other way.
|
|
114
|
+
*/
|
|
207
115
|
growth: 1,
|
|
116
|
+
/** Per-bar opacity by row index, how the category spotlight reaches one bar. Missing reads as 1. */
|
|
208
117
|
dims: fresh<number[]>(() => []),
|
|
209
|
-
|
|
210
|
-
|
|
118
|
+
/** Read as built: whether each bar prints its figure, and where. */
|
|
119
|
+
values: Types.custom<"none" | "end" | "inside">("none"),
|
|
120
|
+
valueStyle: Types.custom<TextStyle>({ fontSize: 24, fill: "white" }),
|
|
121
|
+
/** The wholesale switch behind `showSeries`/`hideSeries`; off skips the bars and their figures. */
|
|
211
122
|
enabled: true,
|
|
212
123
|
},
|
|
213
|
-
// A
|
|
124
|
+
// A node made of its figures would otherwise hug them.
|
|
214
125
|
defaults: { width: "fill", height: "fill" },
|
|
215
|
-
// One
|
|
216
|
-
//
|
|
217
|
-
// with its column as the chart grows, rescales or reflows.
|
|
126
|
+
// One figure per bar, built once (the row count is fixed at construction)
|
|
127
|
+
// and bound to the live geometry so it travels with its bar.
|
|
218
128
|
children: (state) => {
|
|
219
|
-
if (
|
|
220
|
-
return state.bars.map((_, i) =>
|
|
221
|
-
Text({
|
|
222
|
-
...
|
|
223
|
-
text: () =>
|
|
224
|
-
x: () => state.bars[i]?.
|
|
225
|
-
y: () =>
|
|
226
|
-
opacity: () =>
|
|
227
|
-
|
|
228
|
-
|
|
129
|
+
if (state.values === "none") return []
|
|
130
|
+
return asParts(state.bars.map((_, i) => {
|
|
131
|
+
const caption: Node2D = new Text({
|
|
132
|
+
...boundText(() => ({ ...state.valueStyle, ...(state.values === "inside" && { fill: inkOn(state.fill) }) })),
|
|
133
|
+
text: () => state.bars[i]?.caption ?? "",
|
|
134
|
+
x: () => (state.horizontal ? captionAlong(state, i, caption.layoutBounds.width) : (state.bars[i]?.center ?? 0)),
|
|
135
|
+
y: () => (state.horizontal ? (state.bars[i]?.center ?? 0) : captionAlong(state, i, caption.layoutBounds.height)),
|
|
136
|
+
opacity: () => {
|
|
137
|
+
const box = caption.layoutBounds
|
|
138
|
+
const along = state.horizontal ? box.width : box.height
|
|
139
|
+
const across = state.horizontal ? box.height : box.width
|
|
140
|
+
if (state.values === "inside" && !fitsInside(state, i, along, across)) return 0
|
|
141
|
+
// A figure past the end rises in with its bar's last stretch.
|
|
142
|
+
return dimAt(state, i) * clamp((state.growth - 0.6) / 0.4, 0, 1)
|
|
143
|
+
},
|
|
144
|
+
}) as unknown as Node2D
|
|
145
|
+
return caption
|
|
146
|
+
}))
|
|
229
147
|
},
|
|
230
|
-
|
|
231
|
-
load: (loader, state) =>
|
|
232
|
-
loadPaints(loader, { fills: [state.fill], shadows: [state.barShadow], strokes: [state.barStroke] }),
|
|
148
|
+
load: (loader, state) => loadPaints(loader, { fills: [state.fill] }),
|
|
233
149
|
commands: {
|
|
234
|
-
/**
|
|
235
|
-
* Raise every column from the baseline by tweening `growth` `0 → 1`.
|
|
236
|
-
*
|
|
237
|
-
* The mirror of `ChartLine.enter`: same contract that the resting state is
|
|
238
|
-
* the finished one, so the chart arms this back to 0 before animating rather
|
|
239
|
-
* than the node hiding itself at construction. No priming to 0 first: the
|
|
240
|
-
* value at `t` is a function of `t`, so a repeat call re-raises by definition.
|
|
241
|
-
*/
|
|
150
|
+
/** Raise every bar from the baseline (`growth` 0 to 1). The resting state is the finished one; the chart arms it back to 0. */
|
|
242
151
|
enter: (timeline) => {
|
|
243
152
|
const ease = frameEase(timeline)
|
|
244
153
|
return timeline.animate((t) => ({ growth: ease(t) }))
|
|
@@ -249,34 +158,23 @@ export const ChartBars = component2d({
|
|
|
249
158
|
for (let i = 0; i < state.bars.length; i++) {
|
|
250
159
|
const bar = state.bars[i]
|
|
251
160
|
const ends = endsAt(state, i)
|
|
252
|
-
if (!ends || bar.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// a stack read as a single capped column.
|
|
270
|
-
cornerRadius:
|
|
271
|
-
radius > 0
|
|
272
|
-
? ends.end >= ends.base
|
|
273
|
-
? { topLeft: radius, topRight: radius, bottomLeft: 0, bottomRight: 0 }
|
|
274
|
-
: { bottomLeft: radius, bottomRight: radius, topLeft: 0, topRight: 0 }
|
|
275
|
-
: 0,
|
|
276
|
-
})
|
|
277
|
-
rect.shadow(state.barShadow)
|
|
278
|
-
rect.fill(state.fill)
|
|
279
|
-
rect.stroke(state.barStroke)
|
|
161
|
+
if (!ends || bar.thickness <= 0) continue
|
|
162
|
+
// A segment inside a stack reaches a pixel under the next, so no seam of background shows between them.
|
|
163
|
+
const seam = bar.round ? 0 : 1
|
|
164
|
+
const length = Math.abs(ends.end - ends.base) + seam
|
|
165
|
+
if (length <= seam) continue
|
|
166
|
+
const radius = bar.round ? Math.max(0, Math.min(state.cornerRadius, length, bar.thickness / 2)) : 0
|
|
167
|
+
const forward = ends.end >= ends.base
|
|
168
|
+
const mid = (ends.base + ends.end) / 2 + (forward ? seam : -seam) / 2
|
|
169
|
+
// Corner keys are visual (y-up): only the far end rounds.
|
|
170
|
+
const far = state.horizontal
|
|
171
|
+
? (forward ? { topRight: radius, bottomRight: radius, topLeft: 0, bottomLeft: 0 } : { topLeft: radius, bottomLeft: radius, topRight: 0, bottomRight: 0 })
|
|
172
|
+
: (forward ? { topLeft: radius, topRight: radius, bottomLeft: 0, bottomRight: 0 } : { bottomLeft: radius, bottomRight: radius, topLeft: 0, topRight: 0 })
|
|
173
|
+
// One `Graphics2D` per bar: opacity belongs to the command list, and the spotlight dims single bars.
|
|
174
|
+
const g = new Graphics2D().rect(state.horizontal
|
|
175
|
+
? { x: mid, y: bar.center, width: length, height: bar.thickness, cornerRadius: radius > 0 ? far : 0 }
|
|
176
|
+
: { x: bar.center, y: mid, width: bar.thickness, height: length, cornerRadius: radius > 0 ? far : 0 })
|
|
177
|
+
g.fill(state.fill)
|
|
280
178
|
ctx.draw(g.opacity(dimAt(state, i)))
|
|
281
179
|
}
|
|
282
180
|
}
|
|
@@ -285,3 +183,8 @@ export const ChartBars = component2d({
|
|
|
285
183
|
})
|
|
286
184
|
|
|
287
185
|
export type ChartBars = NodeOf<typeof ChartBars>
|
|
186
|
+
|
|
187
|
+
/** A figure as the bar prints it when the chart gives no caption. */
|
|
188
|
+
export function barCaption(value: number): string {
|
|
189
|
+
return value === 0 ? "" : formatMarker(value)
|
|
190
|
+
}
|
package/src/bar-chart/index.ts
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The bar chart: grouped or stacked columns off a category axis.
|
|
3
|
-
*
|
|
4
|
-
* Shares the plot panel, the value axis and the legend with the line chart —
|
|
5
|
-
* which is what lets a scene swap one for the other without re-theming — and
|
|
6
|
-
* brings its own category axis, since a band scale is not a continuous one.
|
|
7
|
-
*/
|
|
8
1
|
export { BarChart, barChartDomain } from "./bar-chart"
|
|
9
|
-
export type { BarChartDomain,
|
|
2
|
+
export type { BarChartDomain, BarChartStyle, BarChartValues } from "./bar-chart"
|
|
10
3
|
export { ChartBars } from "./chart-bars"
|
|
11
|
-
export type { BarGeometry
|
|
12
|
-
export { CategoryAxis } from "./category-axis"
|
|
13
|
-
export type { CategoryAxisProps } from "./category-axis"
|
|
4
|
+
export type { BarGeometry } from "./chart-bars"
|
|
5
|
+
export { CategoryAxis, slotCentre } from "./category-axis"
|
|
14
6
|
export {
|
|
15
|
-
|
|
7
|
+
BAR_LABELS,
|
|
8
|
+
BAR_MARKS,
|
|
16
9
|
barCategories,
|
|
17
10
|
barSpans,
|
|
18
11
|
barValueExtent,
|
|
19
|
-
|
|
20
|
-
resolveBarChartTheme,
|
|
21
|
-
resolveBarFill,
|
|
12
|
+
inkOn,
|
|
22
13
|
resolveBarScale,
|
|
23
|
-
stackCapIndex,
|
|
24
14
|
} from "./shared"
|
|
25
15
|
export type {
|
|
26
|
-
|
|
27
|
-
BarChartTheme,
|
|
16
|
+
BarOrientation,
|
|
28
17
|
BarSeries,
|
|
29
18
|
BarSpan,
|
|
30
|
-
|
|
19
|
+
BarStack,
|
|
31
20
|
} from "./shared"
|