@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.
Files changed (207) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +109 -28
  3. package/dist/bar-chart/bar-chart.d.ts +265 -39
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +219 -171
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +12 -7
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/category-axis.js +56 -42
  10. package/dist/bar-chart/category-axis.js.map +1 -1
  11. package/dist/bar-chart/chart-bars.d.ts +76 -52
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +105 -89
  14. package/dist/bar-chart/chart-bars.js.map +1 -1
  15. package/dist/bar-chart/index.d.ts +4 -4
  16. package/dist/bar-chart/index.d.ts.map +1 -1
  17. package/dist/bar-chart/index.js +2 -2
  18. package/dist/bar-chart/index.js.map +1 -1
  19. package/dist/bar-chart/shared.d.ts +41 -65
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +73 -117
  22. package/dist/bar-chart/shared.js.map +1 -1
  23. package/dist/browser/chunks/chunk-7XCRX6GE.js +2 -0
  24. package/dist/browser/chunks/chunk-7XCRX6GE.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 +5 -2
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +5 -2
  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 +51 -15
  38. package/dist/kit/cartesian.d.ts.map +1 -1
  39. package/dist/kit/cartesian.js +81 -29
  40. package/dist/kit/cartesian.js.map +1 -1
  41. package/dist/kit/chart-canvas.d.ts +32 -24
  42. package/dist/kit/chart-canvas.d.ts.map +1 -1
  43. package/dist/kit/chart-canvas.js +52 -46
  44. package/dist/kit/chart-canvas.js.map +1 -1
  45. package/dist/kit/chart-legend.d.ts +8 -4
  46. package/dist/kit/chart-legend.d.ts.map +1 -1
  47. package/dist/kit/chart-legend.js +30 -20
  48. package/dist/kit/chart-legend.js.map +1 -1
  49. package/dist/{line-chart → kit}/horizontal-axis.d.ts +31 -6
  50. package/dist/kit/horizontal-axis.d.ts.map +1 -0
  51. package/dist/{line-chart → kit}/horizontal-axis.js +23 -24
  52. package/dist/kit/horizontal-axis.js.map +1 -0
  53. package/dist/kit/motion.d.ts +5 -0
  54. package/dist/kit/motion.d.ts.map +1 -1
  55. package/dist/kit/motion.js +8 -0
  56. package/dist/kit/motion.js.map +1 -1
  57. package/dist/{pie-chart → kit}/pie-legend.d.ts +3 -3
  58. package/dist/kit/pie-legend.d.ts.map +1 -0
  59. package/dist/{pie-chart → kit}/pie-legend.js +11 -10
  60. package/dist/kit/pie-legend.js.map +1 -0
  61. package/dist/kit/polar.d.ts +31 -0
  62. package/dist/kit/polar.d.ts.map +1 -0
  63. package/dist/kit/polar.js +33 -0
  64. package/dist/kit/polar.js.map +1 -0
  65. package/dist/kit/shared.d.ts +11 -50
  66. package/dist/kit/shared.d.ts.map +1 -1
  67. package/dist/kit/shared.js +58 -141
  68. package/dist/kit/shared.js.map +1 -1
  69. package/dist/kit/style.d.ts +517 -0
  70. package/dist/kit/style.d.ts.map +1 -0
  71. package/dist/kit/style.js +292 -0
  72. package/dist/kit/style.js.map +1 -0
  73. package/dist/kit/vertical-axis.d.ts +22 -2
  74. package/dist/kit/vertical-axis.d.ts.map +1 -1
  75. package/dist/kit/vertical-axis.js +56 -43
  76. package/dist/kit/vertical-axis.js.map +1 -1
  77. package/dist/line-chart/chart-line.d.ts +95 -3
  78. package/dist/line-chart/chart-line.d.ts.map +1 -1
  79. package/dist/line-chart/chart-line.js +165 -53
  80. package/dist/line-chart/chart-line.js.map +1 -1
  81. package/dist/line-chart/chart-region.d.ts +1 -1
  82. package/dist/line-chart/chart-region.d.ts.map +1 -1
  83. package/dist/line-chart/chart-region.js +7 -8
  84. package/dist/line-chart/chart-region.js.map +1 -1
  85. package/dist/line-chart/index.d.ts +5 -6
  86. package/dist/line-chart/index.d.ts.map +1 -1
  87. package/dist/line-chart/index.js +3 -4
  88. package/dist/line-chart/index.js.map +1 -1
  89. package/dist/line-chart/line-chart.d.ts +281 -18
  90. package/dist/line-chart/line-chart.d.ts.map +1 -1
  91. package/dist/line-chart/line-chart.js +284 -134
  92. package/dist/line-chart/line-chart.js.map +1 -1
  93. package/dist/line-chart/shared.d.ts +51 -34
  94. package/dist/line-chart/shared.d.ts.map +1 -1
  95. package/dist/line-chart/shared.js +175 -83
  96. package/dist/line-chart/shared.js.map +1 -1
  97. package/dist/line-chart/x-scale.d.ts +2 -0
  98. package/dist/line-chart/x-scale.d.ts.map +1 -1
  99. package/dist/line-chart/x-scale.js +15 -0
  100. package/dist/line-chart/x-scale.js.map +1 -1
  101. package/dist/nodes.d.ts +3 -1
  102. package/dist/nodes.d.ts.map +1 -1
  103. package/dist/nodes.js +9 -1
  104. package/dist/nodes.js.map +1 -1
  105. package/dist/pie-chart/index.d.ts +2 -3
  106. package/dist/pie-chart/index.d.ts.map +1 -1
  107. package/dist/pie-chart/index.js +1 -2
  108. package/dist/pie-chart/index.js.map +1 -1
  109. package/dist/pie-chart/pie-chart.d.ts +114 -18
  110. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  111. package/dist/pie-chart/pie-chart.js +38 -24
  112. package/dist/pie-chart/pie-chart.js.map +1 -1
  113. package/dist/pie-chart/pie-ring.d.ts +16 -5
  114. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  115. package/dist/pie-chart/pie-ring.js +89 -55
  116. package/dist/pie-chart/pie-ring.js.map +1 -1
  117. package/dist/pie-chart/shared.d.ts +61 -62
  118. package/dist/pie-chart/shared.d.ts.map +1 -1
  119. package/dist/pie-chart/shared.js +44 -123
  120. package/dist/pie-chart/shared.js.map +1 -1
  121. package/dist/radar-chart/index.d.ts +5 -0
  122. package/dist/radar-chart/index.d.ts.map +1 -0
  123. package/dist/radar-chart/index.js +5 -0
  124. package/dist/radar-chart/index.js.map +1 -0
  125. package/dist/radar-chart/radar-chart.d.ts +212 -0
  126. package/dist/radar-chart/radar-chart.d.ts.map +1 -0
  127. package/dist/radar-chart/radar-chart.js +224 -0
  128. package/dist/radar-chart/radar-chart.js.map +1 -0
  129. package/dist/radar-chart/radar-grid.d.ts +49 -0
  130. package/dist/radar-chart/radar-grid.d.ts.map +1 -0
  131. package/dist/radar-chart/radar-grid.js +63 -0
  132. package/dist/radar-chart/radar-grid.js.map +1 -0
  133. package/dist/radar-chart/radar-series.d.ts +59 -0
  134. package/dist/radar-chart/radar-series.d.ts.map +1 -0
  135. package/dist/radar-chart/radar-series.js +71 -0
  136. package/dist/radar-chart/radar-series.js.map +1 -0
  137. package/dist/radar-chart/shared.d.ts +78 -0
  138. package/dist/radar-chart/shared.d.ts.map +1 -0
  139. package/dist/radar-chart/shared.js +129 -0
  140. package/dist/radar-chart/shared.js.map +1 -0
  141. package/dist/radial-chart/index.d.ts +6 -0
  142. package/dist/radial-chart/index.d.ts.map +1 -0
  143. package/dist/radial-chart/index.js +4 -0
  144. package/dist/radial-chart/index.js.map +1 -0
  145. package/dist/radial-chart/radial-chart.d.ts +210 -0
  146. package/dist/radial-chart/radial-chart.d.ts.map +1 -0
  147. package/dist/radial-chart/radial-chart.js +175 -0
  148. package/dist/radial-chart/radial-chart.js.map +1 -0
  149. package/dist/radial-chart/radial-rings.d.ts +121 -0
  150. package/dist/radial-chart/radial-rings.d.ts.map +1 -0
  151. package/dist/radial-chart/radial-rings.js +273 -0
  152. package/dist/radial-chart/radial-rings.js.map +1 -0
  153. package/dist/radial-chart/shared.d.ts +77 -0
  154. package/dist/radial-chart/shared.d.ts.map +1 -0
  155. package/dist/radial-chart/shared.js +78 -0
  156. package/dist/radial-chart/shared.js.map +1 -0
  157. package/package.json +4 -4
  158. package/registry.json +21 -6
  159. package/src/bar-chart/bar-chart.ts +263 -179
  160. package/src/bar-chart/category-axis.ts +46 -40
  161. package/src/bar-chart/chart-bars.ts +136 -117
  162. package/src/bar-chart/index.ts +7 -10
  163. package/src/bar-chart/shared.ts +88 -233
  164. package/src/engine.ts +5 -2
  165. package/src/index.ts +2 -0
  166. package/src/kit/cartesian.ts +121 -40
  167. package/src/kit/chart-canvas.ts +49 -54
  168. package/src/kit/chart-legend.ts +28 -20
  169. package/src/kit/horizontal-axis.ts +114 -0
  170. package/src/kit/motion.ts +7 -0
  171. package/src/{pie-chart → kit}/pie-legend.ts +12 -11
  172. package/src/kit/polar.ts +54 -0
  173. package/src/kit/shared.ts +55 -277
  174. package/src/kit/style.ts +353 -0
  175. package/src/kit/vertical-axis.ts +67 -48
  176. package/src/line-chart/chart-line.ts +176 -54
  177. package/src/line-chart/chart-region.ts +6 -10
  178. package/src/line-chart/index.ts +14 -10
  179. package/src/line-chart/line-chart.ts +353 -151
  180. package/src/line-chart/shared.ts +193 -174
  181. package/src/line-chart/x-scale.ts +13 -0
  182. package/src/nodes.ts +9 -1
  183. package/src/pie-chart/index.ts +2 -23
  184. package/src/pie-chart/pie-chart.ts +54 -35
  185. package/src/pie-chart/pie-ring.ts +92 -71
  186. package/src/pie-chart/shared.ts +80 -270
  187. package/src/radar-chart/index.ts +4 -0
  188. package/src/radar-chart/radar-chart.ts +290 -0
  189. package/src/radar-chart/radar-grid.ts +83 -0
  190. package/src/radar-chart/radar-series.ts +88 -0
  191. package/src/radar-chart/shared.ts +153 -0
  192. package/src/radial-chart/index.ts +5 -0
  193. package/src/radial-chart/radial-chart.ts +212 -0
  194. package/src/radial-chart/radial-rings.ts +328 -0
  195. package/src/radial-chart/shared.ts +126 -0
  196. package/dist/browser/chunks/chunk-4SMRCU24.js +0 -2
  197. package/dist/browser/chunks/chunk-4SMRCU24.js.map +0 -7
  198. package/dist/kit/chart-border.d.ts +0 -18
  199. package/dist/kit/chart-border.d.ts.map +0 -1
  200. package/dist/kit/chart-border.js +0 -23
  201. package/dist/kit/chart-border.js.map +0 -1
  202. package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
  203. package/dist/line-chart/horizontal-axis.js.map +0 -1
  204. package/dist/pie-chart/pie-legend.d.ts.map +0 -1
  205. package/dist/pie-chart/pie-legend.js.map +0 -1
  206. package/src/kit/chart-border.ts +0 -36
  207. package/src/line-chart/horizontal-axis.ts +0 -122
@@ -0,0 +1,353 @@
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. Every run of text a chart draws is its own
6
+ * `Types.typography` field, so axis labels, titles, values and the legend are
7
+ * each set the way a text node is.
8
+ */
9
+
10
+ import {
11
+ Types,
12
+ textOps,
13
+ type Fill,
14
+ type StrokeResolved,
15
+ type Stroke as _Stroke,
16
+ type StyleInput,
17
+ type TextStyle,
18
+ // Named so the emitted style types reach these through the sdk, not core.
19
+ type FillResolved as _FillResolved,
20
+ type Type as _Type,
21
+ } from "@motionscript/sdk"
22
+
23
+ import type { AxisConfig, ChartSeries, LegendConfig } from "./shared"
24
+
25
+ /** The style family every chart shares; the built-in presets are written for it. */
26
+ export const CHART_STYLE_FAMILY = "chart"
27
+
28
+ /** The out-of-the-box series fills, cycled by series (or row) index. */
29
+ export const CHART_PALETTE_DEFAULT = ["#4f8cff", "#2fbf8f", "#f5a524", "#e5547a", "#9b6cf0"] as const
30
+
31
+ /** The default ink of values, legend captions and centre text, on the dark stage. */
32
+ export const CHART_FOREGROUND = "#f4f4f5"
33
+ /** The default ink of axis markers, axis captions and spoke labels. */
34
+ export const CHART_MUTED = "#a1a1aa"
35
+
36
+ export const CHART_SERIES = {
37
+ /** A fill per series (a pie's or radial's per row), cycled; each may stack several paints. */
38
+ seriesFills: Types.palette(CHART_PALETTE_DEFAULT),
39
+ }
40
+
41
+ export const CHART_BACKGROUND = {
42
+ /** Behind the whole chart, legend and axes included; none by default, so the chart reads against the stage. */
43
+ background: Types.fill([]),
44
+ }
45
+
46
+ export const GRID_BACKGROUND = {
47
+ ...CHART_BACKGROUND,
48
+ /** Behind the plot only, under the grid lines; none by default. */
49
+ gridBackground: Types.fill([]),
50
+ }
51
+
52
+ /** The legend's captions, which every chart with a legend draws. */
53
+ export const LEGEND_TEXT = {
54
+ legendText: Types.typography({ fontSize: 28, fontWeight: 500, fill: CHART_FOREGROUND }),
55
+ }
56
+
57
+ export const CARTESIAN_TEXT = {
58
+ /** The markers under the plot: categories on a column chart, values on a horizontal bar chart. */
59
+ xLabelText: Types.typography({ fontSize: 28, fontWeight: 500, fill: CHART_MUTED }),
60
+ /** The caption under the plot. */
61
+ xTitleText: Types.typography({ fontSize: 31, fontWeight: 600, fill: CHART_MUTED }),
62
+ /** The markers beside the plot. */
63
+ yLabelText: Types.typography({ fontSize: 28, fontWeight: 500, fill: CHART_MUTED }),
64
+ /** The caption beside the plot, turned on its side. */
65
+ yTitleText: Types.typography({ fontSize: 31, fontWeight: 600, fill: CHART_MUTED }),
66
+ /** The figures printed by the marks. */
67
+ valueText: Types.typography({ fontSize: 24, fontWeight: 600, fill: CHART_FOREGROUND }),
68
+ ...LEGEND_TEXT,
69
+ }
70
+
71
+ export const CARTESIAN_GRID = {
72
+ /** The lines along the value ticks of a column chart (one per tick); empty draws none. */
73
+ horizontalGrid: Types.stroke({ weight: 2, fill: "white/15" }),
74
+ /** The lines under the category markers; empty draws none. */
75
+ verticalGrid: Types.stroke([]),
76
+ /** The baseline and the value-axis rule; empty draws none. */
77
+ axisStroke: Types.stroke([]),
78
+ /** Read as the chart is built: whether the category (x) axis row is drawn. */
79
+ showXAxis: Types.boolean(true, { animatable: false }),
80
+ /** Read as the chart is built: whether the value (y) axis column is drawn. */
81
+ showYAxis: Types.boolean(true, { animatable: false }),
82
+ }
83
+
84
+ export type LegendPosition = "bottom" | "top" | "right" | "none"
85
+ export type LegendSwatch = "square" | "circle" | "line"
86
+
87
+ export const CHART_LEGEND = {
88
+ /** Read as the chart is built: where the legend sits, or `none`. */
89
+ legend: Types.oneOf(["bottom", "top", "right", "none"] as const, { animatable: false }),
90
+ swatch: Types.oneOf(["square", "circle", "line"] as const),
91
+ }
92
+
93
+ /** The fields every chart's style resolves, whichever groups it spreads. */
94
+ export interface ChartStyleBase {
95
+ readonly seriesFills: readonly Fill[]
96
+ readonly background: Fill
97
+ readonly legendText: TextStyle
98
+ readonly legend: LegendPosition
99
+ readonly swatch: LegendSwatch
100
+ }
101
+
102
+ /** The cartesian fields, for the line and bar charts. */
103
+ export interface CartesianStyle extends ChartStyleBase {
104
+ readonly gridBackground: Fill
105
+ readonly xLabelText: TextStyle
106
+ readonly xTitleText: TextStyle
107
+ readonly yLabelText: TextStyle
108
+ readonly yTitleText: TextStyle
109
+ readonly valueText: TextStyle
110
+ readonly horizontalGrid: StrokeResolved[]
111
+ readonly verticalGrid: StrokeResolved[]
112
+ readonly axisStroke: StrokeResolved[]
113
+ readonly showXAxis: boolean
114
+ readonly showYAxis: boolean
115
+ }
116
+
117
+ /** The fields a preset restates so its rules and text read on its background (a chart keeps those it draws). */
118
+ const _PRESET_FIELDS = {
119
+ ...CHART_SERIES,
120
+ ...GRID_BACKGROUND,
121
+ ...CARTESIAN_TEXT,
122
+ labelText: Types.typography(),
123
+ centerText: Types.typography(),
124
+ spokeText: Types.typography(),
125
+ horizontalGrid: CARTESIAN_GRID.horizontalGrid,
126
+ gridStroke: Types.stroke([]),
127
+ spokeStroke: Types.stroke([]),
128
+ track: Types.fill([]),
129
+ }
130
+
131
+ /** A preset's fields: any chart's, so one preset dresses every chart. */
132
+ export type ChartStylePreset = StyleInput<typeof _PRESET_FIELDS>
133
+
134
+ /** Every text field's ink: `foreground` for values, the legend and centre text, `muted` for markers and captions. */
135
+ function inks(foreground: string, muted: string) {
136
+ const on = (fill: string) => ({ fill })
137
+ return {
138
+ xLabelText: on(muted),
139
+ xTitleText: on(muted),
140
+ yLabelText: on(muted),
141
+ yTitleText: on(muted),
142
+ valueText: on(foreground),
143
+ legendText: on(foreground),
144
+ labelText: on(muted),
145
+ centerText: on(foreground),
146
+ spokeText: on(muted),
147
+ } satisfies ChartStylePreset
148
+ }
149
+
150
+ /** Every rule a chart draws, and a radial's track, in one paint. */
151
+ function rules(fill: string) {
152
+ return {
153
+ horizontalGrid: { weight: 2, fill },
154
+ gridStroke: { weight: 2, fill },
155
+ spokeStroke: { weight: 2, fill },
156
+ track: fill,
157
+ } satisfies ChartStylePreset
158
+ }
159
+
160
+ /** Ready-made looks, each stating every colour so applying one replaces the last. */
161
+ export const CHART_STYLE_PRESETS = {
162
+ midnight: {
163
+ seriesFills: [...CHART_PALETTE_DEFAULT],
164
+ ...inks(CHART_FOREGROUND, CHART_MUTED),
165
+ ...rules("white/15"),
166
+ track: "white/10",
167
+ background: [],
168
+ gridBackground: [],
169
+ },
170
+ paper: {
171
+ seriesFills: ["#2563eb", "#059669", "#d97706", "#db2777", "#7c3aed"],
172
+ ...inks("#1c1917", "#57534e"),
173
+ ...rules("#1c1917/12"),
174
+ background: "#fafaf9",
175
+ gridBackground: [],
176
+ },
177
+ ember: {
178
+ seriesFills: ["#d9603b", "#8194ad", "#e0a93b", "#5e9d6b", "#9ec85a"],
179
+ ...inks("#e9e1d4", "#8a7f70"),
180
+ ...rules("#2c2820"),
181
+ background: "#15130e",
182
+ gridBackground: [],
183
+ },
184
+ neon: {
185
+ seriesFills: ["#22d3ee", "#a3e635", "#f472b6", "#facc15", "#818cf8"],
186
+ ...inks("#ffffff", "#94a3b8"),
187
+ ...rules("white/10"),
188
+ background: [],
189
+ gridBackground: [],
190
+ },
191
+ ocean: {
192
+ seriesFills: ["#0ea5e9", "#14b8a6", "#6366f1", "#38bdf8", "#2dd4bf"],
193
+ ...inks("#e0f2fe", "#7dd3fc"),
194
+ ...rules("#7dd3fc/20"),
195
+ background: "#082f49",
196
+ gridBackground: [],
197
+ },
198
+ sunset: {
199
+ seriesFills: ["#f97316", "#ef4444", "#ec4899", "#f59e0b", "#a855f7"],
200
+ ...inks("#fff7ed", "#fdba74"),
201
+ ...rules("#fdba74/20"),
202
+ background: [],
203
+ gridBackground: [],
204
+ },
205
+ pastel: {
206
+ seriesFills: ["#a5b4fc", "#f9a8d4", "#86efac", "#fde68a", "#7dd3fc"],
207
+ ...inks("#f8fafc", "#cbd5e1"),
208
+ ...rules("white/12"),
209
+ background: [],
210
+ gridBackground: [],
211
+ },
212
+ mono: {
213
+ seriesFills: ["#fafafa", "#a3a3a3", "#737373", "#d4d4d4", "#525252"],
214
+ ...inks("#fafafa", "#a3a3a3"),
215
+ ...rules("white/12"),
216
+ background: [],
217
+ gridBackground: [],
218
+ },
219
+ } satisfies Record<string, ChartStylePreset>
220
+
221
+ /** Series `index`'s fill: its own `fill`, else the style's series fills, cycled. */
222
+ export function paletteFill(style: Pick<ChartStyleBase, "seriesFills">, index: number, own?: Fill): Fill {
223
+ if (own != null) return own
224
+ const fills = style.seriesFills
225
+ return fills.length > 0 ? fills[index % fills.length] : "white"
226
+ }
227
+
228
+ /** {@link paletteFill} for a series. */
229
+ export function seriesFill(style: Pick<ChartStyleBase, "seriesFills">, series: ChartSeries, index: number): Fill {
230
+ return paletteFill(style, index, series.fill)
231
+ }
232
+
233
+ /** A text style's size in px; an autofit or unset size reads as `fallback`. */
234
+ export function sizeOf(text: TextStyle | undefined, fallback = 28): number {
235
+ const size = text?.fontSize
236
+ return typeof size === "number" && Number.isFinite(size) ? size : fallback
237
+ }
238
+
239
+ /**
240
+ * `text` as a text node takes it, aligned where the chart puts it, with `over`
241
+ * laid on top. Unset keys are dropped: an `undefined` spread onto a node is a
242
+ * value, and an unset face must stay unset so the host's default applies.
243
+ */
244
+ export function placedText(text: TextStyle, textAlign: TextStyle["textAlign"] = "center", over?: TextStyle): TextStyle {
245
+ return textOps.styleProps({ ...textOps.mergeStyle(text, over), textAlign }) as TextStyle
246
+ }
247
+
248
+ /** Axis markers on `axis`, aligned as that axis sets them. */
249
+ export function markerText(style: Pick<CartesianStyle, "xLabelText" | "yLabelText">, axis: "x" | "y"): TextStyle {
250
+ return placedText(axis === "x" ? style.xLabelText : style.yLabelText, axis === "x" ? "center" : "right")
251
+ }
252
+
253
+ /** The figures printed by the marks. */
254
+ export function valueText(style: Pick<CartesianStyle, "valueText">, textAlign: TextStyle["textAlign"] = "center"): TextStyle {
255
+ return placedText(style.valueText, textAlign)
256
+ }
257
+
258
+ /**
259
+ * The kit axis parts' config from a style. `x` is the axis under the plot;
260
+ * its grid lines are vertical (one per x marker), the `y` axis's horizontal.
261
+ */
262
+ export function axisConfigOf(
263
+ style: Pick<CartesianStyle, "verticalGrid" | "horizontalGrid" | "xLabelText" | "xTitleText" | "yLabelText" | "yTitleText">,
264
+ axis: "x" | "y"
265
+ ): AxisConfig {
266
+ const gap = Math.round(sizeOf(axis === "x" ? style.xLabelText : style.yLabelText) * 0.6)
267
+ return {
268
+ labelStyle: markerText(style, axis),
269
+ titleStyle: placedText(axis === "x" ? style.xTitleText : style.yTitleText),
270
+ lineStyle: axis === "x" ? style.verticalGrid : style.horizontalGrid,
271
+ tickPadding: axis === "x" ? { top: gap } : { right: gap },
272
+ titlePadding: axis === "x" ? { top: Math.round(gap / 2) } : { right: gap },
273
+ }
274
+ }
275
+
276
+ /** The kit legend's config: captions in `legendText`, the swatch shaped by `swatch` and sized off the captions. */
277
+ export function legendConfigOf(style: Pick<ChartStyleBase, "swatch" | "legendText">): LegendConfig {
278
+ const type = sizeOf(style.legendText)
279
+ const size = Math.max(8, Math.round(type * 0.6))
280
+ const marker =
281
+ style.swatch === "line"
282
+ ? { width: Math.round(size * 2), height: Math.max(3, Math.round(size / 4)), borderRadius: size }
283
+ : style.swatch === "circle"
284
+ ? { width: size, height: size, borderRadius: size / 2 }
285
+ : { width: size, height: size, borderRadius: Math.round(size / 4) }
286
+ return {
287
+ padding: 0,
288
+ textStyle: placedText(style.legendText, "left"),
289
+ itemGap: Math.round(type * 1.4),
290
+ markerStyle: { ...marker, stroke: [], shadow: [] },
291
+ }
292
+ }
293
+
294
+ /** Which text a chart drew from the retired shared text fields. */
295
+ export type ChartTextLayout = "cartesian" | "pie" | "radar" | "radial"
296
+
297
+ /**
298
+ * The `style` entry's hook for documents written before every run of text got
299
+ * its own field (0.1.1-alpha.1): `barRadius` became `barCornerRadius`, `palette` became `seriesFills`; `foreground`,
300
+ * `muted` and the one `fontFamily`/`fontSize`/`fontWeight` became the text
301
+ * fields as each chart derived them; a cartesian or radar `background`, which
302
+ * painted the plot, became `gridBackground`. A current style comes back as is.
303
+ */
304
+ export function migrateChartStyle(layout: ChartTextLayout): (value: unknown) => unknown {
305
+ return (value) => {
306
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return value
307
+ const old = value as Record<string, unknown>
308
+ const retired = ["palette", "foreground", "muted", "fontFamily", "fontSize", "fontWeight", "barRadius"]
309
+ const plotBackground = layout === "cartesian" || layout === "radar"
310
+ if (!retired.some((key) => key in old) && !(plotBackground && "background" in old && !("gridBackground" in old))) return value
311
+
312
+ const { palette, foreground, muted, fontFamily, fontSize, fontWeight, barRadius, ...rest } = old
313
+ const out: Record<string, unknown> = { ...rest }
314
+ // A bar radius rounded only the far end; its replacement states every corner.
315
+ if (typeof barRadius === "number" && out.barCornerRadius === undefined) {
316
+ out.barCornerRadius = { topLeft: barRadius, topRight: barRadius, bottomRight: 0, bottomLeft: 0 }
317
+ }
318
+ if (palette !== undefined && out.seriesFills === undefined) out.seriesFills = palette
319
+ if (plotBackground && "background" in old && !("gridBackground" in old)) {
320
+ out.gridBackground = old.background
321
+ delete out.background
322
+ }
323
+ const size = typeof fontSize === "number" ? fontSize : undefined
324
+ const weight = typeof fontWeight === "number" ? fontWeight : undefined
325
+ const text = (ink: unknown, scale = 1, heavier = 0): Record<string, unknown> | undefined => {
326
+ const t: Record<string, unknown> = {}
327
+ if (typeof fontFamily === "string" && fontFamily !== "") t.fontFamily = fontFamily
328
+ if (size !== undefined) t.fontSize = Math.round(size * scale)
329
+ if (weight !== undefined) t.fontWeight = Math.min(900, weight + heavier)
330
+ if (ink !== undefined) t.fill = ink
331
+ return Object.keys(t).length > 0 ? t : undefined
332
+ }
333
+ const fields: Record<string, Record<string, unknown> | undefined> = { legendText: text(foreground) }
334
+ if (layout === "cartesian") {
335
+ fields.xLabelText = text(muted)
336
+ fields.yLabelText = text(muted)
337
+ fields.xTitleText = text(muted, 1.1, 100)
338
+ fields.yTitleText = text(muted, 1.1, 100)
339
+ fields.valueText = text(foreground, 0.85, 100)
340
+ } else if (layout === "pie") {
341
+ fields.labelText = text(muted)
342
+ fields.valueText = text(foreground, 1, 200)
343
+ fields.centerText = text(foreground, 1.8, 300)
344
+ } else if (layout === "radar") {
345
+ fields.spokeText = text(muted)
346
+ } else {
347
+ fields.labelText = text(muted)
348
+ fields.centerText = text(foreground, 1.8, 300)
349
+ }
350
+ for (const [key, field] of Object.entries(fields)) if (field && out[key] === undefined) out[key] = field
351
+ return out
352
+ }
353
+ }
@@ -5,7 +5,6 @@ import {
5
5
  Types,
6
6
  component2d,
7
7
  insetsOps,
8
- textOps,
9
8
  type NodeOf,
10
9
  // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
11
10
  type Node2D,
@@ -13,6 +12,7 @@ import {
13
12
  } from "@motionscript/sdk"
14
13
  import { enterBudget } from "@motionscript/sdk/component"
15
14
 
15
+ import { boundText } from "./cartesian"
16
16
  import { asParts, fresh } from "./entries"
17
17
  import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "./motion"
18
18
  import {
@@ -30,11 +30,65 @@ export function markerCaption(s: { readonly markers: readonly number[]; readonly
30
30
  return (index >= 0 ? s.captions[index] : undefined) ?? formatMarker(value)
31
31
  }
32
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
+
33
86
  /**
34
87
  * The numeric vertical axis the cartesian charts share: a column of {@link Text}
35
88
  * markers, highest at the top, each centred on its value's fraction across
36
- * `range`. Caption and position are bound, so a rescale tweens live; only the
37
- * marker count is fixed at construction. The resting state is landed.
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.
38
92
  */
39
93
  export const VerticalAxis = component2d({
40
94
  key: "verticalAxis",
@@ -52,61 +106,26 @@ export const VerticalAxis = component2d({
52
106
  // A node made of markers would otherwise hug them.
53
107
  defaults: { width: "fill", height: "fill" },
54
108
  children: (state) => {
55
- const tickPad = insetsOps.resolve(state.axis.tickPadding).right
56
- const titlePad = insetsOps.resolve(state.axis.titlePadding).right
57
-
58
- const row: Node2D[] = []
59
- // The row's parent is the axis, whose box the markers are placed in.
60
- const box = () => (row[0]?.parent as Node2D | null | undefined)?.layoutBounds ?? { width: 0, height: 0 }
61
109
  const sorted = () => [...state.markers].sort((a, b) => b - a)
62
110
  const fracAt = (value: number | undefined) => {
63
111
  if (value === undefined) return 0
64
112
  const [min, max] = state.range
65
113
  return max <= min ? 0 : (value - min) / (max - min)
66
114
  }
67
-
68
115
  // A slot with no value (the live list is shorter than the column built) renders empty, not its neighbour.
69
- const markers = state.markers.map((_, i) => {
70
- const marker: Node2D = new Text({
71
- ...textOps.styleProps(state.axis.labelStyle),
72
- text: () => {
116
+ return asParts([
117
+ captionColumn({
118
+ count: state.markers.length,
119
+ axis: () => state.axis,
120
+ text: (i) => {
73
121
  const value = sorted()[i]
74
122
  return value === undefined ? "" : markerCaption(state, value)
75
123
  },
76
- y: () => {
77
- // Node2D space is y-up, so the fraction runs from the bottom.
78
- const height = box().height
79
- const centred = (fracAt(sorted()[i]) - 0.5) * height
80
- // Held inside the top edge for a theme whose markers are taller than the headroom; only the top, since the chart's own axis and legend lie below.
81
- const limit = height / 2 - marker.layoutBounds.height / 2
82
- return Math.min(centred, limit)
83
- },
84
- opacity: () => arrivalOf(state.arrivals, i).opacity,
85
- x: () => arrivalOf(state.arrivals, i).shift,
86
- })
87
- return marker
88
- })
89
-
90
- const children: Node2D[] = []
91
- if (state.label !== "") {
92
- // Node2D space is y-up and rotation is clockwise, so -90° reads bottom-to-top.
93
- children.push(
94
- new RotatedBox({
95
- angle: -90,
96
- width: "hug",
97
- height: "hug",
98
- children: new Text({
99
- ...textOps.styleProps(state.axis.titleStyle),
100
- text: state.label,
101
- opacity: () => arrivalOf(state.arrivals, "label").opacity,
102
- x: () => arrivalOf(state.arrivals, "label").shift,
103
- }),
104
- })
105
- )
106
- }
107
- children.push(new Rect({ height: "fill", width: "hug", padding: { right: tickPad }, children: markers }))
108
- row.push(new Rect({ flow: "horizontal", gap: titlePad, children }))
109
- return asParts(row)
124
+ place: (i, height) => (fracAt(sorted()[i]) - 0.5) * height,
125
+ arrivals: () => state.arrivals,
126
+ label: state.label,
127
+ }),
128
+ ])
110
129
  },
111
130
  commands: {
112
131
  /**