@motionscript/charts 0.1.1-alpha.0 → 0.1.1-alpha.2
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 +34 -0
- package/README.md +109 -28
- package/dist/bar-chart/bar-chart.d.ts +265 -39
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +219 -171
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +12 -7
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +56 -42
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +76 -52
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +105 -89
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +4 -4
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +2 -2
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +41 -65
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +73 -117
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-7XCRX6GE.js +2 -0
- package/dist/browser/chunks/chunk-7XCRX6GE.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/engine.d.ts +5 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +5 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/kit/cartesian.d.ts +51 -15
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +81 -29
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +32 -24
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +52 -46
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +8 -4
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +30 -20
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/{line-chart → kit}/horizontal-axis.d.ts +31 -6
- package/dist/kit/horizontal-axis.d.ts.map +1 -0
- package/dist/{line-chart → kit}/horizontal-axis.js +23 -24
- package/dist/kit/horizontal-axis.js.map +1 -0
- package/dist/kit/motion.d.ts +5 -0
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +8 -0
- package/dist/kit/motion.js.map +1 -1
- package/dist/{pie-chart → kit}/pie-legend.d.ts +3 -3
- package/dist/kit/pie-legend.d.ts.map +1 -0
- package/dist/{pie-chart → kit}/pie-legend.js +11 -10
- package/dist/kit/pie-legend.js.map +1 -0
- package/dist/kit/polar.d.ts +31 -0
- package/dist/kit/polar.d.ts.map +1 -0
- package/dist/kit/polar.js +33 -0
- package/dist/kit/polar.js.map +1 -0
- package/dist/kit/shared.d.ts +11 -50
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +58 -141
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/style.d.ts +517 -0
- package/dist/kit/style.d.ts.map +1 -0
- package/dist/kit/style.js +292 -0
- package/dist/kit/style.js.map +1 -0
- package/dist/kit/vertical-axis.d.ts +22 -2
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +56 -43
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +95 -3
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +165 -53
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +1 -1
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +7 -8
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/index.d.ts +5 -6
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +3 -4
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +281 -18
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +284 -134
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +51 -34
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +175 -83
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +2 -0
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +15 -0
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +3 -1
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +9 -1
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +2 -3
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +1 -2
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +114 -18
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +38 -24
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +16 -5
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +89 -55
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +61 -62
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +44 -123
- 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 +212 -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 +49 -0
- package/dist/radar-chart/radar-grid.d.ts.map +1 -0
- package/dist/radar-chart/radar-grid.js +63 -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 +78 -0
- package/dist/radar-chart/shared.d.ts.map +1 -0
- package/dist/radar-chart/shared.js +129 -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 +210 -0
- package/dist/radial-chart/radial-chart.d.ts.map +1 -0
- package/dist/radial-chart/radial-chart.js +175 -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 +77 -0
- package/dist/radial-chart/shared.d.ts.map +1 -0
- package/dist/radial-chart/shared.js +78 -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 +263 -179
- package/src/bar-chart/category-axis.ts +46 -40
- package/src/bar-chart/chart-bars.ts +136 -117
- package/src/bar-chart/index.ts +7 -10
- package/src/bar-chart/shared.ts +88 -233
- package/src/engine.ts +5 -2
- package/src/index.ts +2 -0
- package/src/kit/cartesian.ts +121 -40
- package/src/kit/chart-canvas.ts +49 -54
- package/src/kit/chart-legend.ts +28 -20
- package/src/kit/horizontal-axis.ts +114 -0
- package/src/kit/motion.ts +7 -0
- package/src/{pie-chart → kit}/pie-legend.ts +12 -11
- package/src/kit/polar.ts +54 -0
- package/src/kit/shared.ts +55 -277
- package/src/kit/style.ts +353 -0
- package/src/kit/vertical-axis.ts +67 -48
- package/src/line-chart/chart-line.ts +176 -54
- package/src/line-chart/chart-region.ts +6 -10
- package/src/line-chart/index.ts +14 -10
- package/src/line-chart/line-chart.ts +353 -151
- package/src/line-chart/shared.ts +193 -174
- package/src/line-chart/x-scale.ts +13 -0
- package/src/nodes.ts +9 -1
- package/src/pie-chart/index.ts +2 -23
- package/src/pie-chart/pie-chart.ts +54 -35
- package/src/pie-chart/pie-ring.ts +92 -71
- package/src/pie-chart/shared.ts +80 -270
- package/src/radar-chart/index.ts +4 -0
- package/src/radar-chart/radar-chart.ts +290 -0
- package/src/radar-chart/radar-grid.ts +83 -0
- package/src/radar-chart/radar-series.ts +88 -0
- package/src/radar-chart/shared.ts +153 -0
- package/src/radial-chart/index.ts +5 -0
- package/src/radial-chart/radial-chart.ts +212 -0
- package/src/radial-chart/radial-rings.ts +328 -0
- package/src/radial-chart/shared.ts +126 -0
- package/dist/browser/chunks/chunk-4SMRCU24.js +0 -2
- package/dist/browser/chunks/chunk-4SMRCU24.js.map +0 -7
- package/dist/kit/chart-border.d.ts +0 -18
- package/dist/kit/chart-border.d.ts.map +0 -1
- package/dist/kit/chart-border.js +0 -23
- package/dist/kit/chart-border.js.map +0 -1
- package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
- package/dist/line-chart/horizontal-axis.js.map +0 -1
- package/dist/pie-chart/pie-legend.d.ts.map +0 -1
- package/dist/pie-chart/pie-legend.js.map +0 -1
- package/src/kit/chart-border.ts +0 -36
- package/src/line-chart/horizontal-axis.ts +0 -122
|
@@ -4,8 +4,6 @@ import {
|
|
|
4
4
|
Types,
|
|
5
5
|
clamp,
|
|
6
6
|
component2d,
|
|
7
|
-
insetsOps,
|
|
8
|
-
textOps,
|
|
9
7
|
type NodeOf,
|
|
10
8
|
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
11
9
|
type Node2D,
|
|
@@ -13,21 +11,25 @@ import {
|
|
|
13
11
|
} from "@motionscript/sdk"
|
|
14
12
|
import { enterBudget } from "@motionscript/sdk/component"
|
|
15
13
|
|
|
14
|
+
import { boundText } from "../kit/cartesian"
|
|
16
15
|
import { asParts, fresh } from "../kit/entries"
|
|
17
16
|
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
type AxisConfig,
|
|
21
|
-
} from "../kit/shared"
|
|
17
|
+
import { DEFAULT_X_AXIS, type AxisConfig } from "../kit/shared"
|
|
18
|
+
import { axisPads, captionColumn } from "../kit/vertical-axis"
|
|
22
19
|
|
|
23
20
|
const SLIDE = 24
|
|
24
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
|
+
|
|
25
27
|
/**
|
|
26
28
|
* The categorical axis of a {@link BarChart}: one {@link Text} caption per row,
|
|
27
|
-
* centred on its slot.
|
|
28
|
-
*
|
|
29
|
-
* exactly the plot's
|
|
30
|
-
* side told about the other. The resting state is landed.
|
|
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.
|
|
31
33
|
*/
|
|
32
34
|
export const CategoryAxis = component2d({
|
|
33
35
|
key: "categoryAxis",
|
|
@@ -35,64 +37,69 @@ export const CategoryAxis = component2d({
|
|
|
35
37
|
categories: fresh<string[]>(() => []),
|
|
36
38
|
axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
|
|
37
39
|
label: "",
|
|
38
|
-
|
|
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. */
|
|
39
42
|
dims: fresh<number[]>(() => []),
|
|
40
43
|
arrivals: arrivalsEntry(),
|
|
41
44
|
},
|
|
42
45
|
// A node made of captions would otherwise hug them.
|
|
43
46
|
defaults: { width: "fill", height: "fill" },
|
|
44
47
|
children: (state) => {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
])
|
|
55
63
|
}
|
|
56
64
|
|
|
65
|
+
const pads = () => axisPads(state.axis, "top")
|
|
57
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.
|
|
58
|
-
const
|
|
59
|
-
new Rect({
|
|
67
|
+
const captions = state.categories.map((caption, i) => {
|
|
68
|
+
const slot: Node2D = new Rect({
|
|
60
69
|
width: "hug",
|
|
61
70
|
height: "hug",
|
|
62
71
|
flow: "freeform",
|
|
63
|
-
opacity: () =>
|
|
64
|
-
x: () =>
|
|
72
|
+
opacity: () => dim(i),
|
|
73
|
+
x: () => slotCentre(i, state.categories.length, (slot.parent as Node2D | null)?.layoutBounds.width ?? 0),
|
|
65
74
|
children: [
|
|
66
75
|
new Text({
|
|
67
|
-
...
|
|
76
|
+
...boundText(() => state.axis.labelStyle),
|
|
68
77
|
text: caption,
|
|
69
78
|
opacity: () => arrivalOf(state.arrivals, i).opacity,
|
|
70
79
|
y: () => arrivalOf(state.arrivals, i).shift,
|
|
71
80
|
}),
|
|
72
81
|
],
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
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]
|
|
77
86
|
if (state.label !== "") {
|
|
78
87
|
children.push(
|
|
79
88
|
new Text({
|
|
80
|
-
...
|
|
89
|
+
...boundText(() => state.axis.titleStyle),
|
|
81
90
|
text: state.label,
|
|
82
91
|
opacity: () => arrivalOf(state.arrivals, "label").opacity,
|
|
83
92
|
y: () => arrivalOf(state.arrivals, "label").shift,
|
|
84
|
-
})
|
|
93
|
+
}) as unknown as Node2D
|
|
85
94
|
)
|
|
86
95
|
}
|
|
87
|
-
|
|
88
|
-
column.push(new Rect({ flow: "vertical", gap: titlePad, height: "hug", width: "fill", children }))
|
|
89
|
-
return asParts(column)
|
|
96
|
+
return asParts([new Rect({ flow: "vertical", gap: () => pads().title, height: "hug", width: "fill", children }) as unknown as Node2D])
|
|
90
97
|
},
|
|
91
98
|
commands: {
|
|
92
99
|
/**
|
|
93
|
-
* Slide and fade captions in
|
|
94
|
-
* the whole entrance ({@link enterBudget}). `stagger` is seconds
|
|
95
|
-
* captions (derived when omitted); `slide` is the starting offset in px.
|
|
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.
|
|
96
103
|
*/
|
|
97
104
|
enter: {
|
|
98
105
|
args: {
|
|
@@ -109,9 +116,8 @@ export const CategoryAxis = component2d({
|
|
|
109
116
|
if (count === 0) return arm
|
|
110
117
|
const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
|
|
111
118
|
const ease = frameEase(timeline)
|
|
112
|
-
const lastCaptionEnds = (count - 1) * step + per
|
|
113
119
|
const captions = state.categories.map((_, i) => ({ at: i * step, duration: per, piece: arrival(timeline, i, from, ease) }))
|
|
114
|
-
const label = captioned ? [{ at:
|
|
120
|
+
const label = captioned ? [{ at: (count - 1) * step + per, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
|
|
115
121
|
return timeline.together([arm, beats(timeline, [...captions, ...label])])
|
|
116
122
|
},
|
|
117
123
|
},
|
|
@@ -5,168 +5,157 @@ import {
|
|
|
5
5
|
clamp,
|
|
6
6
|
component2d,
|
|
7
7
|
fillOps,
|
|
8
|
-
shadowOps,
|
|
9
|
-
strokeOps,
|
|
10
|
-
textOps,
|
|
11
8
|
type Fill,
|
|
12
9
|
type FillResolved,
|
|
13
10
|
type NodeOf,
|
|
14
11
|
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
15
|
-
type Node2D
|
|
12
|
+
type Node2D,
|
|
16
13
|
type Node2DProps as _Node2DProps,
|
|
17
|
-
type
|
|
18
|
-
type
|
|
19
|
-
type Stroke,
|
|
20
|
-
type StrokeResolved,
|
|
14
|
+
type RectCornerRadius as _RectCornerRadius,
|
|
15
|
+
type RectCornerStyle as _RectCornerStyle,
|
|
21
16
|
type TextStyle,
|
|
17
|
+
type CornerRadiusResolved,
|
|
18
|
+
type CornerStyleResolved,
|
|
22
19
|
} from "@motionscript/sdk"
|
|
23
20
|
|
|
24
|
-
import {
|
|
21
|
+
import { boundText } from "../kit/cartesian"
|
|
22
|
+
import { asParts, fresh } from "../kit/entries"
|
|
25
23
|
import { frameEase } from "../kit/motion"
|
|
26
24
|
import { loadPaints } from "../kit/paints"
|
|
27
25
|
import { formatMarker } from "../kit/shared"
|
|
28
|
-
import {
|
|
26
|
+
import { inkOn } from "./shared"
|
|
29
27
|
|
|
30
28
|
/**
|
|
31
|
-
* One drawn
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
* One drawn bar in the plot's centred pixel space (origin at the centre, y-up),
|
|
30
|
+
* mapped by the parent {@link BarChart}: `center` and `thickness` across the
|
|
31
|
+
* category slot, `from` and `to` along the value axis (y for a column, x for a
|
|
32
|
+
* horizontal bar). Two ends rather than a base and a length, like `BarSpan`:
|
|
33
|
+
* one shape fits grouped, stacked and negative.
|
|
35
34
|
*/
|
|
36
35
|
export interface BarGeometry {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/** Pixel y of the base: the baseline when grouped, the running stack top when stacked. */
|
|
36
|
+
center: number
|
|
37
|
+
thickness: number
|
|
38
|
+
/** The base: the baseline when grouped, the running stack end when stacked. */
|
|
41
39
|
from: number
|
|
42
|
-
/**
|
|
40
|
+
/** The far end at full growth. */
|
|
43
41
|
to: number
|
|
44
|
-
/** Whether the far end takes the
|
|
42
|
+
/** Whether the far end takes the far corners (the stack's last segment on its side). */
|
|
45
43
|
round: boolean
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
/** The
|
|
49
|
-
caption
|
|
44
|
+
/** Whether the base takes the base corners (it stands on the baseline: every grouped bar, a stack's first segment). */
|
|
45
|
+
grounded?: boolean
|
|
46
|
+
/** The figure printed by the bar; empty for none. */
|
|
47
|
+
caption: string
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
/**
|
|
53
|
-
const
|
|
54
|
-
/** A segment shorter than this many multiples of the type size prints no figure. */
|
|
55
|
-
const VALUE_FIT = 1.6
|
|
56
|
-
/** Fallback type size when the theme's value style leaves it unset or autofit. */
|
|
57
|
-
const VALUE_SIZE = 24
|
|
50
|
+
/** A figure inside a bar needs this much room around it, in multiples of the type size. */
|
|
51
|
+
const INSIDE_ROOM = 0.6
|
|
58
52
|
|
|
59
|
-
interface
|
|
53
|
+
interface Bars {
|
|
60
54
|
readonly bars: readonly BarGeometry[]
|
|
61
55
|
readonly baseline: number
|
|
56
|
+
readonly horizontal: boolean
|
|
62
57
|
readonly growth: number
|
|
63
58
|
readonly dims: readonly number[]
|
|
64
59
|
readonly enabled: boolean
|
|
60
|
+
readonly values: "none" | "end" | "inside"
|
|
65
61
|
readonly valueStyle: TextStyle
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
* read this because they are child nodes, which a `render` guard would leave on
|
|
71
|
-
* screen over a series that is not there.
|
|
72
|
-
*/
|
|
73
|
-
function dimAt(s: Columns, index: number): number {
|
|
64
|
+
/** Bar `i`'s dimming: the category spotlight, then the series switch. */
|
|
65
|
+
function dimAt(s: Bars, index: number): number {
|
|
74
66
|
if (!s.enabled) return 0
|
|
75
67
|
return clamp(s.dims[index] ?? 1, 0, 1)
|
|
76
68
|
}
|
|
77
69
|
|
|
78
|
-
|
|
70
|
+
/** Bar `i`'s two ends as grown so far, along the value axis. */
|
|
71
|
+
function endsAt(s: Bars, index: number): { base: number; end: number } | null {
|
|
79
72
|
const bar = s.bars[index]
|
|
80
73
|
if (!bar) return null
|
|
81
74
|
const g = clamp(s.growth, 0, 1)
|
|
82
75
|
return { base: s.baseline + (bar.from - s.baseline) * g, end: s.baseline + (bar.to - s.baseline) * g }
|
|
83
76
|
}
|
|
84
77
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
return typeof size === "number" ? size : VALUE_SIZE
|
|
78
|
+
function sizeOf(style: TextStyle): number {
|
|
79
|
+
return typeof style.fontSize === "number" ? style.fontSize : 24
|
|
88
80
|
}
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
/** Where bar `i`'s figure sits along the value axis: past its end, or in its middle. */
|
|
83
|
+
function captionAlong(s: Bars, index: number, extent: number): number {
|
|
84
|
+
const ends = endsAt(s, index)
|
|
85
|
+
if (!ends) return 0
|
|
86
|
+
if (s.values === "inside") return (ends.base + ends.end) / 2
|
|
87
|
+
const direction = ends.end >= ends.base ? 1 : -1
|
|
88
|
+
return ends.end + direction * (sizeOf(s.valueStyle) * 0.4 + extent / 2)
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
/**
|
|
96
|
-
function
|
|
91
|
+
/** Whether bar `i` has room for a figure `extent` long inside it. */
|
|
92
|
+
function fitsInside(s: Bars, index: number, extent: number, across: number): boolean {
|
|
97
93
|
const bar = s.bars[index]
|
|
98
|
-
if (!bar || bar.value === 0) return ""
|
|
99
|
-
// An interior segment prints inside itself and must hold a line of type; a grouped column prints past its end.
|
|
100
|
-
if (insideAt(s, index)) {
|
|
101
|
-
const ends = endsAt(s, index)
|
|
102
|
-
if (!ends) return ""
|
|
103
|
-
if (Math.abs(ends.end - ends.base) < valueSize(s) * VALUE_FIT) return ""
|
|
104
|
-
}
|
|
105
|
-
return bar.caption ?? formatMarker(bar.value)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function captionY(s: Columns, index: number): number {
|
|
109
94
|
const ends = endsAt(s, index)
|
|
110
|
-
if (!ends) return
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const direction = ends.end >= ends.base ? 1 : -1
|
|
114
|
-
return ends.end + direction * (VALUE_GAP + valueSize(s) / 2)
|
|
95
|
+
if (!bar || !ends) return false
|
|
96
|
+
const room = sizeOf(s.valueStyle) * INSIDE_ROOM
|
|
97
|
+
return Math.abs(ends.end - ends.base) >= extent + room && bar.thickness >= across
|
|
115
98
|
}
|
|
116
99
|
|
|
117
100
|
/**
|
|
118
|
-
* One series of a {@link BarChart}: its
|
|
119
|
-
*
|
|
101
|
+
* One series of a {@link BarChart}: its bar in every category plus the printed
|
|
102
|
+
* figures. A node per series, like `ChartLine`, so `showSeries` and
|
|
120
103
|
* `spotlightSeries` mean the same on both: enable the node or tween its opacity.
|
|
121
104
|
* Positions come from the parent, which owns the slot arithmetic.
|
|
122
|
-
*
|
|
123
|
-
* The figures are {@link Text} children, so they take the theme's type style,
|
|
124
|
-
* are measured by layout, and hide with the series.
|
|
125
105
|
*/
|
|
126
106
|
export const ChartBars = component2d({
|
|
127
107
|
key: "chartBars",
|
|
128
108
|
state: {
|
|
129
109
|
bars: fresh<BarGeometry[]>(() => []),
|
|
130
|
-
/** Pixel
|
|
110
|
+
/** Pixel position of value 0 along the value axis, where every bar grows from. */
|
|
131
111
|
baseline: 0,
|
|
112
|
+
/** Bars grow along x from a category column rather than along y from a row. */
|
|
113
|
+
horizontal: false,
|
|
132
114
|
fill: Types.custom<Fill, FillResolved[]>([], { resolve: fillOps.resolve, lerp: fillOps.lerp }),
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
115
|
+
/** As for a bar standing up from the baseline: `topLeft`/`topRight` round its far end, the bottom pair its base. */
|
|
116
|
+
cornerRadius: Types.cornerRadius(8),
|
|
117
|
+
cornerStyle: Types.cornerStyle("rounded"),
|
|
136
118
|
/**
|
|
137
|
-
* Rise of every
|
|
138
|
-
*
|
|
139
|
-
* mid-animation and a negative
|
|
119
|
+
* Rise of every bar in `[0, 1]` about the baseline, not each bar's own base:
|
|
120
|
+
* every segment travels by the same factor, so a stack stays flush
|
|
121
|
+
* mid-animation and a negative bar grows the other way.
|
|
140
122
|
*/
|
|
141
123
|
growth: 1,
|
|
142
|
-
/** Per-
|
|
124
|
+
/** Per-bar opacity by row index, how the category spotlight reaches one bar. Missing reads as 1. */
|
|
143
125
|
dims: fresh<number[]>(() => []),
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
/** Read as built: whether each bar prints its figure, and where. */
|
|
127
|
+
values: Types.custom<"none" | "end" | "inside">("none"),
|
|
128
|
+
valueStyle: Types.custom<TextStyle>({ fontSize: 24, fill: "white" }),
|
|
129
|
+
/** The wholesale switch behind `showSeries`/`hideSeries`; off skips the bars and their figures. */
|
|
147
130
|
enabled: true,
|
|
148
131
|
},
|
|
149
132
|
// A node made of its figures would otherwise hug them.
|
|
150
133
|
defaults: { width: "fill", height: "fill" },
|
|
151
|
-
// One
|
|
152
|
-
// and bound to the live geometry so it travels with its
|
|
134
|
+
// One figure per bar, built once (the row count is fixed at construction)
|
|
135
|
+
// and bound to the live geometry so it travels with its bar.
|
|
153
136
|
children: (state) => {
|
|
154
|
-
if (
|
|
155
|
-
return state.bars.map((_, i) =>
|
|
156
|
-
Text({
|
|
157
|
-
...
|
|
158
|
-
text: () =>
|
|
159
|
-
x: () => state.bars[i]?.
|
|
160
|
-
y: () =>
|
|
161
|
-
opacity: () =>
|
|
162
|
-
|
|
163
|
-
|
|
137
|
+
if (state.values === "none") return []
|
|
138
|
+
return asParts(state.bars.map((_, i) => {
|
|
139
|
+
const caption: Node2D = new Text({
|
|
140
|
+
...boundText(() => ({ ...state.valueStyle, ...(state.values === "inside" && { fill: inkOn(state.fill) }) })),
|
|
141
|
+
text: () => state.bars[i]?.caption ?? "",
|
|
142
|
+
x: () => (state.horizontal ? captionAlong(state, i, caption.layoutBounds.width) : (state.bars[i]?.center ?? 0)),
|
|
143
|
+
y: () => (state.horizontal ? (state.bars[i]?.center ?? 0) : captionAlong(state, i, caption.layoutBounds.height)),
|
|
144
|
+
opacity: () => {
|
|
145
|
+
const box = caption.layoutBounds
|
|
146
|
+
const along = state.horizontal ? box.width : box.height
|
|
147
|
+
const across = state.horizontal ? box.height : box.width
|
|
148
|
+
if (state.values === "inside" && !fitsInside(state, i, along, across)) return 0
|
|
149
|
+
// A figure past the end rises in with its bar's last stretch.
|
|
150
|
+
return dimAt(state, i) * clamp((state.growth - 0.6) / 0.4, 0, 1)
|
|
151
|
+
},
|
|
152
|
+
}) as unknown as Node2D
|
|
153
|
+
return caption
|
|
154
|
+
}))
|
|
164
155
|
},
|
|
165
|
-
|
|
166
|
-
load: (loader, state) =>
|
|
167
|
-
loadPaints(loader, { fills: [state.fill], shadows: [state.barShadow], strokes: [state.barStroke] }),
|
|
156
|
+
load: (loader, state) => loadPaints(loader, { fills: [state.fill] }),
|
|
168
157
|
commands: {
|
|
169
|
-
/** Raise every
|
|
158
|
+
/** Raise every bar from the baseline (`growth` 0 to 1). The resting state is the finished one; the chart arms it back to 0. */
|
|
170
159
|
enter: (timeline) => {
|
|
171
160
|
const ease = frameEase(timeline)
|
|
172
161
|
return timeline.animate((t) => ({ growth: ease(t) }))
|
|
@@ -177,29 +166,19 @@ export const ChartBars = component2d({
|
|
|
177
166
|
for (let i = 0; i < state.bars.length; i++) {
|
|
178
167
|
const bar = state.bars[i]
|
|
179
168
|
const ends = endsAt(state, i)
|
|
180
|
-
if (!ends || bar.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
height,
|
|
192
|
-
|
|
193
|
-
cornerRadius:
|
|
194
|
-
radius > 0
|
|
195
|
-
? ends.end >= ends.base
|
|
196
|
-
? { topLeft: radius, topRight: radius, bottomLeft: 0, bottomRight: 0 }
|
|
197
|
-
: { bottomLeft: radius, bottomRight: radius, topLeft: 0, topRight: 0 }
|
|
198
|
-
: 0,
|
|
199
|
-
})
|
|
200
|
-
rect.shadow(state.barShadow)
|
|
201
|
-
rect.fill(state.fill)
|
|
202
|
-
rect.stroke(state.barStroke)
|
|
169
|
+
if (!ends || bar.thickness <= 0) continue
|
|
170
|
+
// A segment inside a stack reaches a pixel under the next, so no seam of background shows between them.
|
|
171
|
+
const seam = bar.round ? 0 : 1
|
|
172
|
+
const length = Math.abs(ends.end - ends.base) + seam
|
|
173
|
+
if (length <= seam) continue
|
|
174
|
+
const forward = ends.end >= ends.base
|
|
175
|
+
const mid = (ends.base + ends.end) / 2 + (forward ? seam : -seam) / 2
|
|
176
|
+
const corners = barCorners(state, bar, forward, length)
|
|
177
|
+
// One `Graphics2D` per bar: opacity belongs to the command list, and the spotlight dims single bars.
|
|
178
|
+
const g = new Graphics2D().rect(state.horizontal
|
|
179
|
+
? { x: mid, y: bar.center, width: length, height: bar.thickness, ...corners }
|
|
180
|
+
: { x: bar.center, y: mid, width: bar.thickness, height: length, ...corners })
|
|
181
|
+
g.fill(state.fill)
|
|
203
182
|
ctx.draw(g.opacity(dimAt(state, i)))
|
|
204
183
|
}
|
|
205
184
|
}
|
|
@@ -208,3 +187,43 @@ export const ChartBars = component2d({
|
|
|
208
187
|
})
|
|
209
188
|
|
|
210
189
|
export type ChartBars = NodeOf<typeof ChartBars>
|
|
190
|
+
|
|
191
|
+
type Corners<T> = { topLeft: T; topRight: T; bottomRight: T; bottomLeft: T }
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Turns corners written for a bar standing up into the drawn bar's visual
|
|
195
|
+
* (y-up) corners: mirrored for a bar below zero, turned a quarter clockwise
|
|
196
|
+
* for one growing right, and both for one growing left.
|
|
197
|
+
*/
|
|
198
|
+
function orient<T>(c: Corners<T>, horizontal: boolean, forward: boolean): Corners<T> {
|
|
199
|
+
if (!horizontal) return forward ? c : { topLeft: c.bottomLeft, topRight: c.bottomRight, bottomRight: c.topRight, bottomLeft: c.topLeft }
|
|
200
|
+
return forward
|
|
201
|
+
? { topLeft: c.bottomLeft, topRight: c.topLeft, bottomRight: c.topRight, bottomLeft: c.bottomRight }
|
|
202
|
+
: { topLeft: c.topLeft, topRight: c.bottomLeft, bottomRight: c.bottomRight, bottomLeft: c.topRight }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Bar `bar`'s rect corners: the far pair only where its stack ends, the base pair only on the baseline, each held inside the bar. */
|
|
206
|
+
export function barCorners(
|
|
207
|
+
state: { readonly cornerRadius: CornerRadiusResolved; readonly cornerStyle: CornerStyleResolved; readonly horizontal: boolean },
|
|
208
|
+
bar: BarGeometry,
|
|
209
|
+
forward: boolean,
|
|
210
|
+
length: number
|
|
211
|
+
): { cornerRadius: Corners<number>; cornerStyle: CornerStyleResolved } {
|
|
212
|
+
const grounded = bar.grounded ?? true
|
|
213
|
+
const r = state.cornerRadius
|
|
214
|
+
const ends = (bar.round ? 1 : 0) + (grounded ? 1 : 0)
|
|
215
|
+
const cap = Math.max(0, Math.min(bar.thickness / 2, ends > 1 ? length / 2 : length))
|
|
216
|
+
const fit = (v: number, on: boolean) => (on ? Math.max(0, Math.min(v, cap)) : 0)
|
|
217
|
+
const upright: Corners<number> = {
|
|
218
|
+
topLeft: fit(r.topLeft, bar.round),
|
|
219
|
+
topRight: fit(r.topRight, bar.round),
|
|
220
|
+
bottomRight: fit(r.bottomRight, grounded),
|
|
221
|
+
bottomLeft: fit(r.bottomLeft, grounded),
|
|
222
|
+
}
|
|
223
|
+
return { cornerRadius: orient(upright, state.horizontal, forward), cornerStyle: orient(state.cornerStyle, state.horizontal, forward) }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** A figure as the bar prints it when the chart gives no caption. */
|
|
227
|
+
export function barCaption(value: number): string {
|
|
228
|
+
return value === 0 ? "" : formatMarker(value)
|
|
229
|
+
}
|
package/src/bar-chart/index.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
export { BarChart, barChartDomain } from "./bar-chart"
|
|
2
|
-
export type { BarChartDomain, BarChartValues } from "./bar-chart"
|
|
2
|
+
export type { BarChartDomain, BarChartStyle, BarChartValues } from "./bar-chart"
|
|
3
3
|
export { ChartBars } from "./chart-bars"
|
|
4
4
|
export type { BarGeometry } from "./chart-bars"
|
|
5
|
-
export { CategoryAxis } from "./category-axis"
|
|
5
|
+
export { CategoryAxis, slotCentre } from "./category-axis"
|
|
6
6
|
export {
|
|
7
|
-
|
|
7
|
+
BAR_LABELS,
|
|
8
|
+
BAR_MARKS,
|
|
8
9
|
barCategories,
|
|
9
10
|
barSpans,
|
|
10
11
|
barValueExtent,
|
|
11
|
-
|
|
12
|
-
resolveBarChartTheme,
|
|
13
|
-
resolveBarFill,
|
|
12
|
+
inkOn,
|
|
14
13
|
resolveBarScale,
|
|
15
|
-
stackCapIndex,
|
|
16
14
|
} from "./shared"
|
|
17
15
|
export type {
|
|
18
|
-
|
|
19
|
-
BarChartTheme,
|
|
16
|
+
BarOrientation,
|
|
20
17
|
BarSeries,
|
|
21
18
|
BarSpan,
|
|
22
|
-
|
|
19
|
+
BarStack,
|
|
23
20
|
} from "./shared"
|