@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.
Files changed (215) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +98 -28
  3. package/dist/bar-chart/bar-chart.d.ts +242 -119
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +231 -260
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +16 -38
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/category-axis.js +59 -68
  10. package/dist/bar-chart/category-axis.js.map +1 -1
  11. package/dist/bar-chart/chart-bars.d.ts +60 -91
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +88 -125
  14. package/dist/bar-chart/chart-bars.js.map +1 -1
  15. package/dist/bar-chart/index.d.ts +5 -13
  16. package/dist/bar-chart/index.d.ts.map +1 -1
  17. package/dist/bar-chart/index.js +2 -9
  18. package/dist/bar-chart/index.js.map +1 -1
  19. package/dist/bar-chart/shared.d.ts +40 -132
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +70 -158
  22. package/dist/bar-chart/shared.js.map +1 -1
  23. package/dist/browser/chunks/chunk-R3Q5EE6E.js +2 -0
  24. package/dist/browser/chunks/chunk-R3Q5EE6E.js.map +7 -0
  25. package/dist/browser/index.js +1 -1
  26. package/dist/browser/index.js.map +4 -4
  27. package/dist/browser/kit.js +1 -1
  28. package/dist/browser/manifest.json +1 -1
  29. package/dist/engine.d.ts +8 -17
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +8 -17
  32. package/dist/engine.js.map +1 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/kit/cartesian.d.ts +49 -35
  38. package/dist/kit/cartesian.d.ts.map +1 -1
  39. package/dist/kit/cartesian.js +80 -42
  40. package/dist/kit/cartesian.js.map +1 -1
  41. package/dist/kit/chart-canvas.d.ts +37 -40
  42. package/dist/kit/chart-canvas.d.ts.map +1 -1
  43. package/dist/kit/chart-canvas.js +53 -47
  44. package/dist/kit/chart-canvas.js.map +1 -1
  45. package/dist/kit/chart-legend.d.ts +16 -59
  46. package/dist/kit/chart-legend.d.ts.map +1 -1
  47. package/dist/kit/chart-legend.js +38 -76
  48. package/dist/kit/chart-legend.js.map +1 -1
  49. package/dist/kit/entries.d.ts +2 -0
  50. package/dist/kit/entries.d.ts.map +1 -1
  51. package/dist/kit/entries.js +2 -0
  52. package/dist/kit/entries.js.map +1 -1
  53. package/dist/kit/horizontal-axis.d.ts +77 -0
  54. package/dist/kit/horizontal-axis.d.ts.map +1 -0
  55. package/dist/kit/horizontal-axis.js +99 -0
  56. package/dist/kit/horizontal-axis.js.map +1 -0
  57. package/dist/kit/motion.d.ts +16 -3
  58. package/dist/kit/motion.d.ts.map +1 -1
  59. package/dist/kit/motion.js +15 -1
  60. package/dist/kit/motion.js.map +1 -1
  61. package/dist/{pie-chart → kit}/pie-legend.d.ts +14 -29
  62. package/dist/kit/pie-legend.d.ts.map +1 -0
  63. package/dist/kit/pie-legend.js +93 -0
  64. package/dist/kit/pie-legend.js.map +1 -0
  65. package/dist/kit/polar.d.ts +31 -0
  66. package/dist/kit/polar.d.ts.map +1 -0
  67. package/dist/kit/polar.js +33 -0
  68. package/dist/kit/polar.js.map +1 -0
  69. package/dist/kit/shared.d.ts +60 -214
  70. package/dist/kit/shared.d.ts.map +1 -1
  71. package/dist/kit/shared.js +91 -291
  72. package/dist/kit/shared.js.map +1 -1
  73. package/dist/kit/style.d.ts +254 -0
  74. package/dist/kit/style.d.ts.map +1 -0
  75. package/dist/kit/style.js +197 -0
  76. package/dist/kit/style.js.map +1 -0
  77. package/dist/kit/vertical-axis.d.ts +36 -45
  78. package/dist/kit/vertical-axis.d.ts.map +1 -1
  79. package/dist/kit/vertical-axis.js +69 -81
  80. package/dist/kit/vertical-axis.js.map +1 -1
  81. package/dist/line-chart/chart-line.d.ts +107 -40
  82. package/dist/line-chart/chart-line.d.ts.map +1 -1
  83. package/dist/line-chart/chart-line.js +172 -89
  84. package/dist/line-chart/chart-line.js.map +1 -1
  85. package/dist/line-chart/chart-region.d.ts +14 -27
  86. package/dist/line-chart/chart-region.d.ts.map +1 -1
  87. package/dist/line-chart/chart-region.js +13 -28
  88. package/dist/line-chart/chart-region.js.map +1 -1
  89. package/dist/line-chart/index.d.ts +5 -18
  90. package/dist/line-chart/index.d.ts.map +1 -1
  91. package/dist/line-chart/index.js +3 -16
  92. package/dist/line-chart/index.js.map +1 -1
  93. package/dist/line-chart/line-chart.d.ts +272 -119
  94. package/dist/line-chart/line-chart.d.ts.map +1 -1
  95. package/dist/line-chart/line-chart.js +309 -257
  96. package/dist/line-chart/line-chart.js.map +1 -1
  97. package/dist/line-chart/shared.d.ts +73 -130
  98. package/dist/line-chart/shared.d.ts.map +1 -1
  99. package/dist/line-chart/shared.js +195 -156
  100. package/dist/line-chart/shared.js.map +1 -1
  101. package/dist/line-chart/x-scale.d.ts +16 -55
  102. package/dist/line-chart/x-scale.d.ts.map +1 -1
  103. package/dist/line-chart/x-scale.js +44 -111
  104. package/dist/line-chart/x-scale.js.map +1 -1
  105. package/dist/nodes.d.ts +3 -13
  106. package/dist/nodes.d.ts.map +1 -1
  107. package/dist/nodes.js +9 -13
  108. package/dist/nodes.js.map +1 -1
  109. package/dist/pie-chart/index.d.ts +3 -15
  110. package/dist/pie-chart/index.d.ts.map +1 -1
  111. package/dist/pie-chart/index.js +1 -11
  112. package/dist/pie-chart/index.js.map +1 -1
  113. package/dist/pie-chart/pie-chart.d.ts +147 -85
  114. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  115. package/dist/pie-chart/pie-chart.js +65 -86
  116. package/dist/pie-chart/pie-chart.js.map +1 -1
  117. package/dist/pie-chart/pie-ring.d.ts +65 -196
  118. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  119. package/dist/pie-chart/pie-ring.js +154 -292
  120. package/dist/pie-chart/pie-ring.js.map +1 -1
  121. package/dist/pie-chart/shared.d.ts +78 -172
  122. package/dist/pie-chart/shared.d.ts.map +1 -1
  123. package/dist/pie-chart/shared.js +54 -177
  124. package/dist/pie-chart/shared.js.map +1 -1
  125. package/dist/radar-chart/index.d.ts +5 -0
  126. package/dist/radar-chart/index.d.ts.map +1 -0
  127. package/dist/radar-chart/index.js +5 -0
  128. package/dist/radar-chart/index.js.map +1 -0
  129. package/dist/radar-chart/radar-chart.d.ts +218 -0
  130. package/dist/radar-chart/radar-chart.d.ts.map +1 -0
  131. package/dist/radar-chart/radar-chart.js +224 -0
  132. package/dist/radar-chart/radar-chart.js.map +1 -0
  133. package/dist/radar-chart/radar-grid.d.ts +43 -0
  134. package/dist/radar-chart/radar-grid.d.ts.map +1 -0
  135. package/dist/radar-chart/radar-grid.js +61 -0
  136. package/dist/radar-chart/radar-grid.js.map +1 -0
  137. package/dist/radar-chart/radar-series.d.ts +59 -0
  138. package/dist/radar-chart/radar-series.d.ts.map +1 -0
  139. package/dist/radar-chart/radar-series.js +71 -0
  140. package/dist/radar-chart/radar-series.js.map +1 -0
  141. package/dist/radar-chart/shared.d.ts +73 -0
  142. package/dist/radar-chart/shared.d.ts.map +1 -0
  143. package/dist/radar-chart/shared.js +123 -0
  144. package/dist/radar-chart/shared.js.map +1 -0
  145. package/dist/radial-chart/index.d.ts +6 -0
  146. package/dist/radial-chart/index.d.ts.map +1 -0
  147. package/dist/radial-chart/index.js +4 -0
  148. package/dist/radial-chart/index.js.map +1 -0
  149. package/dist/radial-chart/radial-chart.d.ts +216 -0
  150. package/dist/radial-chart/radial-chart.d.ts.map +1 -0
  151. package/dist/radial-chart/radial-chart.js +174 -0
  152. package/dist/radial-chart/radial-chart.js.map +1 -0
  153. package/dist/radial-chart/radial-rings.d.ts +121 -0
  154. package/dist/radial-chart/radial-rings.d.ts.map +1 -0
  155. package/dist/radial-chart/radial-rings.js +273 -0
  156. package/dist/radial-chart/radial-rings.js.map +1 -0
  157. package/dist/radial-chart/shared.d.ts +73 -0
  158. package/dist/radial-chart/shared.d.ts.map +1 -0
  159. package/dist/radial-chart/shared.js +79 -0
  160. package/dist/radial-chart/shared.js.map +1 -0
  161. package/package.json +4 -4
  162. package/registry.json +21 -6
  163. package/src/bar-chart/bar-chart.ts +274 -315
  164. package/src/bar-chart/category-axis.ts +53 -88
  165. package/src/bar-chart/chart-bars.ts +108 -205
  166. package/src/bar-chart/index.ts +8 -19
  167. package/src/bar-chart/shared.ts +81 -309
  168. package/src/engine.ts +8 -17
  169. package/src/index.ts +2 -0
  170. package/src/kit/cartesian.ts +118 -55
  171. package/src/kit/chart-canvas.ts +50 -80
  172. package/src/kit/chart-legend.ts +36 -97
  173. package/src/kit/entries.ts +4 -1
  174. package/src/kit/horizontal-axis.ts +114 -0
  175. package/src/kit/motion.ts +18 -2
  176. package/src/{pie-chart → kit}/pie-legend.ts +25 -65
  177. package/src/kit/polar.ts +54 -0
  178. package/src/kit/shared.ts +104 -487
  179. package/src/kit/style.ts +257 -0
  180. package/src/kit/vertical-axis.ts +81 -115
  181. package/src/line-chart/chart-line.ts +185 -91
  182. package/src/line-chart/chart-region.ts +14 -31
  183. package/src/line-chart/index.ts +14 -22
  184. package/src/line-chart/line-chart.ts +377 -315
  185. package/src/line-chart/shared.ts +229 -304
  186. package/src/line-chart/x-scale.ts +44 -121
  187. package/src/nodes.ts +9 -13
  188. package/src/pie-chart/index.ts +3 -35
  189. package/src/pie-chart/pie-chart.ts +83 -134
  190. package/src/pie-chart/pie-ring.ts +162 -368
  191. package/src/pie-chart/shared.ts +92 -379
  192. package/src/radar-chart/index.ts +4 -0
  193. package/src/radar-chart/radar-chart.ts +287 -0
  194. package/src/radar-chart/radar-grid.ts +78 -0
  195. package/src/radar-chart/radar-series.ts +88 -0
  196. package/src/radar-chart/shared.ts +146 -0
  197. package/src/radial-chart/index.ts +5 -0
  198. package/src/radial-chart/radial-chart.ts +210 -0
  199. package/src/radial-chart/radial-rings.ts +328 -0
  200. package/src/radial-chart/shared.ts +114 -0
  201. package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
  202. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
  203. package/dist/kit/chart-border.d.ts +0 -27
  204. package/dist/kit/chart-border.d.ts.map +0 -1
  205. package/dist/kit/chart-border.js +0 -28
  206. package/dist/kit/chart-border.js.map +0 -1
  207. package/dist/line-chart/horizontal-axis.d.ts +0 -79
  208. package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
  209. package/dist/line-chart/horizontal-axis.js +0 -129
  210. package/dist/line-chart/horizontal-axis.js.map +0 -1
  211. package/dist/pie-chart/pie-legend.d.ts.map +0 -1
  212. package/dist/pie-chart/pie-legend.js +0 -117
  213. package/dist/pie-chart/pie-legend.js.map +0 -1
  214. package/src/kit/chart-border.ts +0 -50
  215. package/src/line-chart/horizontal-axis.ts +0 -151
@@ -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
+ }
@@ -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 shared by the cartesian charts: a **column of
63
- * {@link Text} nodes**, one per marker value, running top-to-bottom (highest
64
- * value at the top). Pure layout — no `Graphics` — so the markers are real text
65
- * nodes the scene can measure, restyle, or animate individually.
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 plain node filled its parent; one made of its markers would hug them.
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
- // One text node per marker, ordered top → bottom. A slot with no value (the
109
- // live list is shorter than the column built for it) renders as empty text
110
- // rather than repeating its neighbour.
111
- const markers = state.markers.map((_, i) => {
112
- const marker: Node2D = new Text({
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
- y: () => {
119
- // Node2D space is y-up, so the fraction runs from the bottom.
120
- const height = box().height
121
- const centred = (fracAt(sorted()[i]) - 0.5) * height
122
- // Held inside the axis's top edge — the backstop for a chart whose
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 the markers in bottom to top, then slide the caption in from the
159
- * right after them. Its first frame arms them all: shifted right by `slide`
160
- * and transparent.
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
- // Bottom marker leads: the column is top → bottom, so walk it in reverse.
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
  },