@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
package/src/kit/style.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chart family's style: one `style` entry per chart, built from the groups
|
|
3
|
+
* below, so a look saved off one chart lays onto any other (a field the other
|
|
4
|
+
* chart does not draw is dropped). A chart spreads the shared groups and adds a
|
|
5
|
+
* `marks` group of its own.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Types,
|
|
10
|
+
type Fill,
|
|
11
|
+
type StrokeResolved,
|
|
12
|
+
type Stroke as _Stroke,
|
|
13
|
+
type StyleInput,
|
|
14
|
+
type TextStyle,
|
|
15
|
+
// Named so the emitted style types reach these through the sdk, not core.
|
|
16
|
+
type FillResolved as _FillResolved,
|
|
17
|
+
type Type as _Type,
|
|
18
|
+
} from "@motionscript/sdk"
|
|
19
|
+
|
|
20
|
+
import type { AxisConfig, ChartSeries, LegendConfig } from "./shared"
|
|
21
|
+
|
|
22
|
+
/** The style family every chart shares; a host keys saved styles by it. */
|
|
23
|
+
export const CHART_STYLE_FAMILY = "chart"
|
|
24
|
+
|
|
25
|
+
/** The out-of-the-box series colours, cycled by series (or row) index. */
|
|
26
|
+
export const CHART_PALETTE_DEFAULT = ["#4f8cff", "#2fbf8f", "#f5a524", "#e5547a", "#9b6cf0"] as const
|
|
27
|
+
|
|
28
|
+
export const CHART_COLORS = {
|
|
29
|
+
/** A colour per series (a pie's or radial's per row), cycled. */
|
|
30
|
+
palette: Types.palette(CHART_PALETTE_DEFAULT),
|
|
31
|
+
/** Values, legend captions, centre text. */
|
|
32
|
+
foreground: Types.fill("#f4f4f5"),
|
|
33
|
+
/** Axis markers and captions, spoke labels. */
|
|
34
|
+
muted: Types.fill("#a1a1aa"),
|
|
35
|
+
/** Behind the plot; none by default, so the chart reads against the stage. */
|
|
36
|
+
background: Types.fill([]),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const CHART_TEXT = {
|
|
40
|
+
/** Empty: the scene's default face. */
|
|
41
|
+
fontFamily: Types.font(""),
|
|
42
|
+
/** Marker and legend size at 1080p; titles and values scale from it. */
|
|
43
|
+
fontSize: Types.number(28, { min: 8, max: 160, step: 1, unit: "px" }),
|
|
44
|
+
fontWeight: Types.number(500, { min: 100, max: 900, step: 100 }),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const CARTESIAN_GRID = {
|
|
48
|
+
/** The lines along the value ticks of a column chart (one per tick); empty draws none. */
|
|
49
|
+
horizontalGrid: Types.stroke({ weight: 2, fill: "white/15" }),
|
|
50
|
+
/** The lines under the category markers; empty draws none. */
|
|
51
|
+
verticalGrid: Types.stroke([]),
|
|
52
|
+
/** The baseline and the value-axis rule; empty draws none. */
|
|
53
|
+
axisStroke: Types.stroke([]),
|
|
54
|
+
/** Read as the chart is built: whether the category (x) axis row is drawn. */
|
|
55
|
+
showXAxis: Types.boolean(true, { animatable: false }),
|
|
56
|
+
/** Read as the chart is built: whether the value (y) axis column is drawn. */
|
|
57
|
+
showYAxis: Types.boolean(true, { animatable: false }),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type LegendPosition = "bottom" | "top" | "right" | "none"
|
|
61
|
+
export type LegendSwatch = "square" | "circle" | "line"
|
|
62
|
+
|
|
63
|
+
export const CHART_LEGEND = {
|
|
64
|
+
/** Read as the chart is built: where the legend sits, or `none`. */
|
|
65
|
+
legend: Types.oneOf(["bottom", "top", "right", "none"] as const, { animatable: false }),
|
|
66
|
+
swatch: Types.oneOf(["square", "circle", "line"] as const),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The type fields, and the two text colours: what every text helper reads. */
|
|
70
|
+
export interface ChartTextStyle {
|
|
71
|
+
readonly foreground: Fill
|
|
72
|
+
readonly muted: Fill
|
|
73
|
+
readonly fontFamily: string
|
|
74
|
+
readonly fontSize: number
|
|
75
|
+
readonly fontWeight: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The fields every chart's style resolves, whichever groups it spreads. */
|
|
79
|
+
export interface ChartStyleBase extends ChartTextStyle {
|
|
80
|
+
readonly palette: readonly Fill[]
|
|
81
|
+
readonly background: Fill
|
|
82
|
+
readonly legend: LegendPosition
|
|
83
|
+
readonly swatch: LegendSwatch
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The cartesian grid fields, for the line and bar charts. */
|
|
87
|
+
export interface CartesianStyle extends ChartStyleBase {
|
|
88
|
+
readonly horizontalGrid: StrokeResolved[]
|
|
89
|
+
readonly verticalGrid: StrokeResolved[]
|
|
90
|
+
readonly axisStroke: StrokeResolved[]
|
|
91
|
+
readonly showXAxis: boolean
|
|
92
|
+
readonly showYAxis: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The rule and track fields a preset restates so its rules read on its background (a chart keeps those it draws). */
|
|
96
|
+
const _PRESET_RULES = {
|
|
97
|
+
horizontalGrid: CARTESIAN_GRID.horizontalGrid,
|
|
98
|
+
gridStroke: Types.stroke([]),
|
|
99
|
+
spokeStroke: Types.stroke([]),
|
|
100
|
+
track: Types.fill([]),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A preset's fields: any chart's, so one preset dresses every chart. */
|
|
104
|
+
export type ChartStylePreset = StyleInput<typeof CHART_COLORS & typeof CHART_TEXT & typeof _PRESET_RULES>
|
|
105
|
+
|
|
106
|
+
/** Ready-made looks, each stating every colour so applying one replaces the last. */
|
|
107
|
+
export const CHART_STYLE_PRESETS = {
|
|
108
|
+
midnight: {
|
|
109
|
+
palette: [...CHART_PALETTE_DEFAULT],
|
|
110
|
+
foreground: "#f4f4f5",
|
|
111
|
+
muted: "#a1a1aa",
|
|
112
|
+
horizontalGrid: { weight: 2, fill: "white/15" },
|
|
113
|
+
gridStroke: { weight: 2, fill: "white/15" },
|
|
114
|
+
spokeStroke: { weight: 2, fill: "white/15" },
|
|
115
|
+
track: "white/10",
|
|
116
|
+
background: [],
|
|
117
|
+
},
|
|
118
|
+
paper: {
|
|
119
|
+
palette: ["#2563eb", "#059669", "#d97706", "#db2777", "#7c3aed"],
|
|
120
|
+
foreground: "#1c1917",
|
|
121
|
+
muted: "#57534e",
|
|
122
|
+
horizontalGrid: { weight: 2, fill: "#1c1917/12" },
|
|
123
|
+
gridStroke: { weight: 2, fill: "#1c1917/12" },
|
|
124
|
+
spokeStroke: { weight: 2, fill: "#1c1917/12" },
|
|
125
|
+
track: "#1c1917/12",
|
|
126
|
+
background: "#fafaf9",
|
|
127
|
+
},
|
|
128
|
+
ember: {
|
|
129
|
+
palette: ["#d9603b", "#8194ad", "#e0a93b", "#5e9d6b", "#9ec85a"],
|
|
130
|
+
foreground: "#e9e1d4",
|
|
131
|
+
muted: "#8a7f70",
|
|
132
|
+
horizontalGrid: { weight: 2, fill: "#2c2820" },
|
|
133
|
+
gridStroke: { weight: 2, fill: "#2c2820" },
|
|
134
|
+
spokeStroke: { weight: 2, fill: "#2c2820" },
|
|
135
|
+
track: "#2c2820",
|
|
136
|
+
background: "#15130e",
|
|
137
|
+
},
|
|
138
|
+
neon: {
|
|
139
|
+
palette: ["#22d3ee", "#a3e635", "#f472b6", "#facc15", "#818cf8"],
|
|
140
|
+
foreground: "#ffffff",
|
|
141
|
+
muted: "#94a3b8",
|
|
142
|
+
horizontalGrid: { weight: 2, fill: "white/10" },
|
|
143
|
+
gridStroke: { weight: 2, fill: "white/10" },
|
|
144
|
+
spokeStroke: { weight: 2, fill: "white/10" },
|
|
145
|
+
track: "white/10",
|
|
146
|
+
background: [],
|
|
147
|
+
},
|
|
148
|
+
ocean: {
|
|
149
|
+
palette: ["#0ea5e9", "#14b8a6", "#6366f1", "#38bdf8", "#2dd4bf"],
|
|
150
|
+
foreground: "#e0f2fe",
|
|
151
|
+
muted: "#7dd3fc",
|
|
152
|
+
horizontalGrid: { weight: 2, fill: "#7dd3fc/20" },
|
|
153
|
+
gridStroke: { weight: 2, fill: "#7dd3fc/20" },
|
|
154
|
+
spokeStroke: { weight: 2, fill: "#7dd3fc/20" },
|
|
155
|
+
track: "#7dd3fc/20",
|
|
156
|
+
background: "#082f49",
|
|
157
|
+
},
|
|
158
|
+
sunset: {
|
|
159
|
+
palette: ["#f97316", "#ef4444", "#ec4899", "#f59e0b", "#a855f7"],
|
|
160
|
+
foreground: "#fff7ed",
|
|
161
|
+
muted: "#fdba74",
|
|
162
|
+
horizontalGrid: { weight: 2, fill: "#fdba74/20" },
|
|
163
|
+
gridStroke: { weight: 2, fill: "#fdba74/20" },
|
|
164
|
+
spokeStroke: { weight: 2, fill: "#fdba74/20" },
|
|
165
|
+
track: "#fdba74/20",
|
|
166
|
+
background: [],
|
|
167
|
+
},
|
|
168
|
+
pastel: {
|
|
169
|
+
palette: ["#a5b4fc", "#f9a8d4", "#86efac", "#fde68a", "#7dd3fc"],
|
|
170
|
+
foreground: "#f8fafc",
|
|
171
|
+
muted: "#cbd5e1",
|
|
172
|
+
horizontalGrid: { weight: 2, fill: "white/12" },
|
|
173
|
+
gridStroke: { weight: 2, fill: "white/12" },
|
|
174
|
+
spokeStroke: { weight: 2, fill: "white/12" },
|
|
175
|
+
track: "white/12",
|
|
176
|
+
background: [],
|
|
177
|
+
},
|
|
178
|
+
mono: {
|
|
179
|
+
palette: ["#fafafa", "#a3a3a3", "#737373", "#d4d4d4", "#525252"],
|
|
180
|
+
foreground: "#fafafa",
|
|
181
|
+
muted: "#a3a3a3",
|
|
182
|
+
horizontalGrid: { weight: 2, fill: "white/12" },
|
|
183
|
+
gridStroke: { weight: 2, fill: "white/12" },
|
|
184
|
+
spokeStroke: { weight: 2, fill: "white/12" },
|
|
185
|
+
track: "white/12",
|
|
186
|
+
background: [],
|
|
187
|
+
},
|
|
188
|
+
} satisfies Record<string, ChartStylePreset>
|
|
189
|
+
|
|
190
|
+
/** Series `index`'s colour: its own `fill`, else the palette's, cycled. */
|
|
191
|
+
export function paletteFill(style: Pick<ChartStyleBase, "palette">, index: number, own?: Fill): Fill {
|
|
192
|
+
if (own != null) return own
|
|
193
|
+
const palette = style.palette
|
|
194
|
+
return palette.length > 0 ? palette[index % palette.length] : "white"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** {@link paletteFill} for a series. */
|
|
198
|
+
export function seriesFill(style: Pick<ChartStyleBase, "palette">, series: ChartSeries, index: number): Fill {
|
|
199
|
+
return paletteFill(style, index, series.fill)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function face(style: Pick<ChartTextStyle, "fontFamily">): Pick<TextStyle, "fontFamily"> {
|
|
203
|
+
return style.fontFamily ? { fontFamily: style.fontFamily } : {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Axis markers, spoke labels: muted, at the base size. */
|
|
207
|
+
export function markerText(style: ChartTextStyle, textAlign: TextStyle["textAlign"] = "center"): TextStyle {
|
|
208
|
+
return { ...face(style), fontSize: style.fontSize, fontWeight: style.fontWeight, textAlign, fill: style.muted }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Axis captions: muted, a step larger and heavier than the markers. */
|
|
212
|
+
export function titleText(style: ChartTextStyle): TextStyle {
|
|
213
|
+
return {
|
|
214
|
+
...face(style),
|
|
215
|
+
fontSize: Math.round(style.fontSize * 1.1),
|
|
216
|
+
fontWeight: Math.min(900, style.fontWeight + 100),
|
|
217
|
+
textAlign: "center",
|
|
218
|
+
fill: style.muted,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Values on marks and legend captions: the foreground, at the base size. */
|
|
223
|
+
export function valueText(style: ChartTextStyle, textAlign: TextStyle["textAlign"] = "center"): TextStyle {
|
|
224
|
+
return { ...face(style), fontSize: style.fontSize, fontWeight: Math.min(900, style.fontWeight + 100), textAlign, fill: style.foreground }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The kit axis parts' config from a style. `x` is the axis under the plot;
|
|
229
|
+
* its grid lines are vertical (one per x marker), the `y` axis's horizontal.
|
|
230
|
+
*/
|
|
231
|
+
export function axisConfigOf(style: Pick<CartesianStyle, "verticalGrid" | "horizontalGrid"> & ChartTextStyle, axis: "x" | "y"): AxisConfig {
|
|
232
|
+
const gap = Math.round(style.fontSize * 0.6)
|
|
233
|
+
return {
|
|
234
|
+
labelStyle: markerText(style, axis === "x" ? "center" : "right"),
|
|
235
|
+
titleStyle: titleText(style),
|
|
236
|
+
lineStyle: axis === "x" ? style.verticalGrid : style.horizontalGrid,
|
|
237
|
+
tickPadding: axis === "x" ? { top: gap } : { right: gap },
|
|
238
|
+
titlePadding: axis === "x" ? { top: Math.round(gap / 2) } : { right: gap },
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The kit legend's config: captions in the foreground, the swatch shaped by `swatch`. */
|
|
243
|
+
export function legendConfigOf(style: ChartTextStyle & Pick<ChartStyleBase, "swatch">): LegendConfig {
|
|
244
|
+
const size = Math.max(8, Math.round(style.fontSize * 0.6))
|
|
245
|
+
const marker =
|
|
246
|
+
style.swatch === "line"
|
|
247
|
+
? { width: Math.round(size * 2), height: Math.max(3, Math.round(size / 4)), borderRadius: size }
|
|
248
|
+
: style.swatch === "circle"
|
|
249
|
+
? { width: size, height: size, borderRadius: size / 2 }
|
|
250
|
+
: { width: size, height: size, borderRadius: Math.round(size / 4) }
|
|
251
|
+
return {
|
|
252
|
+
padding: 0,
|
|
253
|
+
textStyle: { ...face(style), fontSize: style.fontSize, fontWeight: style.fontWeight, textAlign: "left", fill: style.foreground },
|
|
254
|
+
itemGap: Math.round(style.fontSize * 1.4),
|
|
255
|
+
markerStyle: { ...marker, stroke: [], shadow: [] },
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/kit/vertical-axis.ts
CHANGED
|
@@ -5,166 +5,133 @@ import {
|
|
|
5
5
|
Types,
|
|
6
6
|
component2d,
|
|
7
7
|
insetsOps,
|
|
8
|
-
textOps,
|
|
9
8
|
type NodeOf,
|
|
9
|
+
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
10
10
|
type Node2D,
|
|
11
|
-
type Node2DProps,
|
|
11
|
+
type Node2DProps as _Node2DProps,
|
|
12
12
|
} from "@motionscript/sdk"
|
|
13
|
+
import { enterBudget } from "@motionscript/sdk/component"
|
|
13
14
|
|
|
15
|
+
import { boundText } from "./cartesian"
|
|
14
16
|
import { asParts, fresh } from "./entries"
|
|
15
17
|
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "./motion"
|
|
16
18
|
import {
|
|
17
19
|
DEFAULT_Y_AXIS,
|
|
18
|
-
enterBudget,
|
|
19
20
|
formatMarker,
|
|
20
21
|
type AxisConfig,
|
|
21
22
|
type Range,
|
|
22
23
|
} from "./shared"
|
|
23
24
|
|
|
24
|
-
export interface VerticalAxisProps extends Node2DProps {
|
|
25
|
-
/** `[min, max]` of the vertical scale the markers are positioned within. */
|
|
26
|
-
range: Range
|
|
27
|
-
/**
|
|
28
|
-
* Tick values (in data units) to draw up the axis, each placed by its fraction
|
|
29
|
-
* across {@link VerticalAxisProps.range}. Need not be evenly spaced or include
|
|
30
|
-
* the ends.
|
|
31
|
-
*/
|
|
32
|
-
markers: number[]
|
|
33
|
-
/**
|
|
34
|
-
* How each marker is captioned, one per entry of `markers` and in its order.
|
|
35
|
-
* Empty (the default), or short, captions a marker with its bare numeric
|
|
36
|
-
* reading; a chart with a number format on this axis writes them through it.
|
|
37
|
-
*/
|
|
38
|
-
captions: string[]
|
|
39
|
-
/** Marker/caption text style, grid stroke, and gutters — see {@link AxisConfig}. */
|
|
40
|
-
axis: AxisConfig
|
|
41
|
-
/**
|
|
42
|
-
* Rotated caption drawn in the gutter to the left of the marker column. Empty
|
|
43
|
-
* string (the default) hides it — no caption node is built and no label gutter
|
|
44
|
-
* is reserved.
|
|
45
|
-
*/
|
|
46
|
-
label: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** How far a marker starts to the right of its slot when it enters, in px. */
|
|
50
25
|
const SLIDE = 24
|
|
51
26
|
|
|
52
|
-
/** How a tick value is captioned — see {@link VerticalAxisProps.captions}. */
|
|
53
|
-
export type MarkerLabel = (value: number) => string
|
|
54
|
-
|
|
55
27
|
/** What a marker's caption reads: the one listed for its value, else its bare reading. */
|
|
56
28
|
export function markerCaption(s: { readonly markers: readonly number[]; readonly captions: readonly string[] }, value: number): string {
|
|
57
29
|
const index = s.markers.indexOf(value)
|
|
58
30
|
return (index >= 0 ? s.captions[index] : undefined) ?? formatMarker(value)
|
|
59
31
|
}
|
|
60
32
|
|
|
33
|
+
/** The gap between an axis's markers and the plot, and between the markers and the caption, read off its config. */
|
|
34
|
+
export function axisPads(axis: AxisConfig, side: "top" | "right"): { tick: number; title: number } {
|
|
35
|
+
return { tick: insetsOps.resolve(axis.tickPadding)[side], title: insetsOps.resolve(axis.titlePadding)[side] }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A column of right-aligned captions beside the plot: `place(i)` is caption
|
|
40
|
+
* `i`'s centre height in the column (y-up), whose height it is handed. The
|
|
41
|
+
* value axis and a horizontal bar chart's category axis are both this column.
|
|
42
|
+
*/
|
|
43
|
+
export function captionColumn(spec: {
|
|
44
|
+
readonly count: number
|
|
45
|
+
readonly axis: () => AxisConfig
|
|
46
|
+
readonly text: (i: number) => string
|
|
47
|
+
readonly place: (i: number, height: number) => number
|
|
48
|
+
readonly arrivals: () => Readonly<Record<string, Arrival>>
|
|
49
|
+
readonly opacity?: (i: number) => number
|
|
50
|
+
readonly label: string
|
|
51
|
+
}): Node2D {
|
|
52
|
+
const pads = () => axisPads(spec.axis(), "right")
|
|
53
|
+
const markers = Array.from({ length: spec.count }, (_, i) => {
|
|
54
|
+
const column = () => (marker.parent as Node2D | null)?.layoutBounds ?? { width: 0, height: 0 }
|
|
55
|
+
const marker: Node2D = new Text({
|
|
56
|
+
...boundText(() => spec.axis().labelStyle),
|
|
57
|
+
text: () => spec.text(i),
|
|
58
|
+
// Right-aligned against the tick padding: freeform children sit on the content box's centre.
|
|
59
|
+
x: () => (column().width - pads().tick) / 2 - marker.layoutBounds.width / 2 + arrivalOf(spec.arrivals(), i).shift,
|
|
60
|
+
y: () => spec.place(i, column().height),
|
|
61
|
+
opacity: () => arrivalOf(spec.arrivals(), i).opacity * (spec.opacity?.(i) ?? 1),
|
|
62
|
+
})
|
|
63
|
+
return marker
|
|
64
|
+
})
|
|
65
|
+
const children: Node2D[] = []
|
|
66
|
+
if (spec.label !== "") {
|
|
67
|
+
// Node2D space is y-up and rotation is clockwise, so -90° reads bottom-to-top.
|
|
68
|
+
children.push(
|
|
69
|
+
new RotatedBox({
|
|
70
|
+
angle: -90,
|
|
71
|
+
width: "hug",
|
|
72
|
+
height: "hug",
|
|
73
|
+
children: new Text({
|
|
74
|
+
...boundText(() => spec.axis().titleStyle),
|
|
75
|
+
text: spec.label,
|
|
76
|
+
opacity: () => arrivalOf(spec.arrivals(), "label").opacity,
|
|
77
|
+
x: () => arrivalOf(spec.arrivals(), "label").shift,
|
|
78
|
+
}),
|
|
79
|
+
}) as unknown as Node2D
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
children.push(new Rect({ height: "fill", width: "hug", padding: () => ({ right: pads().tick }), children: markers }) as unknown as Node2D)
|
|
83
|
+
return new Rect({ flow: "horizontal", gap: () => pads().title, height: "fill", children }) as unknown as Node2D
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
/**
|
|
62
|
-
* The numeric vertical axis
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* Each marker is positioned so its centre lands exactly on its value's fraction
|
|
68
|
-
* across `range` within the axis's filled height, so unevenly-spaced markers
|
|
69
|
-
* land where their values fall. Both the caption and the position are *bound*,
|
|
70
|
-
* not snapshotted: a chart that rescales its axes (see `LineChart.zoomTo`)
|
|
71
|
-
* tweens `range`/`markers` live. Only the marker **count** is fixed at
|
|
72
|
-
* construction.
|
|
73
|
-
*
|
|
74
|
-
* `enter` staggers them in **bottom-to-top**: each marker starts pushed a
|
|
75
|
-
* little to the right and transparent, then fades and slides left into place.
|
|
76
|
-
* The resting state is the landed one, so a chart that is never told to enter
|
|
77
|
-
* still reads as a finished chart.
|
|
87
|
+
* The numeric vertical axis the cartesian charts share: a column of {@link Text}
|
|
88
|
+
* markers, highest at the top, each centred on its value's fraction across
|
|
89
|
+
* `range` and right-aligned against the plot. Caption and position are bound, so
|
|
90
|
+
* a rescale tweens live; only the marker count is fixed at construction. The
|
|
91
|
+
* resting state is landed.
|
|
78
92
|
*/
|
|
79
93
|
export const VerticalAxis = component2d({
|
|
80
94
|
key: "verticalAxis",
|
|
81
95
|
state: {
|
|
82
96
|
range: fresh<Range>(() => [0, 1]),
|
|
97
|
+
/** Tick values in data units, each placed by its fraction across `range`; need not be even or include the ends. */
|
|
83
98
|
markers: fresh<number[]>(() => []),
|
|
99
|
+
/** One caption per marker in its order; a missing one reads as the bare numeric value. */
|
|
84
100
|
captions: fresh<string[]>(() => []),
|
|
85
101
|
axis: Types.custom<AxisConfig>(DEFAULT_Y_AXIS),
|
|
102
|
+
/** Rotated caption in the gutter left of the markers; empty builds no caption node and reserves no gutter. */
|
|
86
103
|
label: "",
|
|
87
104
|
arrivals: arrivalsEntry(),
|
|
88
105
|
},
|
|
89
|
-
// A
|
|
106
|
+
// A node made of markers would otherwise hug them.
|
|
90
107
|
defaults: { width: "fill", height: "fill" },
|
|
91
108
|
children: (state) => {
|
|
92
|
-
// Composed from plain entries (range/markers/label), so it's built once here.
|
|
93
|
-
const tickPad = insetsOps.resolve(state.axis.tickPadding).right
|
|
94
|
-
const titlePad = insetsOps.resolve(state.axis.titlePadding).right
|
|
95
|
-
|
|
96
|
-
const row: Node2D[] = []
|
|
97
|
-
// The row's parent is the axis: its box is what the markers are placed in.
|
|
98
|
-
const box = () => (row[0]?.parent as Node2D | null | undefined)?.layoutBounds ?? { width: 0, height: 0 }
|
|
99
|
-
/** The live tick values, top → bottom (highest first) — re-read every frame. */
|
|
100
109
|
const sorted = () => [...state.markers].sort((a, b) => b - a)
|
|
101
|
-
/** Tick `i`'s fraction across the live `range`, 0 at the bottom edge. */
|
|
102
110
|
const fracAt = (value: number | undefined) => {
|
|
103
111
|
if (value === undefined) return 0
|
|
104
112
|
const [min, max] = state.range
|
|
105
113
|
return max <= min ? 0 : (value - min) / (max - min)
|
|
106
114
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
...textOps.styleProps(state.axis.labelStyle),
|
|
114
|
-
text: () => {
|
|
115
|
+
// A slot with no value (the live list is shorter than the column built) renders empty, not its neighbour.
|
|
116
|
+
return asParts([
|
|
117
|
+
captionColumn({
|
|
118
|
+
count: state.markers.length,
|
|
119
|
+
axis: () => state.axis,
|
|
120
|
+
text: (i) => {
|
|
115
121
|
const value = sorted()[i]
|
|
116
122
|
return value === undefined ? "" : markerCaption(state, value)
|
|
117
123
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// headroom is smaller than its marker text is tall, which a theme with
|
|
124
|
-
// much larger markers can arrange. Only the top, because what lies
|
|
125
|
-
// below the axis is the chart's own horizontal axis and legend.
|
|
126
|
-
const limit = height / 2 - marker.layoutBounds.height / 2
|
|
127
|
-
return Math.min(centred, limit)
|
|
128
|
-
},
|
|
129
|
-
opacity: () => arrivalOf(state.arrivals, i).opacity,
|
|
130
|
-
x: () => arrivalOf(state.arrivals, i).shift,
|
|
131
|
-
})
|
|
132
|
-
return marker
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const children: Node2D[] = []
|
|
136
|
-
if (state.label !== "") {
|
|
137
|
-
// Node2D space is y-up and rotation is clockwise, so -90° reads bottom-to-top.
|
|
138
|
-
children.push(
|
|
139
|
-
new RotatedBox({
|
|
140
|
-
angle: -90,
|
|
141
|
-
width: "hug",
|
|
142
|
-
height: "hug",
|
|
143
|
-
children: new Text({
|
|
144
|
-
...textOps.styleProps(state.axis.titleStyle),
|
|
145
|
-
text: state.label,
|
|
146
|
-
opacity: () => arrivalOf(state.arrivals, "label").opacity,
|
|
147
|
-
x: () => arrivalOf(state.arrivals, "label").shift,
|
|
148
|
-
}),
|
|
149
|
-
})
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
children.push(new Rect({ height: "fill", width: "hug", padding: { right: tickPad }, children: markers }))
|
|
153
|
-
row.push(new Rect({ flow: "horizontal", gap: titlePad, children }))
|
|
154
|
-
return asParts(row)
|
|
124
|
+
place: (i, height) => (fracAt(sorted()[i]) - 0.5) * height,
|
|
125
|
+
arrivals: () => state.arrivals,
|
|
126
|
+
label: state.label,
|
|
127
|
+
}),
|
|
128
|
+
])
|
|
155
129
|
},
|
|
156
130
|
commands: {
|
|
157
131
|
/**
|
|
158
|
-
* Stagger
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* The timeline's duration is the **whole** entrance, not one marker's slide:
|
|
163
|
-
* the stagger and the caption are carved out of it rather than added on top.
|
|
164
|
-
* See {@link enterBudget} for why that distinction matters here.
|
|
165
|
-
*
|
|
166
|
-
* `stagger` is seconds between successive markers (derived when omitted);
|
|
167
|
-
* `slide` is the starting rightward offset in px.
|
|
132
|
+
* Stagger markers in bottom to top, then the caption. The duration is the
|
|
133
|
+
* whole entrance ({@link enterBudget}). `stagger` is seconds between markers
|
|
134
|
+
* (derived when omitted); `slide` is the starting rightward offset in px.
|
|
168
135
|
*/
|
|
169
136
|
enter: {
|
|
170
137
|
args: {
|
|
@@ -181,9 +148,8 @@ export const VerticalAxis = component2d({
|
|
|
181
148
|
if (count === 0) return arm
|
|
182
149
|
const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
|
|
183
150
|
const ease = frameEase(timeline)
|
|
184
|
-
//
|
|
151
|
+
// The bottom marker leads, so walk the top-to-bottom column in reverse.
|
|
185
152
|
const markers = state.markers.map((_, i) => ({ at: (count - 1 - i) * step, duration: per, piece: arrival(timeline, i, from, ease) }))
|
|
186
|
-
// Once every marker has landed, the caption slides in from the right.
|
|
187
153
|
const label = captioned ? [{ at: (count - 1) * step + per, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
|
|
188
154
|
return timeline.together([arm, beats(timeline, [...markers, ...label])])
|
|
189
155
|
},
|