@motionscript/charts 0.1.1-alpha.1 → 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 (81) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +19 -8
  3. package/dist/bar-chart/bar-chart.d.ts +97 -65
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +9 -5
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/chart-bars.d.ts +29 -5
  8. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  9. package/dist/bar-chart/chart-bars.js +33 -8
  10. package/dist/bar-chart/chart-bars.js.map +1 -1
  11. package/dist/bar-chart/shared.d.ts +9 -4
  12. package/dist/bar-chart/shared.d.ts.map +1 -1
  13. package/dist/bar-chart/shared.js +10 -5
  14. package/dist/bar-chart/shared.js.map +1 -1
  15. package/dist/browser/chunks/chunk-7XCRX6GE.js +2 -0
  16. package/dist/browser/chunks/chunk-7XCRX6GE.js.map +7 -0
  17. package/dist/browser/index.js +1 -1
  18. package/dist/browser/index.js.map +3 -3
  19. package/dist/browser/kit.js +1 -1
  20. package/dist/browser/manifest.json +1 -1
  21. package/dist/kit/cartesian.d.ts +6 -4
  22. package/dist/kit/cartesian.d.ts.map +1 -1
  23. package/dist/kit/cartesian.js +5 -3
  24. package/dist/kit/cartesian.js.map +1 -1
  25. package/dist/kit/style.d.ts +339 -76
  26. package/dist/kit/style.d.ts.map +1 -1
  27. package/dist/kit/style.js +196 -101
  28. package/dist/kit/style.js.map +1 -1
  29. package/dist/line-chart/line-chart.d.ts +81 -57
  30. package/dist/line-chart/line-chart.d.ts.map +1 -1
  31. package/dist/line-chart/line-chart.js +9 -7
  32. package/dist/line-chart/line-chart.js.map +1 -1
  33. package/dist/pie-chart/pie-chart.d.ts +43 -43
  34. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  35. package/dist/pie-chart/pie-chart.js +2 -1
  36. package/dist/pie-chart/pie-chart.js.map +1 -1
  37. package/dist/pie-chart/shared.d.ts +15 -8
  38. package/dist/pie-chart/shared.d.ts.map +1 -1
  39. package/dist/pie-chart/shared.js +21 -20
  40. package/dist/pie-chart/shared.js.map +1 -1
  41. package/dist/radar-chart/index.d.ts +1 -1
  42. package/dist/radar-chart/index.d.ts.map +1 -1
  43. package/dist/radar-chart/index.js +1 -1
  44. package/dist/radar-chart/index.js.map +1 -1
  45. package/dist/radar-chart/radar-chart.d.ts +37 -43
  46. package/dist/radar-chart/radar-chart.d.ts.map +1 -1
  47. package/dist/radar-chart/radar-chart.js +12 -12
  48. package/dist/radar-chart/radar-chart.js.map +1 -1
  49. package/dist/radar-chart/radar-grid.d.ts +7 -1
  50. package/dist/radar-chart/radar-grid.d.ts.map +1 -1
  51. package/dist/radar-chart/radar-grid.js +17 -15
  52. package/dist/radar-chart/radar-grid.js.map +1 -1
  53. package/dist/radar-chart/shared.d.ts +7 -2
  54. package/dist/radar-chart/shared.d.ts.map +1 -1
  55. package/dist/radar-chart/shared.js +6 -0
  56. package/dist/radar-chart/shared.js.map +1 -1
  57. package/dist/radial-chart/radial-chart.d.ts +37 -43
  58. package/dist/radial-chart/radial-chart.d.ts.map +1 -1
  59. package/dist/radial-chart/radial-chart.js +2 -1
  60. package/dist/radial-chart/radial-chart.js.map +1 -1
  61. package/dist/radial-chart/shared.d.ts +13 -9
  62. package/dist/radial-chart/shared.d.ts.map +1 -1
  63. package/dist/radial-chart/shared.js +15 -16
  64. package/dist/radial-chart/shared.js.map +1 -1
  65. package/package.json +4 -4
  66. package/src/bar-chart/bar-chart.ts +17 -6
  67. package/src/bar-chart/chart-bars.ts +48 -9
  68. package/src/bar-chart/shared.ts +20 -5
  69. package/src/kit/cartesian.ts +10 -7
  70. package/src/kit/style.ts +214 -118
  71. package/src/line-chart/line-chart.ts +14 -8
  72. package/src/pie-chart/pie-chart.ts +3 -1
  73. package/src/pie-chart/shared.ts +30 -22
  74. package/src/radar-chart/index.ts +1 -1
  75. package/src/radar-chart/radar-chart.ts +16 -13
  76. package/src/radar-chart/radar-grid.ts +18 -13
  77. package/src/radar-chart/shared.ts +9 -2
  78. package/src/radial-chart/radial-chart.ts +3 -1
  79. package/src/radial-chart/shared.ts +30 -18
  80. package/dist/browser/chunks/chunk-R3Q5EE6E.js +0 -2
  81. package/dist/browser/chunks/chunk-R3Q5EE6E.js.map +0 -7
package/src/kit/style.ts CHANGED
@@ -2,11 +2,14 @@
2
2
  * The chart family's style: one `style` entry per chart, built from the groups
3
3
  * below, so a look saved off one chart lays onto any other (a field the other
4
4
  * chart does not draw is dropped). A chart spreads the shared groups and adds a
5
- * `marks` group of its own.
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.
6
8
  */
7
9
 
8
10
  import {
9
11
  Types,
12
+ textOps,
10
13
  type Fill,
11
14
  type StrokeResolved,
12
15
  type Stroke as _Stroke,
@@ -19,29 +22,50 @@ import {
19
22
 
20
23
  import type { AxisConfig, ChartSeries, LegendConfig } from "./shared"
21
24
 
22
- /** The style family every chart shares; a host keys saved styles by it. */
25
+ /** The style family every chart shares; the built-in presets are written for it. */
23
26
  export const CHART_STYLE_FAMILY = "chart"
24
27
 
25
- /** The out-of-the-box series colours, cycled by series (or row) index. */
28
+ /** The out-of-the-box series fills, cycled by series (or row) index. */
26
29
  export const CHART_PALETTE_DEFAULT = ["#4f8cff", "#2fbf8f", "#f5a524", "#e5547a", "#9b6cf0"] as const
27
30
 
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. */
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. */
36
43
  background: Types.fill([]),
37
44
  }
38
45
 
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 }),
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,
45
69
  }
46
70
 
47
71
  export const CARTESIAN_GRID = {
@@ -66,25 +90,23 @@ export const CHART_LEGEND = {
66
90
  swatch: Types.oneOf(["square", "circle", "line"] as const),
67
91
  }
68
92
 
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
93
  /** The fields every chart's style resolves, whichever groups it spreads. */
79
- export interface ChartStyleBase extends ChartTextStyle {
80
- readonly palette: readonly Fill[]
94
+ export interface ChartStyleBase {
95
+ readonly seriesFills: readonly Fill[]
81
96
  readonly background: Fill
97
+ readonly legendText: TextStyle
82
98
  readonly legend: LegendPosition
83
99
  readonly swatch: LegendSwatch
84
100
  }
85
101
 
86
- /** The cartesian grid fields, for the line and bar charts. */
102
+ /** The cartesian fields, for the line and bar charts. */
87
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
88
110
  readonly horizontalGrid: StrokeResolved[]
89
111
  readonly verticalGrid: StrokeResolved[]
90
112
  readonly axisStroke: StrokeResolved[]
@@ -92,8 +114,14 @@ export interface CartesianStyle extends ChartStyleBase {
92
114
  readonly showYAxis: boolean
93
115
  }
94
116
 
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 = {
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(),
97
125
  horizontalGrid: CARTESIAN_GRID.horizontalGrid,
98
126
  gridStroke: Types.stroke([]),
99
127
  spokeStroke: Types.stroke([]),
@@ -101,147 +129,154 @@ const _PRESET_RULES = {
101
129
  }
102
130
 
103
131
  /** 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>
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
+ }
105
159
 
106
160
  /** Ready-made looks, each stating every colour so applying one replaces the last. */
107
161
  export const CHART_STYLE_PRESETS = {
108
162
  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" },
163
+ seriesFills: [...CHART_PALETTE_DEFAULT],
164
+ ...inks(CHART_FOREGROUND, CHART_MUTED),
165
+ ...rules("white/15"),
115
166
  track: "white/10",
116
167
  background: [],
168
+ gridBackground: [],
117
169
  },
118
170
  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",
171
+ seriesFills: ["#2563eb", "#059669", "#d97706", "#db2777", "#7c3aed"],
172
+ ...inks("#1c1917", "#57534e"),
173
+ ...rules("#1c1917/12"),
126
174
  background: "#fafaf9",
175
+ gridBackground: [],
127
176
  },
128
177
  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",
178
+ seriesFills: ["#d9603b", "#8194ad", "#e0a93b", "#5e9d6b", "#9ec85a"],
179
+ ...inks("#e9e1d4", "#8a7f70"),
180
+ ...rules("#2c2820"),
136
181
  background: "#15130e",
182
+ gridBackground: [],
137
183
  },
138
184
  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",
185
+ seriesFills: ["#22d3ee", "#a3e635", "#f472b6", "#facc15", "#818cf8"],
186
+ ...inks("#ffffff", "#94a3b8"),
187
+ ...rules("white/10"),
146
188
  background: [],
189
+ gridBackground: [],
147
190
  },
148
191
  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",
192
+ seriesFills: ["#0ea5e9", "#14b8a6", "#6366f1", "#38bdf8", "#2dd4bf"],
193
+ ...inks("#e0f2fe", "#7dd3fc"),
194
+ ...rules("#7dd3fc/20"),
156
195
  background: "#082f49",
196
+ gridBackground: [],
157
197
  },
158
198
  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",
199
+ seriesFills: ["#f97316", "#ef4444", "#ec4899", "#f59e0b", "#a855f7"],
200
+ ...inks("#fff7ed", "#fdba74"),
201
+ ...rules("#fdba74/20"),
166
202
  background: [],
203
+ gridBackground: [],
167
204
  },
168
205
  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",
206
+ seriesFills: ["#a5b4fc", "#f9a8d4", "#86efac", "#fde68a", "#7dd3fc"],
207
+ ...inks("#f8fafc", "#cbd5e1"),
208
+ ...rules("white/12"),
176
209
  background: [],
210
+ gridBackground: [],
177
211
  },
178
212
  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",
213
+ seriesFills: ["#fafafa", "#a3a3a3", "#737373", "#d4d4d4", "#525252"],
214
+ ...inks("#fafafa", "#a3a3a3"),
215
+ ...rules("white/12"),
186
216
  background: [],
217
+ gridBackground: [],
187
218
  },
188
219
  } satisfies Record<string, ChartStylePreset>
189
220
 
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 {
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 {
192
223
  if (own != null) return own
193
- const palette = style.palette
194
- return palette.length > 0 ? palette[index % palette.length] : "white"
224
+ const fills = style.seriesFills
225
+ return fills.length > 0 ? fills[index % fills.length] : "white"
195
226
  }
196
227
 
197
228
  /** {@link paletteFill} for a series. */
198
- export function seriesFill(style: Pick<ChartStyleBase, "palette">, series: ChartSeries, index: number): Fill {
229
+ export function seriesFill(style: Pick<ChartStyleBase, "seriesFills">, series: ChartSeries, index: number): Fill {
199
230
  return paletteFill(style, index, series.fill)
200
231
  }
201
232
 
202
- function face(style: Pick<ChartTextStyle, "fontFamily">): Pick<TextStyle, "fontFamily"> {
203
- return style.fontFamily ? { fontFamily: style.fontFamily } : {}
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
204
237
  }
205
238
 
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 }
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
209
246
  }
210
247
 
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
- }
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")
220
251
  }
221
252
 
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 }
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)
225
256
  }
226
257
 
227
258
  /**
228
259
  * The kit axis parts' config from a style. `x` is the axis under the plot;
229
260
  * its grid lines are vertical (one per x marker), the `y` axis's horizontal.
230
261
  */
231
- export function axisConfigOf(style: Pick<CartesianStyle, "verticalGrid" | "horizontalGrid"> & ChartTextStyle, axis: "x" | "y"): AxisConfig {
232
- const gap = Math.round(style.fontSize * 0.6)
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)
233
267
  return {
234
- labelStyle: markerText(style, axis === "x" ? "center" : "right"),
235
- titleStyle: titleText(style),
268
+ labelStyle: markerText(style, axis),
269
+ titleStyle: placedText(axis === "x" ? style.xTitleText : style.yTitleText),
236
270
  lineStyle: axis === "x" ? style.verticalGrid : style.horizontalGrid,
237
271
  tickPadding: axis === "x" ? { top: gap } : { right: gap },
238
272
  titlePadding: axis === "x" ? { top: Math.round(gap / 2) } : { right: gap },
239
273
  }
240
274
  }
241
275
 
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))
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))
245
280
  const marker =
246
281
  style.swatch === "line"
247
282
  ? { width: Math.round(size * 2), height: Math.max(3, Math.round(size / 4)), borderRadius: size }
@@ -250,8 +285,69 @@ export function legendConfigOf(style: ChartTextStyle & Pick<ChartStyleBase, "swa
250
285
  : { width: size, height: size, borderRadius: Math.round(size / 4) }
251
286
  return {
252
287
  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),
288
+ textStyle: placedText(style.legendText, "left"),
289
+ itemGap: Math.round(type * 1.4),
255
290
  markerStyle: { ...marker, stroke: [], shadow: [] },
256
291
  }
257
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
+ }
@@ -12,6 +12,7 @@ import {
12
12
  // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
13
13
  type Node2D,
14
14
  type Node2DProps as _Node2DProps,
15
+ type TextStyle as _TextStyle,
15
16
  type Fill as _Fill,
16
17
  type FillResolved as _FillResolved,
17
18
  type Stroke as _Stroke,
@@ -46,14 +47,17 @@ import {
46
47
  } from "../kit/shared"
47
48
  import {
48
49
  CARTESIAN_GRID,
49
- CHART_COLORS,
50
+ CARTESIAN_TEXT,
50
51
  CHART_LEGEND,
52
+ CHART_SERIES,
51
53
  CHART_STYLE_FAMILY,
52
54
  CHART_STYLE_PRESETS,
53
- CHART_TEXT,
55
+ GRID_BACKGROUND,
54
56
  axisConfigOf,
55
57
  legendConfigOf,
58
+ migrateChartStyle,
56
59
  seriesFill,
60
+ sizeOf,
57
61
  valueText,
58
62
  } from "../kit/style"
59
63
  import { VerticalAxis, axisPads } from "../kit/vertical-axis"
@@ -75,7 +79,7 @@ import { resolveXScale, type XScale } from "./x-scale"
75
79
 
76
80
  const LINE_STYLE = Types.style(
77
81
  CHART_STYLE_FAMILY,
78
- { colors: CHART_COLORS, text: CHART_TEXT, grid: CARTESIAN_GRID, marks: LINE_MARKS, labels: LINE_LABELS, legend: CHART_LEGEND },
82
+ { series: CHART_SERIES, background: GRID_BACKGROUND, text: CARTESIAN_TEXT, grid: CARTESIAN_GRID, marks: LINE_MARKS, labels: LINE_LABELS, legend: CHART_LEGEND },
79
83
  { presets: CHART_STYLE_PRESETS }
80
84
  )
81
85
 
@@ -352,6 +356,7 @@ export const LineChart = component2d({
352
356
  height: "fill",
353
357
  weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
354
358
  },
359
+ migrate: { style: migrateChartStyle("cartesian") },
355
360
  allowedChildren: [],
356
361
  children: (state) => {
357
362
  const style = () => state.style
@@ -450,7 +455,7 @@ export const LineChart = component2d({
450
455
  state.data.forEach((_row, k) => {
451
456
  const line = lines[i]
452
457
  const label: Node2D = new Text({
453
- ...boundText(() => ({ ...valueText(style()), fontSize: Math.round(style().fontSize * 0.8) })),
458
+ ...boundText(() => valueText(style())),
454
459
  text: () => {
455
460
  const value = pixels(i).values[k]
456
461
  return value === undefined ? "" : yLabel(state, value, domains().y.ticks)
@@ -462,7 +467,7 @@ export const LineChart = component2d({
462
467
  },
463
468
  y: () => {
464
469
  const point = pixels(i).points[k]
465
- const clear = label.layoutBounds.height / 2 + (style().dots ? style().dotSize / 2 : style().strokeWidth / 2) + Math.round(style().fontSize * 0.25)
470
+ const clear = label.layoutBounds.height / 2 + (style().dots ? style().dotSize / 2 : style().strokeWidth / 2) + Math.round(sizeOf(style().valueText) * 0.25)
466
471
  return (point?.y ?? 0) + (below ? -clear : clear)
467
472
  },
468
473
  // In once the trace has passed its point.
@@ -492,7 +497,7 @@ export const LineChart = component2d({
492
497
  clip: () => state.clipped || overflows(state, domains()),
493
498
  children: [
494
499
  new ChartCanvas({
495
- fill: () => style().background,
500
+ fill: () => style().gridBackground,
496
501
  xGridStroke: () => style().verticalGrid,
497
502
  yGridStroke: () => style().horizontalGrid,
498
503
  xMarkers: () => domains().x.ticks,
@@ -506,8 +511,8 @@ export const LineChart = component2d({
506
511
  // Under the marks, so the highlight does not tint the stretch it picks out.
507
512
  new ChartRegion({
508
513
  box: spotlight,
509
- stroke: () => ({ weight: 2, fill: fade(style().foreground, 0.7), dash: [14, 12], cap: "butt", align: "center" }),
510
- fill: () => fade(style().foreground, 0.06),
514
+ stroke: () => ({ weight: 2, fill: fade(style().valueText.fill ?? "white", 0.7), dash: [14, 12], cap: "butt", align: "center" }),
515
+ fill: () => fade(style().valueText.fill ?? "white", 0.06),
511
516
  reveal: () => state.regionReveal,
512
517
  }),
513
518
  ...areas,
@@ -518,6 +523,7 @@ export const LineChart = component2d({
518
523
 
519
524
  return asParts([
520
525
  cartesianFrame({
526
+ background: () => style().background,
521
527
  left: vAxis,
522
528
  ghost,
523
529
  plot,
@@ -10,6 +10,7 @@ import {
10
10
  // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
11
11
  type Node2D,
12
12
  type Node2DProps as _Node2DProps,
13
+ type TextStyle as _TextStyle,
13
14
  type Fill as _Fill,
14
15
  type FillResolved as _FillResolved,
15
16
  } from "@motionscript/sdk"
@@ -18,7 +19,7 @@ import { asParts, fresh } from "../kit/entries"
18
19
  import { beats, type AnyTimeline, type Beat } from "../kit/motion"
19
20
  import { PieLegend } from "../kit/pie-legend"
20
21
  import { PieRing } from "./pie-ring"
21
- import { CHART_STYLE_FAMILY, CHART_STYLE_PRESETS, paletteFill } from "../kit/style"
22
+ import { CHART_STYLE_FAMILY, CHART_STYLE_PRESETS, migrateChartStyle, paletteFill } from "../kit/style"
22
23
  import {
23
24
  PIE_SPOTLIGHT_DIM,
24
25
  PIE_STYLE_GROUPS,
@@ -127,6 +128,7 @@ export const PieChart = component2d({
127
128
  height: "fill",
128
129
  pops: (state) => slicesOf(state).map(() => 0),
129
130
  },
131
+ migrate: { style: migrateChartStyle("pie") },
130
132
  allowedChildren: [],
131
133
  // The structure (a label and legend row per row) is read once; slices, paint and selection are bound, so a theme tween re-paints and a `set` of `data` re-cuts.
132
134
  children: (state) => {
@@ -1,10 +1,15 @@
1
1
  import { Types, type Fill, type TextStyle, type Stroke, type FillResolved as _FillResolved } from "@motionscript/sdk"
2
2
 
3
3
  import {
4
- CHART_COLORS,
4
+ CHART_BACKGROUND,
5
+ CHART_FOREGROUND,
5
6
  CHART_LEGEND,
6
- CHART_TEXT,
7
+ CHART_MUTED,
8
+ CHART_SERIES,
9
+ LEGEND_TEXT,
7
10
  legendConfigOf,
11
+ placedText,
12
+ sizeOf,
8
13
  type ChartStyleBase,
9
14
  } from "../kit/style"
10
15
  import { num, text, type LegendConfig } from "../kit/shared"
@@ -13,7 +18,7 @@ import { DEG, PIE_SPOTLIGHT_DIM, ringPoint, selectionFade, type CenterConfig, ty
13
18
  export { DEG, PIE_SPOTLIGHT_DIM, ringPoint, selectionFade, type CenterConfig, type PieSlice }
14
19
 
15
20
  /**
16
- * One slice per row, painted from `palette` by row order (cycling). Paint comes
21
+ * One slice per row, painted from `seriesFills` by row order (cycling). Paint comes
17
22
  * from the palette, not the rows, because a pie's mark is a row where a line's
18
23
  * colour belongs to a column; that lets a pie be fed a CSV of plain numbers.
19
24
  */
@@ -147,6 +152,9 @@ export interface LabelConfig {
147
152
 
148
153
  /** What a pie's `style` resolves to. */
149
154
  export interface PieStyle extends ChartStyleBase {
155
+ readonly labelText: TextStyle
156
+ readonly valueText: TextStyle
157
+ readonly centerText: TextStyle
150
158
  readonly innerRadius: number
151
159
  readonly sliceGap: number
152
160
  readonly sliceRadius: number
@@ -155,8 +163,17 @@ export interface PieStyle extends ChartStyleBase {
155
163
 
156
164
  /** The groups of a pie's `style`: no grid, as nothing is ruled. */
157
165
  export const PIE_STYLE_GROUPS = {
158
- colors: CHART_COLORS,
159
- text: CHART_TEXT,
166
+ series: CHART_SERIES,
167
+ background: CHART_BACKGROUND,
168
+ text: {
169
+ /** Each slice's name. */
170
+ labelText: Types.typography({ fontSize: 28, fontWeight: 500, fill: CHART_MUTED }),
171
+ /** Each slice's share or value, beside its name; inside a wedge both are set larger, inked for contrast. */
172
+ valueText: Types.typography({ fontSize: 28, fontWeight: 700, fill: CHART_FOREGROUND }),
173
+ /** The summary in the hole; its caption is set smaller, in the slice names' ink. */
174
+ centerText: Types.typography({ fontSize: 50, fontWeight: 800, fill: CHART_FOREGROUND }),
175
+ ...LEGEND_TEXT,
176
+ },
160
177
  marks: {
161
178
  /** Hole radius as a fraction of the outer radius: 0 is a pie, about 0.6 a donut. */
162
179
  innerRadius: Types.number(0, { min: 0, max: 0.9, step: 0.05 }),
@@ -176,34 +193,25 @@ export function legendOf(style: PieStyle): LegendConfig {
176
193
  return legendConfigOf(style)
177
194
  }
178
195
 
179
- function face(style: PieStyle): Pick<TextStyle, "fontFamily"> {
180
- return style.fontFamily ? { fontFamily: style.fontFamily } : {}
181
- }
182
-
183
196
  export function ringConfigOf(style: PieStyle): RingConfig {
184
197
  return { gap: style.sliceGap, holeRadius: style.innerRadius, startAngle: 0, selectedGrow: 1.08, cornerRadius: style.sliceRadius }
185
198
  }
186
199
 
187
200
  export function labelConfigOf(style: PieStyle): LabelConfig {
188
- const base: TextStyle = { ...face(style), fontSize: style.fontSize, fontWeight: style.fontWeight, textAlign: "center", fill: style.muted }
189
- const bold = Math.min(900, style.fontWeight + 200)
201
+ const base = placedText(style.labelText)
202
+ const value = placedText(style.valueText)
190
203
  return {
191
204
  textStyle: base,
192
- percentStyle: { ...base, fontWeight: bold, fill: style.foreground },
193
- insideStyle: { ...base, fontSize: Math.round(style.fontSize * 1.9), fontWeight: bold, fill: "#111113" },
194
- leaderStroke: { weight: 2, fill: style.muted },
205
+ percentStyle: value,
206
+ insideStyle: { ...value, fontSize: Math.round(sizeOf(style.valueText) * 1.9), fill: "#111113" },
207
+ leaderStroke: { weight: 2, fill: style.labelText.fill ?? "white" },
195
208
  }
196
209
  }
197
210
 
198
211
  export function centerConfigOf(style: PieStyle): CenterConfig {
212
+ const size = sizeOf(style.centerText, 50)
199
213
  return {
200
- labelStyle: {
201
- ...face(style),
202
- fontSize: Math.round(style.fontSize * 1.8),
203
- fontWeight: Math.min(900, style.fontWeight + 300),
204
- textAlign: "center",
205
- fill: style.foreground,
206
- },
207
- captionStyle: { ...face(style), fontSize: Math.round(style.fontSize * 0.8), fontWeight: style.fontWeight, textAlign: "center", fill: style.muted },
214
+ labelStyle: placedText(style.centerText),
215
+ captionStyle: placedText(style.labelText, "center", { fontSize: Math.round(size * 0.45) }),
208
216
  }
209
217
  }