@motionscript/charts 0.1.1-alpha.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 (207) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +98 -28
  3. package/dist/bar-chart/bar-chart.d.ts +233 -39
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +215 -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 +52 -52
  12. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  13. package/dist/bar-chart/chart-bars.js +80 -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 +36 -65
  20. package/dist/bar-chart/shared.d.ts.map +1 -1
  21. package/dist/bar-chart/shared.js +68 -117
  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 +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 +49 -15
  38. package/dist/kit/cartesian.d.ts.map +1 -1
  39. package/dist/kit/cartesian.js +79 -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 +254 -0
  70. package/dist/kit/style.d.ts.map +1 -0
  71. package/dist/kit/style.js +197 -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 +257 -18
  90. package/dist/line-chart/line-chart.d.ts.map +1 -1
  91. package/dist/line-chart/line-chart.js +282 -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 +37 -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 +53 -61
  118. package/dist/pie-chart/shared.d.ts.map +1 -1
  119. package/dist/pie-chart/shared.js +41 -121
  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 +218 -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 +43 -0
  130. package/dist/radar-chart/radar-grid.d.ts.map +1 -0
  131. package/dist/radar-chart/radar-grid.js +61 -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 +73 -0
  138. package/dist/radar-chart/shared.d.ts.map +1 -0
  139. package/dist/radar-chart/shared.js +123 -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 +216 -0
  146. package/dist/radial-chart/radial-chart.d.ts.map +1 -0
  147. package/dist/radial-chart/radial-chart.js +174 -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 +73 -0
  154. package/dist/radial-chart/shared.d.ts.map +1 -0
  155. package/dist/radial-chart/shared.js +79 -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 +252 -179
  160. package/src/bar-chart/category-axis.ts +46 -40
  161. package/src/bar-chart/chart-bars.ts +97 -117
  162. package/src/bar-chart/index.ts +7 -10
  163. package/src/bar-chart/shared.ts +78 -238
  164. package/src/engine.ts +5 -2
  165. package/src/index.ts +2 -0
  166. package/src/kit/cartesian.ts +118 -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 +257 -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 +347 -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 +52 -35
  185. package/src/pie-chart/pie-ring.ts +92 -71
  186. package/src/pie-chart/shared.ts +68 -266
  187. package/src/radar-chart/index.ts +4 -0
  188. package/src/radar-chart/radar-chart.ts +287 -0
  189. package/src/radar-chart/radar-grid.ts +78 -0
  190. package/src/radar-chart/radar-series.ts +88 -0
  191. package/src/radar-chart/shared.ts +146 -0
  192. package/src/radial-chart/index.ts +5 -0
  193. package/src/radial-chart/radial-chart.ts +210 -0
  194. package/src/radial-chart/radial-rings.ts +328 -0
  195. package/src/radial-chart/shared.ts +114 -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
@@ -1,91 +1,66 @@
1
- import {
2
- fillOps,
3
- lerpNumber,
4
- shadowOps,
5
- strokeOps,
6
- textOps,
7
- type DataRecord,
8
- type Fill,
9
- type Shadow,
10
- type ShadowResolved,
11
- type Stroke,
12
- type StrokeResolved,
13
- type TextStyle,
14
- } from "@motionscript/sdk"
15
- import { niceStep } from "@motionscript/sdk/component"
1
+ import { Types, fillOps, type DataRecord, type Fill } from "@motionscript/sdk"
16
2
 
17
- import {
18
- CHART_PALETTE,
19
- DEFAULT_LEGEND,
20
- DEFAULT_PLOT_AREA,
21
- DEFAULT_SERIES_FILLS,
22
- DEFAULT_X_AXIS,
23
- DEFAULT_Y_AXIS,
24
- SPOTLIGHT_DIM,
25
- lerpAxisConfig,
26
- lerpLegendConfig,
27
- lerpPlotArea,
28
- lerpSeriesFills,
29
- num,
30
- resolveAxisConfig,
31
- resolveLegendConfig,
32
- resolvePlotArea,
33
- resolveSeriesFills,
34
- text,
35
- type AxisConfig,
36
- type ChartSeries,
37
- type LegendConfig,
38
- type LegendTheme,
39
- type PlotAreaConfig,
40
- type Range,
41
- } from "../kit/shared"
3
+ import { niceRange, num, text, type ChartSeries, type Range } from "../kit/shared"
42
4
 
43
5
  /** One bar per row, beside its siblings in the category slot or stacked on them: {@link ChartSeries} under the name for what it draws here. */
44
6
  export type BarSeries = ChartSeries
45
7
 
8
+ /** How the series share a category: side by side, stacked, or stacked as shares of 100. */
9
+ export type BarStack = "none" | "stacked" | "percent"
10
+
11
+ /** Which way the bars grow: up from a category row, or right from a category column. */
12
+ export type BarOrientation = "vertical" | "horizontal"
13
+
46
14
  /**
47
- * One bar's span in data units. A span rather than a height because both modes
48
- * fit it, and a negative value just runs the other way with no special case.
15
+ * One bar's span in data units. A span rather than a height because every mode
16
+ * fits it, and a negative value just runs the other way with no special case.
49
17
  */
50
18
  export interface BarSpan {
51
- /** Where the base sits: 0 when grouped, the running total below when stacked. */
19
+ /** Where the base sits: 0 apart, the running total below (or above, for a negative) when stacked. */
52
20
  from: number
53
21
  to: number
54
- /** `to - from`, kept for the figure printed on the bar. */
22
+ /** `to - from`. */
55
23
  value: number
24
+ /** Whether it ends its stack on its side of zero, so its far end is rounded. */
25
+ outer: boolean
56
26
  }
57
27
 
58
28
  /**
59
- * One row's spans, in series order. Stacking is a running sum in listed order,
60
- * so series 0 sits on the baseline. A zero value still yields a degenerate span,
61
- * keeping the list index-aligned with the series.
62
- */
63
- export function barSpans(
64
- row: DataRecord,
65
- series: BarSeries[],
66
- stacked: boolean
67
- ): BarSpan[] {
68
- let running = 0
69
- return series.map((s) => {
70
- const value = num(row, s.field)
71
- if (!stacked) return { from: 0, to: value, value }
72
- const from = running
73
- running += value
74
- return { from, to: running, value }
75
- })
76
- }
77
-
78
- /**
79
- * The series ending a row's stack, which gets the rounded cap; `-1` when the row
80
- * is all zero. It is the last non-zero series, so a zero one does not take the
81
- * cap and leave the segment below square.
29
+ * One row's spans, in series order. Stacking keeps a running sum on each side
30
+ * of zero, so a negative value stacks downward from the baseline rather than
31
+ * eating into the positives. In `percent` each value is its share of the row's
32
+ * total magnitude. A zero value still yields a degenerate span, keeping the list
33
+ * index-aligned with the series. `weights` scale each series' contribution.
82
34
  */
83
- export function stackCapIndex(row: DataRecord, series: BarSeries[]): number {
84
- let last = -1
85
- for (let i = 0; i < series.length; i++) {
86
- if (num(row, series[i].field) !== 0) last = i
35
+ export function barSpans(row: DataRecord, series: readonly BarSeries[], stack: BarStack, weights: readonly number[] = []): BarSpan[] {
36
+ const weight = (i: number) => Math.max(0, weights[i] ?? 1)
37
+ if (stack !== "stacked" && stack !== "percent") {
38
+ return series.map((s) => {
39
+ const value = num(row, s.field)
40
+ return { from: 0, to: value, value, outer: true }
41
+ })
87
42
  }
88
- return last
43
+ const values = series.map((s, i) => num(row, s.field) * weight(i))
44
+ const total = values.reduce((sum, v) => sum + Math.abs(v), 0)
45
+ const scaled = stack === "percent" ? values.map((v) => (total > 0 ? (v / total) * 100 : 0)) : values
46
+ let lastUp = -1
47
+ let lastDown = -1
48
+ scaled.forEach((v, i) => {
49
+ if (v > 0) lastUp = i
50
+ else if (v < 0) lastDown = i
51
+ })
52
+ let up = 0
53
+ let down = 0
54
+ return scaled.map((value, i) => {
55
+ if (value < 0) {
56
+ const from = down
57
+ down += value
58
+ return { from, to: down, value, outer: i === lastDown }
59
+ }
60
+ const from = up
61
+ up += value
62
+ return { from, to: up, value, outer: i === lastUp }
63
+ })
89
64
  }
90
65
 
91
66
  export function barCategories(data: DataRecord[], field: string): string[] {
@@ -94,18 +69,14 @@ export function barCategories(data: DataRecord[], field: string): string[] {
94
69
 
95
70
  /**
96
71
  * The `[min, max]` of everything drawn, zero always included: a scale starting
97
- * at 40 draws a column three times its neighbour for a 10% difference. A cropped
72
+ * at 40 draws a bar three times its neighbour for a 10% difference. A cropped
98
73
  * axis takes an explicit range.
99
74
  */
100
- export function barValueExtent(
101
- data: DataRecord[],
102
- series: BarSeries[],
103
- stacked: boolean
104
- ): Range {
75
+ export function barValueExtent(data: DataRecord[], series: readonly BarSeries[], stack: BarStack, weights?: readonly number[]): Range {
105
76
  let lo = 0
106
77
  let hi = 0
107
78
  for (const row of data) {
108
- for (const span of barSpans(row, series, stacked)) {
79
+ for (const span of barSpans(row, series, stack, weights)) {
109
80
  lo = Math.min(lo, span.from, span.to)
110
81
  hi = Math.max(hi, span.from, span.to)
111
82
  }
@@ -113,174 +84,43 @@ export function barValueExtent(
113
84
  return lo < hi ? [lo, hi] : [0, 1]
114
85
  }
115
86
 
116
- /** The explicit `range`, else the data's extent nudged to tidy bounds; `divisions` (one fewer than the marker count) makes the drawn ticks land on them. */
87
+ /** The explicit `range`, else the data's extent nudged to tidy bounds (a percent stack reads to 100); `divisions` (one fewer than the marker count) makes the drawn ticks land on them. */
117
88
  export function resolveBarScale(
118
89
  data: DataRecord[],
119
- series: BarSeries[],
120
- stacked: boolean,
90
+ series: readonly BarSeries[],
91
+ stack: BarStack,
121
92
  range: Range | undefined,
122
- divisions: number
93
+ divisions: number,
94
+ weights?: readonly number[]
123
95
  ): Range {
124
96
  if (range) return range
125
- const [lo, hi] = barValueExtent(data, series, stacked)
126
- const segments = Math.max(1, Math.round(divisions))
127
- const step = niceStep((hi - lo) / segments || 1)
128
- return [Math.floor(lo / step) * step, Math.ceil(hi / step) * step]
129
- }
130
-
131
- /** How the columns are drawn and the slot divided. */
132
- export interface BarStyleConfig {
133
- /** Radius in px of a column's far-end corners; an interior stacked segment stays square. */
134
- cornerRadius: number
135
- /** Fraction of the category slot the bars fill, `[0, 1]`; the rest separates groups. */
136
- slotFill: number
137
- /** Gap in px between grouped bars within a slot; ignored when stacked. */
138
- groupGap: number
139
- stroke: Stroke
140
- shadow: Shadow
97
+ const [lo, hi] = barValueExtent(data, series, stack, weights)
98
+ if (stack === "percent") return [lo < 0 ? -100 : 0, hi > 0 ? 100 : 0]
99
+ return niceRange([lo, hi], divisions)
141
100
  }
142
101
 
143
- /** A {@link BarChart}'s full, resolved visual theme. */
144
- export interface BarChartConfig {
145
- /** Cycled by series index; a series' own `fill` replaces its entry outright. */
146
- seriesFills: Fill[]
147
- bar: BarStyleConfig
148
- /** The category axis. */
149
- xAxis: AxisConfig
150
- /** The value axis. */
151
- yAxis: AxisConfig
152
- plotArea: PlotAreaConfig
153
- legend: LegendConfig
154
- /** The figure printed on each column. */
155
- valueStyle: TextStyle
156
- /** How far everything outside a spotlit series or category fades. */
157
- spotlight: { dimOpacity: number }
102
+ /** Black or white, whichever reads on `fill`: a figure printed inside a bar. */
103
+ export function inkOn(fill: Fill): string {
104
+ const layers = fillOps.resolve(fill)
105
+ const top = layers[layers.length - 1]
106
+ if (top?.type !== "solid") return "white"
107
+ const [r, g, b] = top.color
108
+ const lift = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
109
+ const luminance = 0.2126 * lift(r) + 0.7152 * lift(g) + 0.0722 * lift(b)
110
+ return luminance > 0.4 ? "#0a0a0a" : "white"
158
111
  }
159
112
 
160
- /** The author's partial theme, deep-merged onto {@link DEFAULT_BAR_CHART_THEME}. */
161
- export type BarChartTheme = {
162
- seriesFills?: Fill[]
163
- bar?: Partial<BarStyleConfig>
164
- xAxis?: Partial<AxisConfig>
165
- yAxis?: Partial<AxisConfig>
166
- plotArea?: Partial<PlotAreaConfig>
167
- legend?: LegendTheme
168
- valueStyle?: TextStyle
169
- spotlight?: Partial<BarChartConfig["spotlight"]>
170
- }
171
-
172
- export const DEFAULT_BAR_CHART_THEME: BarChartConfig = {
173
- seriesFills: DEFAULT_SERIES_FILLS,
174
- bar: {
175
- cornerRadius: 6,
176
- slotFill: 0.72,
177
- groupGap: 6,
178
- stroke: [],
179
- shadow: [],
180
- },
181
- // Captions rather than numeric markers: less weight than the line chart's ticks.
182
- xAxis: {
183
- ...DEFAULT_X_AXIS,
184
- labelStyle: { ...DEFAULT_X_AXIS.labelStyle, fontWeight: 600 },
185
- tickPadding: 16,
186
- },
187
- yAxis: DEFAULT_Y_AXIS,
188
- plotArea: DEFAULT_PLOT_AREA,
189
- legend: DEFAULT_LEGEND,
190
- valueStyle: {
191
- fontSize: 24,
192
- fontWeight: 700,
193
- textAlign: "center",
194
- fill: CHART_PALETTE.markerColor,
195
- },
196
- spotlight: { dimOpacity: SPOTLIGHT_DIM },
197
- }
198
-
199
- function resolveBarStyle(
200
- bar: Partial<BarStyleConfig> | undefined,
201
- previous: BarStyleConfig
202
- ): BarStyleConfig {
203
- return {
204
- cornerRadius: bar?.cornerRadius ?? previous.cornerRadius,
205
- slotFill: bar?.slotFill ?? previous.slotFill,
206
- groupGap: bar?.groupGap ?? previous.groupGap,
207
- stroke: strokeOps.resolve(bar?.stroke ?? previous.stroke),
208
- shadow: shadowOps.resolve(bar?.shadow ?? previous.shadow),
209
- }
210
- }
211
-
212
- function lerpBarStyle(
213
- from: BarStyleConfig,
214
- to: BarStyleConfig,
215
- t: number
216
- ): BarStyleConfig {
217
- return {
218
- cornerRadius: lerpNumber(from.cornerRadius, to.cornerRadius, t),
219
- slotFill: lerpNumber(from.slotFill, to.slotFill, t),
220
- groupGap: lerpNumber(from.groupGap, to.groupGap, t),
221
- stroke: strokeOps.lerp(
222
- from.stroke as StrokeResolved[],
223
- to.stroke as StrokeResolved[],
224
- t
225
- ),
226
- shadow: shadowOps.lerp(
227
- from.shadow as ShadowResolved[],
228
- to.shadow as ShadowResolved[],
229
- t
230
- ),
231
- }
232
- }
233
-
234
- /** The `theme` entry's resolve: merges a partial {@link BarChartTheme} onto `previous` (first resolution: the default). */
235
- export function resolveBarChartTheme(
236
- theme: BarChartTheme | undefined,
237
- previous?: BarChartConfig
238
- ): BarChartConfig {
239
- const base = previous ?? DEFAULT_BAR_CHART_THEME
240
- return {
241
- seriesFills: resolveSeriesFills(theme?.seriesFills, base.seriesFills),
242
- bar: resolveBarStyle(theme?.bar, base.bar),
243
- xAxis: resolveAxisConfig(theme?.xAxis, base.xAxis),
244
- yAxis: resolveAxisConfig(theme?.yAxis, base.yAxis),
245
- plotArea: resolvePlotArea(theme?.plotArea, base.plotArea),
246
- legend: resolveLegendConfig(theme?.legend, base.legend),
247
- valueStyle: textOps.resolveStyle(theme?.valueStyle, base.valueStyle),
248
- spotlight: {
249
- dimOpacity: theme?.spotlight?.dimOpacity ?? base.spotlight.dimOpacity,
250
- },
251
- }
252
- }
253
-
254
- export function lerpBarChartTheme(
255
- from: BarChartConfig,
256
- to: BarChartConfig,
257
- t: number
258
- ): BarChartConfig {
259
- return {
260
- seriesFills: lerpSeriesFills(from.seriesFills, to.seriesFills, t),
261
- bar: lerpBarStyle(from.bar, to.bar, t),
262
- xAxis: lerpAxisConfig(from.xAxis, to.xAxis, t),
263
- yAxis: lerpAxisConfig(from.yAxis, to.yAxis, t),
264
- plotArea: lerpPlotArea(from.plotArea, to.plotArea, t),
265
- legend: lerpLegendConfig(from.legend, to.legend, t),
266
- valueStyle: textOps.lerpStyle(from.valueStyle, to.valueStyle, t),
267
- spotlight: {
268
- dimOpacity: lerpNumber(
269
- from.spotlight.dimOpacity,
270
- to.spotlight.dimOpacity,
271
- t
272
- ),
273
- },
274
- }
113
+ /** A bar chart's own look: how round and how wide the bars are. */
114
+ export const BAR_MARKS = {
115
+ /** Radius of each bar's far corners; a stacked segment inside the stack stays square. */
116
+ barRadius: Types.number(10, { min: 0, max: 64, step: 1, unit: "px" }),
117
+ /** How much of its category slot a bar group fills, the rest spacing the groups. */
118
+ barWidth: Types.number(0.7, { min: 0.05, max: 1, step: 0.05 }),
119
+ /** Space between the bars of one group, side by side. */
120
+ barGap: Types.number(6, { min: 0, max: 40, step: 1, unit: "px" }),
275
121
  }
276
122
 
277
- /** Series `index`'s own `fill`, else the palette's entry. */
278
- export function resolveBarFill(
279
- palette: Fill[],
280
- series: BarSeries,
281
- index: number
282
- ): Fill {
283
- if (series.fill != null) return fillOps.resolve(series.fill)
284
- if (palette.length === 0) return "white"
285
- return palette[index % palette.length]
123
+ export const BAR_LABELS = {
124
+ /** Read as the chart is built: each bar's value past its end, inside it, or none. */
125
+ values: Types.oneOf(["none", "end", "inside"] as const, { animatable: false }),
286
126
  }
package/src/engine.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  /**
2
- * The chart kit: what the three charts share, and what a copied chart does not
2
+ * The chart kit: what the five charts share, and what a copied chart does not
3
3
  * copy. A chart finds its parts by component identity (`VerticalAxis.is`), so a
4
4
  * second copy of `VerticalAxis` would be a second component that the chart never
5
5
  * finds, with no error. Copies import the kit from `@motionscript/charts`.
6
6
  */
7
7
  export * from "./kit/shared"
8
+ export * from "./kit/style"
8
9
  export * from "./kit/cartesian"
9
- export * from "./kit/chart-border"
10
10
  export * from "./kit/chart-canvas"
11
+ export * from "./kit/horizontal-axis"
11
12
  export * from "./kit/chart-legend"
12
13
  export * from "./kit/entries"
13
14
  export * from "./kit/motion"
14
15
  export * from "./kit/paints"
16
+ export * from "./kit/pie-legend"
17
+ export * from "./kit/polar"
15
18
  export * from "./kit/vertical-axis"
16
19
  export * from "@motionscript/sdk/component"
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./line-chart";
2
2
  export * from "./bar-chart";
3
3
  export * from "./pie-chart";
4
+ export * from "./radar-chart";
5
+ export * from "./radial-chart";
4
6
  export * from "./engine";
5
7
  export { NODES } from "./nodes";
@@ -1,62 +1,96 @@
1
- import { Rect, type ChildView, type Node2D, type Size2D } from "@motionscript/sdk"
1
+ import {
2
+ Rect,
3
+ RotatedBox,
4
+ Text,
5
+ textOps,
6
+ type ChildView,
7
+ type Fill,
8
+ type FillResolved,
9
+ type Insets,
10
+ type Node2D,
11
+ type Size2D,
12
+ type TextStyle,
13
+ } from "@motionscript/sdk"
2
14
 
3
- import { VerticalAxis } from "./vertical-axis"
15
+ import type { CartesianStyle, LegendPosition } from "./style"
4
16
 
5
- /**
6
- * Headroom above the plot, in px. Real padding on the composition, so the top
7
- * marker, centred on the top grid line, does not draw its upper half above the
8
- * chart's own box.
9
- */
10
- const TOP_HEADROOM = 24
17
+ /** Space between the frame's rows and columns: the plot and the legend, the axes and the legend. */
18
+ export function frameGap(style: Pick<CartesianStyle, "fontSize">): number {
19
+ return Math.round(style.fontSize * 0.9)
20
+ }
21
+
22
+ /** Room above the plot for the top value marker, which is centred on the top grid line. */
23
+ export function headroom(style: Pick<CartesianStyle, "fontSize">): number {
24
+ return Math.ceil(style.fontSize * 0.7)
25
+ }
26
+
27
+ /** How far an end marker of a horizontal value axis may hang past the plot: half a short caption. */
28
+ export function overhang(style: Pick<CartesianStyle, "fontSize">): number {
29
+ return Math.ceil(style.fontSize * 1.1)
30
+ }
11
31
 
12
32
  interface CartesianParts {
13
- readonly axis: Node2D
33
+ /** The axis beside the plot, on its left. */
34
+ readonly left?: Node2D
35
+ /** An invisible twin of `left` under the plot, so the row there starts at the plot's left edge. */
36
+ readonly ghost?: Node2D
14
37
  /** The box the grid and the marks share. */
15
38
  readonly plot: Node2D
16
- /** An invisible twin of the value axis, reserving its gutter under the plot. */
17
- readonly gutter: Node2D
18
- /** What sits under the plot, top to bottom: the other axis, then the legend. */
19
- readonly below: readonly Node2D[]
39
+ /** The axis under the plot. */
40
+ readonly bottom?: Node2D
41
+ readonly legend?: Node2D
42
+ readonly legendAt: LegendPosition
43
+ readonly gap: () => number
44
+ readonly padding: () => Insets
20
45
  }
21
46
 
22
- /** The frame the line and bar charts share: the value axis beside the plot, and under them the gutter, so the rows below start at the plot's left edge. */
47
+ /**
48
+ * The frame the line and bar charts share. A legend above or below is centred
49
+ * on the whole chart; one on the right is a column beside the plot. An axis the
50
+ * style turns off is simply not here, and the plot takes its room.
51
+ */
23
52
  export function cartesianFrame(parts: CartesianParts): Node2D {
53
+ const top = new Rect({ flow: "horizontal", width: "fill", height: "fill", children: [...(parts.left ? [parts.left] : []), parts.plot] })
54
+ const rows: Node2D[] = [top as unknown as Node2D]
55
+ if (parts.bottom) {
56
+ rows.push(new Rect({ flow: "horizontal", width: "fill", height: "hug", children: [...(parts.ghost ? [parts.ghost] : []), parts.bottom] }) as unknown as Node2D)
57
+ }
58
+ const body = new Rect({ flow: "vertical", width: "fill", height: "fill", children: rows })
59
+ const legend = parts.legend && parts.legendAt !== "none" ? parts.legend : undefined
60
+ const main = new Rect({
61
+ flow: "horizontal",
62
+ width: "fill",
63
+ height: "fill",
64
+ align: "center",
65
+ gap: parts.gap,
66
+ children: [body, ...(legend && parts.legendAt === "right" ? [legend] : [])],
67
+ })
68
+ const across = (node: Node2D) => new Rect({ flow: "horizontal", width: "fill", height: "hug", align: "center", children: [node] })
24
69
  return new Rect({
25
70
  flow: "vertical",
26
71
  width: "fill",
27
72
  height: "fill",
28
- padding: { top: TOP_HEADROOM },
73
+ gap: parts.gap,
74
+ padding: parts.padding,
29
75
  children: [
30
- new Rect({ flow: "horizontal", height: "fill", width: "fill", children: [parts.axis, parts.plot] }),
31
- new Rect({
32
- flow: "horizontal",
33
- width: "fill",
34
- children: [parts.gutter, new Rect({ flow: "vertical", width: "fill", gap: 20, children: [...parts.below] })],
35
- }),
76
+ ...(legend && parts.legendAt === "top" ? [across(legend)] : []),
77
+ main,
78
+ ...(legend && parts.legendAt === "bottom" ? [across(legend)] : []),
36
79
  ],
37
80
  }) as unknown as Node2D
38
81
  }
39
82
 
40
- interface CartesianViews {
41
- readonly axis: ChildView
42
- /** The plot panel's own children: grid, marks, border. */
43
- readonly plot: readonly ChildView[]
44
- readonly below: readonly ChildView[]
45
- }
46
-
47
- /** The last child shaped as {@link cartesianFrame} builds it: the chart builds it after the children it was given and a document adds its own later, so it is found by shape. */
48
- export function cartesianViews(children: readonly ChildView[]): CartesianViews | null {
49
- for (let i = children.length - 1; i >= 0; i--) {
50
- const frame = children[i]
51
- if (!Rect.is(frame)) continue
52
- const [top, bottom] = frame.children
53
- const axis = top?.children[0]
54
- const plot = top?.children[1]
55
- const below = bottom?.children[1]
56
- if (!axis || !VerticalAxis.is(axis) || !plot || !below) continue
57
- return { axis, plot: plot.children, below: below.children }
83
+ /** Every view under `children`, depth first in build order: the axis beside the plot comes before its twin, which holds no parts. */
84
+ export function chartViews(children: readonly ChildView[]): ChildView[] {
85
+ const out: ChildView[] = []
86
+ const walk = (views: readonly ChildView[]) => {
87
+ for (const view of views) {
88
+ out.push(view)
89
+ walk(view.children)
90
+ }
58
91
  }
59
- return null
92
+ walk(children)
93
+ return out
60
94
  }
61
95
 
62
96
  /** A part's laid-out size; zero before it is built or laid out. */
@@ -64,3 +98,47 @@ export function laidOutSize(node: Node2D | undefined): Size2D {
64
98
  const bounds = node?.layoutBounds
65
99
  return { width: bounds?.width ?? 0, height: bounds?.height ?? 0 }
66
100
  }
101
+
102
+ /**
103
+ * A text style as props bound to `read`, so a style tween reaches text built
104
+ * once. Only the keys set at build are bound: an unset family stays unset, so
105
+ * the host's default face still applies.
106
+ */
107
+ export function boundText(read: () => TextStyle): Record<string, unknown> {
108
+ const keys = Object.keys(textOps.styleProps(read())) as (keyof TextStyle)[]
109
+ const props: Record<string, unknown> = {}
110
+ for (const key of keys) props[key] = () => read()[key]
111
+ return props
112
+ }
113
+
114
+ interface GhostSpec {
115
+ readonly captions: () => readonly string[]
116
+ /** How many captions the real axis built. */
117
+ readonly count: number
118
+ readonly text: () => TextStyle
119
+ readonly title: string
120
+ readonly titleText: () => TextStyle
121
+ readonly tickPad: () => number
122
+ readonly titlePad: () => number
123
+ }
124
+
125
+ /**
126
+ * An invisible column as wide as a vertical axis built from the same captions:
127
+ * the widest caption plus the tick padding, and the rotated title before it. A
128
+ * plain tree, so a command looking for the axis never finds this instead.
129
+ */
130
+ export function columnGhost(spec: GhostSpec): Node2D {
131
+ const captions = Array.from({ length: spec.count }, (_, i) => new Text({ ...boundText(spec.text), text: () => spec.captions()[i] ?? "" }))
132
+ const children: Node2D[] = []
133
+ if (spec.title !== "") {
134
+ children.push(new RotatedBox({ angle: -90, width: "hug", height: "hug", children: new Text({ ...boundText(spec.titleText), text: spec.title }) }) as unknown as Node2D)
135
+ }
136
+ children.push(new Rect({ flow: "vertical", width: "hug", height: "hug", padding: () => ({ right: spec.tickPad() }), children: captions }) as unknown as Node2D)
137
+ return new Rect({ opacity: 0, flow: "horizontal", gap: spec.titlePad, width: "hug", height: 0, children }) as unknown as Node2D
138
+ }
139
+
140
+ /** `fill` with every layer's opacity scaled by `k`. */
141
+ export function fade(fill: Fill, k: number): Fill {
142
+ const layers = (Array.isArray(fill) ? fill : [fill]) as FillResolved[]
143
+ return layers.map((layer) => (typeof layer === "object" && layer !== null ? { ...layer, opacity: ((layer as { opacity?: number }).opacity ?? 1) * k } : layer)) as Fill
144
+ }