@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
package/src/kit/chart-canvas.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
Types,
|
|
4
4
|
component2d,
|
|
5
5
|
fillOps,
|
|
6
|
-
shadowOps,
|
|
7
6
|
strokeOps,
|
|
8
7
|
type Fill,
|
|
9
8
|
type FillResolved,
|
|
@@ -11,86 +10,82 @@ import {
|
|
|
11
10
|
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
12
11
|
type Node2D as _Node2D,
|
|
13
12
|
type Node2DProps as _Node2DProps,
|
|
14
|
-
type
|
|
15
|
-
type ShadowResolved,
|
|
13
|
+
type RenderContext,
|
|
16
14
|
type Stroke,
|
|
17
15
|
type StrokeResolved,
|
|
16
|
+
type Vector2,
|
|
18
17
|
} from "@motionscript/sdk"
|
|
19
18
|
|
|
20
19
|
import { fresh } from "./entries"
|
|
21
20
|
import { loadPaints } from "./paints"
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
import type { Range } from "./shared"
|
|
22
|
+
|
|
23
|
+
const strokeEntry = () => Types.custom<Stroke, StrokeResolved[]>([], { resolve: strokeOps.resolve, lerp: strokeOps.lerp })
|
|
24
|
+
|
|
25
|
+
/** Where along a scale a fraction lands in a run of `length` px centred on 0. */
|
|
26
|
+
function along(value: number, [min, max]: Range, length: number): number {
|
|
27
|
+
return (max <= min ? 0 : (value - min) / (max - min) - 0.5) * length
|
|
28
|
+
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
/** One rule per draw: stroking a union of open paths paints nothing, and a dashed rule keeps its own phase. */
|
|
31
|
+
export function rule(ctx: RenderContext, from: Vector2, to: Vector2, stroke: StrokeResolved[], opacity = 1): void {
|
|
32
|
+
ctx.draw(new Graphics2D().line({ points: [from, to] }).stroke(stroke).opacity(opacity))
|
|
33
|
+
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
* The plot backdrop:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
36
|
+
* The plot backdrop: the panel fill, the grid and the axis rules, sized from its
|
|
37
|
+
* box. Grid lines are placed from data-unit marker positions mapped through the
|
|
38
|
+
* extents, so each lands under its marker however the markers are spaced; a line
|
|
39
|
+
* on an edge is drawn just inside it.
|
|
37
40
|
*/
|
|
38
41
|
export const ChartCanvas = component2d({
|
|
39
42
|
key: "chartCanvas",
|
|
40
43
|
state: {
|
|
41
|
-
fill: Types.custom<Fill, FillResolved[]>(
|
|
42
|
-
|
|
43
|
-
xGridStroke: strokeEntry(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* Data-unit positions of the vertical grid lines, one per horizontal marker.
|
|
47
|
-
* Positions at or past an extent edge coincide with the border and are
|
|
48
|
-
* skipped. Empty for a categorical axis, which has no numeric position to rule at.
|
|
49
|
-
*/
|
|
44
|
+
fill: Types.custom<Fill, FillResolved[]>([], { resolve: fillOps.resolve, lerp: fillOps.lerp }),
|
|
45
|
+
/** The vertical grid lines, one per `xMarkers` entry. */
|
|
46
|
+
xGridStroke: strokeEntry(),
|
|
47
|
+
/** The horizontal grid lines, one per `yMarkers` entry. */
|
|
48
|
+
yGridStroke: strokeEntry(),
|
|
50
49
|
xMarkers: fresh<number[]>(() => []),
|
|
51
50
|
yMarkers: fresh<number[]>(() => []),
|
|
52
51
|
/** `[min, max]` of each scale, mapping the markers into the panel. */
|
|
53
52
|
xExtent: fresh<Range>(() => [0, 1]),
|
|
54
53
|
yExtent: fresh<Range>(() => [0, 1]),
|
|
54
|
+
/** The baseline and the value-axis rule. */
|
|
55
|
+
axisStroke: strokeEntry(),
|
|
56
|
+
/** Where the horizontal axis rule sits on the y scale, or `null` for none. */
|
|
57
|
+
xAxisAt: Types.custom<number | null>(null),
|
|
58
|
+
/** Where the vertical axis rule sits on the x scale, or `null` for none. */
|
|
59
|
+
yAxisAt: Types.custom<number | null>(null),
|
|
55
60
|
},
|
|
56
|
-
|
|
57
|
-
load: (loader, state) =>
|
|
58
|
-
loadPaints(loader, { fills: [state.fill], shadows: [state.shadow], strokes: [state.xGridStroke, state.yGridStroke] }),
|
|
61
|
+
load: (loader, state) => loadPaints(loader, { fills: [state.fill], strokes: [state.xGridStroke, state.yGridStroke, state.axisStroke] }),
|
|
59
62
|
render: (ctx, state) => {
|
|
60
63
|
const { width: W, height: H } = ctx.size
|
|
61
64
|
if (W <= 0 || H <= 0) return
|
|
65
|
+
if (state.fill.length > 0) ctx.draw(new Graphics2D().rect({ x: 0, y: 0, width: W, height: H }).fill(state.fill))
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Markers on or past the edges would sit under the border.
|
|
71
|
-
const interior = (f: number) => f > 1e-6 && f < 1 - 1e-6
|
|
72
|
-
|
|
73
|
-
const xStroke = state.xGridStroke[0]
|
|
74
|
-
if (xStroke) {
|
|
75
|
-
const xGrid = new Graphics2D()
|
|
76
|
-
for (const v of state.xMarkers) {
|
|
77
|
-
const f = xFrac(v)
|
|
78
|
-
if (!interior(f)) continue
|
|
79
|
-
xGrid.rect({ x: -W / 2 + f * W, y: 0, width: xStroke.weight, height: H })
|
|
67
|
+
const inside = (at: number, length: number, weight: number) => Math.max(-length / 2 + weight / 2, Math.min(length / 2 - weight / 2, at))
|
|
68
|
+
const verticals = (values: readonly number[], stroke: StrokeResolved[]) => {
|
|
69
|
+
const weight = stroke[0]?.weight ?? 0
|
|
70
|
+
if (weight <= 0) return
|
|
71
|
+
for (const v of values) {
|
|
72
|
+
const x = inside(along(v, state.xExtent, W), W, weight)
|
|
73
|
+
rule(ctx, { x, y: -H / 2 }, { x, y: H / 2 }, stroke)
|
|
80
74
|
}
|
|
81
|
-
ctx.draw(xGrid.fill(xStroke.fill))
|
|
82
75
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!interior(f)) continue
|
|
90
|
-
yGrid.rect({ x: 0, y: -H / 2 + f * H, width: W, height: yStroke.weight })
|
|
76
|
+
const horizontals = (values: readonly number[], stroke: StrokeResolved[]) => {
|
|
77
|
+
const weight = stroke[0]?.weight ?? 0
|
|
78
|
+
if (weight <= 0) return
|
|
79
|
+
for (const v of values) {
|
|
80
|
+
const y = inside(along(v, state.yExtent, H), H, weight)
|
|
81
|
+
rule(ctx, { x: -W / 2, y }, { x: W / 2, y }, stroke)
|
|
91
82
|
}
|
|
92
|
-
ctx.draw(yGrid.fill(yStroke.fill))
|
|
93
83
|
}
|
|
84
|
+
const within = ([min, max]: Range) => (v: number) => v >= Math.min(min, max) - 1e-9 && v <= Math.max(min, max) + 1e-9
|
|
85
|
+
verticals(state.xMarkers.filter(within(state.xExtent)), state.xGridStroke)
|
|
86
|
+
horizontals(state.yMarkers.filter(within(state.yExtent)), state.yGridStroke)
|
|
87
|
+
if (state.xAxisAt !== null) horizontals([state.xAxisAt], state.axisStroke)
|
|
88
|
+
if (state.yAxisAt !== null) verticals([state.yAxisAt], state.axisStroke)
|
|
94
89
|
},
|
|
95
90
|
})
|
|
96
91
|
|
package/src/kit/chart-legend.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
Text,
|
|
7
7
|
Types,
|
|
8
8
|
component2d,
|
|
9
|
-
textOps,
|
|
10
9
|
type Fill,
|
|
11
10
|
type NodeOf,
|
|
12
11
|
// Named so the row's emitted type reaches the Rect surface it spreads through the sdk, not core.
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
type Vector2 as _Vector2,
|
|
29
28
|
} from "@motionscript/sdk"
|
|
30
29
|
|
|
30
|
+
import { boundText } from "./cartesian"
|
|
31
31
|
import { fresh } from "./entries"
|
|
32
32
|
import { frameEase } from "./motion"
|
|
33
33
|
import {
|
|
@@ -40,18 +40,19 @@ import {
|
|
|
40
40
|
const row = { ...Paint.state, ...Flow.state, ...Shapes.corners }
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* One legend entry, hugging its
|
|
44
|
-
* from nothing. The
|
|
45
|
-
* never depends on the entry once having been
|
|
46
|
-
*
|
|
43
|
+
* One legend entry, hugging its swatch and caption; `shown` scales its length
|
|
44
|
+
* along the legend (`along`) from nothing. The length is measured from the
|
|
45
|
+
* content at any reveal, so a frame never depends on the entry once having been
|
|
46
|
+
* fully shown.
|
|
47
47
|
*/
|
|
48
48
|
const LegendEntry = component2d({
|
|
49
49
|
key: "legendEntry",
|
|
50
|
-
state: { ...row, shown: Types.number(1) },
|
|
50
|
+
state: { ...row, shown: Types.number(1), along: Types.oneOf(["horizontal", "vertical"] as const) },
|
|
51
51
|
commands: { ...Paint.commands },
|
|
52
52
|
measure: (measurer, state, children) => {
|
|
53
53
|
const size = Flow.measure(measurer, state, children, Paint.inset(state))
|
|
54
|
-
|
|
54
|
+
if (state.shown >= 1) return size
|
|
55
|
+
return state.along === "vertical" ? { ...size, height: state.shown * size.height } : { ...size, width: state.shown * size.width }
|
|
55
56
|
},
|
|
56
57
|
layout: (box, state, children) => Flow.layout(box, state, children, Paint.inset(state)),
|
|
57
58
|
render: (ctx, state) => Paint.draw(ctx, state, Shapes.rect(ctx.size, state)),
|
|
@@ -65,7 +66,11 @@ function revealOf(value: readonly number[] | Readonly<Record<number, number>>, p
|
|
|
65
66
|
return next
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* A chart's legend: one entry per series, a colour swatch then its caption, in
|
|
71
|
+
* a row (`flow: "horizontal"`) or a column (`"vertical"`, for a legend beside
|
|
72
|
+
* the plot). This node is the row, so the parent places it.
|
|
73
|
+
*/
|
|
69
74
|
export const ChartLegend = component2d({
|
|
70
75
|
key: "chartLegend",
|
|
71
76
|
state: {
|
|
@@ -75,7 +80,7 @@ export const ChartLegend = component2d({
|
|
|
75
80
|
swatches: fresh<Fill[]>(() => []),
|
|
76
81
|
legend: Types.custom<LegendConfig>(DEFAULT_LEGEND),
|
|
77
82
|
/**
|
|
78
|
-
* How far each entry is shown, `0`-`1`, driving its
|
|
83
|
+
* How far each entry is shown, `0`-`1`, driving its length, opacity and gap
|
|
79
84
|
* together. Entries are never added or removed: "is this node a child" is not
|
|
80
85
|
* a function of time, while collapsed-but-present is.
|
|
81
86
|
*/
|
|
@@ -86,34 +91,37 @@ export const ChartLegend = component2d({
|
|
|
86
91
|
flow: "horizontal",
|
|
87
92
|
width: "hug",
|
|
88
93
|
height: "hug",
|
|
89
|
-
gap: (state) => state.legend.itemGap,
|
|
94
|
+
gap: (state) => (state.flow === "vertical" ? Math.round(state.legend.itemGap * 0.4) : state.legend.itemGap),
|
|
90
95
|
padding: (state) => state.legend.padding,
|
|
96
|
+
align: (state) => (state.flow === "vertical" ? "centerLeft" : "center"),
|
|
91
97
|
reveal: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
|
|
92
98
|
},
|
|
93
99
|
// Every entry stays a child, so one always comes back where it belongs.
|
|
94
100
|
children: (state) => {
|
|
95
|
-
const marker = state.legend.markerStyle
|
|
101
|
+
const marker = () => state.legend.markerStyle
|
|
102
|
+
const along = state.flow === "vertical" ? "vertical" : "horizontal"
|
|
96
103
|
return state.series.map((series, index) =>
|
|
97
104
|
LegendEntry({
|
|
98
105
|
flow: "horizontal",
|
|
99
|
-
gap:
|
|
106
|
+
gap: () => Math.round(((state.legend.textStyle.fontSize as number | undefined) ?? 24) * 0.45),
|
|
100
107
|
align: "center",
|
|
101
108
|
width: "hug",
|
|
102
109
|
height: "hug",
|
|
103
|
-
|
|
110
|
+
along,
|
|
111
|
+
// The length follows the reveal in `measure`, and the flanking gap closes with the entry.
|
|
104
112
|
opacity: () => state.reveal[index] ?? 0,
|
|
105
113
|
gapScale: () => state.reveal[index] ?? 0,
|
|
106
114
|
shown: () => state.reveal[index] ?? 0,
|
|
107
115
|
children: [
|
|
108
|
-
Text({ ...textOps.styleProps(state.legend.textStyle), text: series.label }),
|
|
109
116
|
Rect({
|
|
110
|
-
width: marker.width,
|
|
111
|
-
height: marker.height,
|
|
112
|
-
cornerRadius: marker.borderRadius,
|
|
113
|
-
fill: state.swatches[index] ?? "white",
|
|
114
|
-
stroke: marker.stroke,
|
|
115
|
-
shadow: marker.shadow,
|
|
117
|
+
width: () => marker().width,
|
|
118
|
+
height: () => marker().height,
|
|
119
|
+
cornerRadius: () => marker().borderRadius,
|
|
120
|
+
fill: () => state.swatches[index] ?? "white",
|
|
121
|
+
stroke: () => marker().stroke,
|
|
122
|
+
shadow: () => marker().shadow,
|
|
116
123
|
}),
|
|
124
|
+
Text({ ...boundText(() => state.legend.textStyle), text: series.label || series.field }),
|
|
117
125
|
],
|
|
118
126
|
})
|
|
119
127
|
)
|
|
@@ -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
|
@@ -8,6 +8,13 @@ export type AnyTimeline = Timeline<any, any>
|
|
|
8
8
|
/** How far a legend starts below its slot when `enter` brings it in, in px. */
|
|
9
9
|
export const LEGEND_SLIDE = 20
|
|
10
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
|
+
|
|
11
18
|
/** Marker count when an axis states no ticks. */
|
|
12
19
|
export const DEFAULT_MARKERS = 5
|
|
13
20
|
|
|
@@ -29,10 +29,10 @@ 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 { DEFAULT_LEGEND, type LegendConfig } from "
|
|
35
|
-
import type { PieSlice } from "./
|
|
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"
|
|
36
36
|
|
|
37
37
|
const SLIDE = 16
|
|
38
38
|
|
|
@@ -66,7 +66,7 @@ export const PieLegend = component2d({
|
|
|
66
66
|
},
|
|
67
67
|
// The selection dim rides a wrapper, so the entrance animates the entry's own opacity without the two fighting over one value.
|
|
68
68
|
children: (state) => {
|
|
69
|
-
const marker = state.legend.markerStyle
|
|
69
|
+
const marker = () => state.legend.markerStyle
|
|
70
70
|
return state.slices.map((_, index) =>
|
|
71
71
|
Rect({
|
|
72
72
|
width: "hug",
|
|
@@ -76,7 +76,7 @@ export const PieLegend = component2d({
|
|
|
76
76
|
children: [
|
|
77
77
|
Rect({
|
|
78
78
|
flow: "horizontal",
|
|
79
|
-
gap: 12,
|
|
79
|
+
gap: () => Math.round(state.legend.textStyle.fontSize === undefined ? 12 : Number(state.legend.textStyle.fontSize) * 0.4),
|
|
80
80
|
align: "center",
|
|
81
81
|
width: "hug",
|
|
82
82
|
height: "hug",
|
|
@@ -84,15 +84,16 @@ export const PieLegend = component2d({
|
|
|
84
84
|
x: () => arrivalOf(state.arrivals, index).shift,
|
|
85
85
|
children: [
|
|
86
86
|
Rect({
|
|
87
|
-
width: marker.
|
|
88
|
-
height: marker.height,
|
|
89
|
-
cornerRadius: marker.borderRadius,
|
|
87
|
+
width: () => marker().width,
|
|
88
|
+
height: () => marker().height,
|
|
89
|
+
cornerRadius: () => marker().borderRadius,
|
|
90
90
|
fill: () => state.slices[index]?.fill ?? "white",
|
|
91
|
-
stroke: marker.stroke,
|
|
92
|
-
shadow: marker.shadow,
|
|
93
91
|
}),
|
|
94
92
|
Text({
|
|
95
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,
|
|
96
97
|
text: () => state.slices[index]?.category ?? "",
|
|
97
98
|
}),
|
|
98
99
|
],
|
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
|
+
|