@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
@@ -1,91 +1,76 @@
1
1
  import {
2
+ Types,
2
3
  fillOps,
3
- lerpNumber,
4
- shadowOps,
5
- strokeOps,
6
- textOps,
7
4
  type DataRecord,
8
5
  type Fill,
9
- type Shadow,
10
- type ShadowResolved,
11
- type Stroke,
12
- type StrokeResolved,
13
- type TextStyle,
6
+ // Named so the emitted marks types reach these through the sdk, not core.
7
+ type CornerRadiusResolved as _CornerRadiusResolved,
8
+ type CornerStyleResolved as _CornerStyleResolved,
9
+ type RectCornerRadius as _RectCornerRadius,
10
+ type RectCornerStyle as _RectCornerStyle,
14
11
  } from "@motionscript/sdk"
15
- import { niceStep } from "@motionscript/sdk/component"
16
12
 
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"
13
+ import { niceRange, num, text, type ChartSeries, type Range } from "../kit/shared"
42
14
 
43
15
  /** 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
16
  export type BarSeries = ChartSeries
45
17
 
18
+ /** How the series share a category: side by side, stacked, or stacked as shares of 100. */
19
+ export type BarStack = "none" | "stacked" | "percent"
20
+
21
+ /** Which way the bars grow: up from a category row, or right from a category column. */
22
+ export type BarOrientation = "vertical" | "horizontal"
23
+
46
24
  /**
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.
25
+ * One bar's span in data units. A span rather than a height because every mode
26
+ * fits it, and a negative value just runs the other way with no special case.
49
27
  */
50
28
  export interface BarSpan {
51
- /** Where the base sits: 0 when grouped, the running total below when stacked. */
29
+ /** Where the base sits: 0 apart, the running total below (or above, for a negative) when stacked. */
52
30
  from: number
53
31
  to: number
54
- /** `to - from`, kept for the figure printed on the bar. */
32
+ /** `to - from`. */
55
33
  value: number
34
+ /** Whether it ends its stack on its side of zero, so its far end is rounded. */
35
+ outer: boolean
56
36
  }
57
37
 
58
38
  /**
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.
39
+ * One row's spans, in series order. Stacking keeps a running sum on each side
40
+ * of zero, so a negative value stacks downward from the baseline rather than
41
+ * eating into the positives. In `percent` each value is its share of the row's
42
+ * total magnitude. A zero value still yields a degenerate span, keeping the list
43
+ * index-aligned with the series. `weights` scale each series' contribution.
82
44
  */
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
45
+ export function barSpans(row: DataRecord, series: readonly BarSeries[], stack: BarStack, weights: readonly number[] = []): BarSpan[] {
46
+ const weight = (i: number) => Math.max(0, weights[i] ?? 1)
47
+ if (stack !== "stacked" && stack !== "percent") {
48
+ return series.map((s) => {
49
+ const value = num(row, s.field)
50
+ return { from: 0, to: value, value, outer: true }
51
+ })
87
52
  }
88
- return last
53
+ const values = series.map((s, i) => num(row, s.field) * weight(i))
54
+ const total = values.reduce((sum, v) => sum + Math.abs(v), 0)
55
+ const scaled = stack === "percent" ? values.map((v) => (total > 0 ? (v / total) * 100 : 0)) : values
56
+ let lastUp = -1
57
+ let lastDown = -1
58
+ scaled.forEach((v, i) => {
59
+ if (v > 0) lastUp = i
60
+ else if (v < 0) lastDown = i
61
+ })
62
+ let up = 0
63
+ let down = 0
64
+ return scaled.map((value, i) => {
65
+ if (value < 0) {
66
+ const from = down
67
+ down += value
68
+ return { from, to: down, value, outer: i === lastDown }
69
+ }
70
+ const from = up
71
+ up += value
72
+ return { from, to: up, value, outer: i === lastUp }
73
+ })
89
74
  }
90
75
 
91
76
  export function barCategories(data: DataRecord[], field: string): string[] {
@@ -94,18 +79,14 @@ export function barCategories(data: DataRecord[], field: string): string[] {
94
79
 
95
80
  /**
96
81
  * 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
82
+ * at 40 draws a bar three times its neighbour for a 10% difference. A cropped
98
83
  * axis takes an explicit range.
99
84
  */
100
- export function barValueExtent(
101
- data: DataRecord[],
102
- series: BarSeries[],
103
- stacked: boolean
104
- ): Range {
85
+ export function barValueExtent(data: DataRecord[], series: readonly BarSeries[], stack: BarStack, weights?: readonly number[]): Range {
105
86
  let lo = 0
106
87
  let hi = 0
107
88
  for (const row of data) {
108
- for (const span of barSpans(row, series, stacked)) {
89
+ for (const span of barSpans(row, series, stack, weights)) {
109
90
  lo = Math.min(lo, span.from, span.to)
110
91
  hi = Math.max(hi, span.from, span.to)
111
92
  }
@@ -113,174 +94,48 @@ export function barValueExtent(
113
94
  return lo < hi ? [lo, hi] : [0, 1]
114
95
  }
115
96
 
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. */
97
+ /** 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
98
  export function resolveBarScale(
118
99
  data: DataRecord[],
119
- series: BarSeries[],
120
- stacked: boolean,
100
+ series: readonly BarSeries[],
101
+ stack: BarStack,
121
102
  range: Range | undefined,
122
- divisions: number
103
+ divisions: number,
104
+ weights?: readonly number[]
123
105
  ): Range {
124
106
  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
107
+ const [lo, hi] = barValueExtent(data, series, stack, weights)
108
+ if (stack === "percent") return [lo < 0 ? -100 : 0, hi > 0 ? 100 : 0]
109
+ return niceRange([lo, hi], divisions)
141
110
  }
142
111
 
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 }
112
+ /** Black or white, whichever reads on `fill`: a figure printed inside a bar. */
113
+ export function inkOn(fill: Fill): string {
114
+ const layers = fillOps.resolve(fill)
115
+ const top = layers[layers.length - 1]
116
+ if (top?.type !== "solid") return "white"
117
+ const [r, g, b] = top.color
118
+ const lift = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
119
+ const luminance = 0.2126 * lift(r) + 0.7152 * lift(g) + 0.0722 * lift(b)
120
+ return luminance > 0.4 ? "#0a0a0a" : "white"
158
121
  }
159
122
 
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
- }
123
+ /** A bar chart's own look: its corners, and how wide the bars are. */
124
+ export const BAR_MARKS = {
125
+ /**
126
+ * Each corner as for a bar standing up from the baseline: the top pair round
127
+ * its far end, the bottom pair its base. Inside a stack, a segment's far end
128
+ * stays square unless it ends the stack, and its base unless it starts it.
129
+ */
130
+ barCornerRadius: Types.cornerRadius({ topLeft: 10, topRight: 10, bottomRight: 0, bottomLeft: 0 }),
131
+ barCornerStyle: Types.cornerStyle("rounded"),
132
+ /** How much of its category slot a bar group fills, the rest spacing the groups. */
133
+ barWidth: Types.number(0.7, { min: 0.05, max: 1, step: 0.05, unit: "%", scale: 100 }),
134
+ /** Space between the bars of one group, side by side. */
135
+ barGap: Types.number(6, { min: 0, max: 40, step: 1, unit: "px" }),
275
136
  }
276
137
 
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]
138
+ export const BAR_LABELS = {
139
+ /** Read as the chart is built: each bar's value past its end, inside it, or none. */
140
+ values: Types.oneOf(["none", "end", "inside"] as const, { animatable: false }),
286
141
  }
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,99 @@
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 { sizeOf, type CartesianStyle, type 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, "legendText">): number {
19
+ return Math.round(sizeOf(style.legendText) * 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, "yLabelText">): number {
24
+ return Math.ceil(sizeOf(style.yLabelText) * 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, "xLabelText">): number {
29
+ return Math.ceil(sizeOf(style.xLabelText) * 1.1)
30
+ }
11
31
 
12
32
  interface CartesianParts {
13
- readonly axis: Node2D
33
+ /** Behind the whole chart, axes and legend included. */
34
+ readonly background?: () => Fill
35
+ /** The axis beside the plot, on its left. */
36
+ readonly left?: Node2D
37
+ /** An invisible twin of `left` under the plot, so the row there starts at the plot's left edge. */
38
+ readonly ghost?: Node2D
14
39
  /** The box the grid and the marks share. */
15
40
  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[]
41
+ /** The axis under the plot. */
42
+ readonly bottom?: Node2D
43
+ readonly legend?: Node2D
44
+ readonly legendAt: LegendPosition
45
+ readonly gap: () => number
46
+ readonly padding: () => Insets
20
47
  }
21
48
 
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. */
49
+ /**
50
+ * The frame the line and bar charts share. A legend above or below is centred
51
+ * on the whole chart; one on the right is a column beside the plot. An axis the
52
+ * style turns off is simply not here, and the plot takes its room.
53
+ */
23
54
  export function cartesianFrame(parts: CartesianParts): Node2D {
55
+ const top = new Rect({ flow: "horizontal", width: "fill", height: "fill", children: [...(parts.left ? [parts.left] : []), parts.plot] })
56
+ const rows: Node2D[] = [top as unknown as Node2D]
57
+ if (parts.bottom) {
58
+ rows.push(new Rect({ flow: "horizontal", width: "fill", height: "hug", children: [...(parts.ghost ? [parts.ghost] : []), parts.bottom] }) as unknown as Node2D)
59
+ }
60
+ const body = new Rect({ flow: "vertical", width: "fill", height: "fill", children: rows })
61
+ const legend = parts.legend && parts.legendAt !== "none" ? parts.legend : undefined
62
+ const main = new Rect({
63
+ flow: "horizontal",
64
+ width: "fill",
65
+ height: "fill",
66
+ align: "center",
67
+ gap: parts.gap,
68
+ children: [body, ...(legend && parts.legendAt === "right" ? [legend] : [])],
69
+ })
70
+ const across = (node: Node2D) => new Rect({ flow: "horizontal", width: "fill", height: "hug", align: "center", children: [node] })
24
71
  return new Rect({
25
72
  flow: "vertical",
26
73
  width: "fill",
27
74
  height: "fill",
28
- padding: { top: TOP_HEADROOM },
75
+ gap: parts.gap,
76
+ padding: parts.padding,
77
+ ...(parts.background ? { fill: parts.background } : {}),
29
78
  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
- }),
79
+ ...(legend && parts.legendAt === "top" ? [across(legend)] : []),
80
+ main,
81
+ ...(legend && parts.legendAt === "bottom" ? [across(legend)] : []),
36
82
  ],
37
83
  }) as unknown as Node2D
38
84
  }
39
85
 
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 }
86
+ /** Every view under `children`, depth first in build order: the axis beside the plot comes before its twin, which holds no parts. */
87
+ export function chartViews(children: readonly ChildView[]): ChildView[] {
88
+ const out: ChildView[] = []
89
+ const walk = (views: readonly ChildView[]) => {
90
+ for (const view of views) {
91
+ out.push(view)
92
+ walk(view.children)
93
+ }
58
94
  }
59
- return null
95
+ walk(children)
96
+ return out
60
97
  }
61
98
 
62
99
  /** A part's laid-out size; zero before it is built or laid out. */
@@ -64,3 +101,47 @@ export function laidOutSize(node: Node2D | undefined): Size2D {
64
101
  const bounds = node?.layoutBounds
65
102
  return { width: bounds?.width ?? 0, height: bounds?.height ?? 0 }
66
103
  }
104
+
105
+ /**
106
+ * A text style as props bound to `read`, so a style tween reaches text built
107
+ * once. Only the keys set at build are bound: an unset family stays unset, so
108
+ * the host's default face still applies.
109
+ */
110
+ export function boundText(read: () => TextStyle): Record<string, unknown> {
111
+ const keys = Object.keys(textOps.styleProps(read())) as (keyof TextStyle)[]
112
+ const props: Record<string, unknown> = {}
113
+ for (const key of keys) props[key] = () => read()[key]
114
+ return props
115
+ }
116
+
117
+ interface GhostSpec {
118
+ readonly captions: () => readonly string[]
119
+ /** How many captions the real axis built. */
120
+ readonly count: number
121
+ readonly text: () => TextStyle
122
+ readonly title: string
123
+ readonly titleText: () => TextStyle
124
+ readonly tickPad: () => number
125
+ readonly titlePad: () => number
126
+ }
127
+
128
+ /**
129
+ * An invisible column as wide as a vertical axis built from the same captions:
130
+ * the widest caption plus the tick padding, and the rotated title before it. A
131
+ * plain tree, so a command looking for the axis never finds this instead.
132
+ */
133
+ export function columnGhost(spec: GhostSpec): Node2D {
134
+ const captions = Array.from({ length: spec.count }, (_, i) => new Text({ ...boundText(spec.text), text: () => spec.captions()[i] ?? "" }))
135
+ const children: Node2D[] = []
136
+ if (spec.title !== "") {
137
+ children.push(new RotatedBox({ angle: -90, width: "hug", height: "hug", children: new Text({ ...boundText(spec.titleText), text: spec.title }) }) as unknown as Node2D)
138
+ }
139
+ children.push(new Rect({ flow: "vertical", width: "hug", height: "hug", padding: () => ({ right: spec.tickPad() }), children: captions }) as unknown as Node2D)
140
+ return new Rect({ opacity: 0, flow: "horizontal", gap: spec.titlePad, width: "hug", height: 0, children }) as unknown as Node2D
141
+ }
142
+
143
+ /** `fill` with every layer's opacity scaled by `k`. */
144
+ export function fade(fill: Fill, k: number): Fill {
145
+ const layers = (Array.isArray(fill) ? fill : [fill]) as FillResolved[]
146
+ return layers.map((layer) => (typeof layer === "object" && layer !== null ? { ...layer, opacity: ((layer as { opacity?: number }).opacity ?? 1) * k } : layer)) as Fill
147
+ }