@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
@@ -4,8 +4,6 @@ import {
4
4
  Types,
5
5
  clamp,
6
6
  component2d,
7
- insetsOps,
8
- textOps,
9
7
  type NodeOf,
10
8
  // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
11
9
  type Node2D,
@@ -13,21 +11,25 @@ import {
13
11
  } from "@motionscript/sdk"
14
12
  import { enterBudget } from "@motionscript/sdk/component"
15
13
 
14
+ import { boundText } from "../kit/cartesian"
16
15
  import { asParts, fresh } from "../kit/entries"
17
16
  import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
18
- import {
19
- DEFAULT_X_AXIS,
20
- type AxisConfig,
21
- } from "../kit/shared"
17
+ import { DEFAULT_X_AXIS, type AxisConfig } from "../kit/shared"
18
+ import { axisPads, captionColumn } from "../kit/vertical-axis"
22
19
 
23
20
  const SLIDE = 24
24
21
 
22
+ /** Slot `index`'s centre along a run of `length` px divided into `count` equal slots, first slot at `-length / 2`. */
23
+ export function slotCentre(index: number, count: number, length: number): number {
24
+ return count <= 0 ? 0 : -length / 2 + (length / count) * (index + 0.5)
25
+ }
26
+
25
27
  /**
26
28
  * The categorical axis of a {@link BarChart}: one {@link Text} caption per row,
27
- * centred on its slot. Not folded into `HorizontalAxis`, which places markers by
28
- * value on a scale; this divides its width into equal slots. It is laid out to
29
- * exactly the plot's width, so each caption lands under its columns with neither
30
- * side told about the other. The resting state is landed.
29
+ * centred on its slot. `horizontal` is a row under the plot, first category on
30
+ * the left; `vertical` a right-aligned column beside it, first category at the
31
+ * top. It is laid out to exactly the plot's length, so each caption lands by its
32
+ * bars with neither side told about the other. The resting state is landed.
31
33
  */
32
34
  export const CategoryAxis = component2d({
33
35
  key: "categoryAxis",
@@ -35,64 +37,69 @@ export const CategoryAxis = component2d({
35
37
  categories: fresh<string[]>(() => []),
36
38
  axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
37
39
  label: "",
38
- /** Per-category opacity multiplier, so the caption under a dimmed column dims with it. Missing reads as 1. */
40
+ orientation: Types.oneOf(["horizontal", "vertical"] as const),
41
+ /** Per-category opacity multiplier, so the caption by a dimmed bar dims with it. Missing reads as 1. */
39
42
  dims: fresh<number[]>(() => []),
40
43
  arrivals: arrivalsEntry(),
41
44
  },
42
45
  // A node made of captions would otherwise hug them.
43
46
  defaults: { width: "fill", height: "fill" },
44
47
  children: (state) => {
45
- const tickPad = insetsOps.resolve(state.axis.tickPadding).top
46
- const titlePad = insetsOps.resolve(state.axis.titlePadding).top
47
-
48
- const column: Node2D[] = []
49
- // The column's parent is the axis: its box is what the slots divide.
50
- const width = () => (column[0]?.parent as Node2D | null | undefined)?.layoutBounds.width ?? 0
51
- const slotCenter = (index: number) => {
52
- const n = state.categories.length
53
- if (n <= 0) return 0
54
- return -width() / 2 + (width() / n) * (index + 0.5)
48
+ const n = state.categories.length
49
+ const dim = (i: number) => clamp(state.dims[i] ?? 1, 0, 1)
50
+ if (state.orientation === "vertical") {
51
+ return asParts([
52
+ captionColumn({
53
+ count: n,
54
+ axis: () => state.axis,
55
+ text: (i) => state.categories[i] ?? "",
56
+ // y-up: the first category takes the top slot.
57
+ place: (i, height) => -slotCentre(i, state.categories.length, height),
58
+ arrivals: () => state.arrivals,
59
+ opacity: dim,
60
+ label: state.label,
61
+ }),
62
+ ])
55
63
  }
56
64
 
65
+ const pads = () => axisPads(state.axis, "top")
57
66
  // The spotlight dim sits on a hug box around each caption, so the entrance animates the text's own opacity without the two fighting over one value.
58
- const slots = state.categories.map((caption, i) =>
59
- new Rect({
67
+ const captions = state.categories.map((caption, i) => {
68
+ const slot: Node2D = new Rect({
60
69
  width: "hug",
61
70
  height: "hug",
62
71
  flow: "freeform",
63
- opacity: () => clamp(state.dims[i] ?? 1, 0, 1),
64
- x: () => slotCenter(i),
72
+ opacity: () => dim(i),
73
+ x: () => slotCentre(i, state.categories.length, (slot.parent as Node2D | null)?.layoutBounds.width ?? 0),
65
74
  children: [
66
75
  new Text({
67
- ...textOps.styleProps(state.axis.labelStyle),
76
+ ...boundText(() => state.axis.labelStyle),
68
77
  text: caption,
69
78
  opacity: () => arrivalOf(state.arrivals, i).opacity,
70
79
  y: () => arrivalOf(state.arrivals, i).shift,
71
80
  }),
72
81
  ],
73
- })
74
- )
75
-
76
- const children: Node2D[] = [new Rect({ width: "fill", height: "hug", padding: { top: tickPad }, children: slots })]
82
+ }) as unknown as Node2D
83
+ return slot
84
+ })
85
+ const children: Node2D[] = [new Rect({ width: "fill", height: "hug", padding: () => ({ top: pads().tick }), children: captions }) as unknown as Node2D]
77
86
  if (state.label !== "") {
78
87
  children.push(
79
88
  new Text({
80
- ...textOps.styleProps(state.axis.titleStyle),
89
+ ...boundText(() => state.axis.titleStyle),
81
90
  text: state.label,
82
91
  opacity: () => arrivalOf(state.arrivals, "label").opacity,
83
92
  y: () => arrivalOf(state.arrivals, "label").shift,
84
- })
93
+ }) as unknown as Node2D
85
94
  )
86
95
  }
87
-
88
- column.push(new Rect({ flow: "vertical", gap: titlePad, height: "hug", width: "fill", children }))
89
- return asParts(column)
96
+ return asParts([new Rect({ flow: "vertical", gap: () => pads().title, height: "hug", width: "fill", children }) as unknown as Node2D])
90
97
  },
91
98
  commands: {
92
99
  /**
93
- * Slide and fade captions in left to right, then the title. The duration is
94
- * the whole entrance ({@link enterBudget}). `stagger` is seconds between
95
- * captions (derived when omitted); `slide` is the starting offset in px.
100
+ * Slide and fade captions in, first category first, then the title. The
101
+ * duration is the whole entrance ({@link enterBudget}). `stagger` is seconds
102
+ * between captions (derived when omitted); `slide` is the starting offset in px.
96
103
  */
97
104
  enter: {
98
105
  args: {
@@ -109,9 +116,8 @@ export const CategoryAxis = component2d({
109
116
  if (count === 0) return arm
110
117
  const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
111
118
  const ease = frameEase(timeline)
112
- const lastCaptionEnds = (count - 1) * step + per
113
119
  const captions = state.categories.map((_, i) => ({ at: i * step, duration: per, piece: arrival(timeline, i, from, ease) }))
114
- const label = captioned ? [{ at: lastCaptionEnds, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
120
+ const label = captioned ? [{ at: (count - 1) * step + per, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
115
121
  return timeline.together([arm, beats(timeline, [...captions, ...label])])
116
122
  },
117
123
  },
@@ -5,168 +5,149 @@ import {
5
5
  clamp,
6
6
  component2d,
7
7
  fillOps,
8
- shadowOps,
9
- strokeOps,
10
- textOps,
11
8
  type Fill,
12
9
  type FillResolved,
13
10
  type NodeOf,
14
11
  // Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
15
- type Node2D as _Node2D,
12
+ type Node2D,
16
13
  type Node2DProps as _Node2DProps,
17
- type Shadow,
18
- type ShadowResolved,
19
- type Stroke,
20
- type StrokeResolved,
21
14
  type TextStyle,
22
15
  } from "@motionscript/sdk"
23
16
 
24
- import { fresh } from "../kit/entries"
17
+ import { boundText } from "../kit/cartesian"
18
+ import { asParts, fresh } from "../kit/entries"
25
19
  import { frameEase } from "../kit/motion"
26
20
  import { loadPaints } from "../kit/paints"
27
21
  import { formatMarker } from "../kit/shared"
28
- import { DEFAULT_BAR_CHART_THEME } from "./shared"
22
+ import { inkOn } from "./shared"
29
23
 
30
24
  /**
31
- * One drawn column in the plot's centred pixel space (origin at the centre,
32
- * y-up), mapped by the parent {@link BarChart}. `from`/`to` are the two ends
33
- * rather than a base and a height, like {@link BarSpan}: one shape fits grouped,
34
- * stacked and negative.
25
+ * One drawn bar in the plot's centred pixel space (origin at the centre, y-up),
26
+ * mapped by the parent {@link BarChart}: `center` and `thickness` across the
27
+ * category slot, `from` and `to` along the value axis (y for a column, x for a
28
+ * horizontal bar). Two ends rather than a base and a length, like `BarSpan`:
29
+ * one shape fits grouped, stacked and negative.
35
30
  */
36
31
  export interface BarGeometry {
37
- /** Centre x. */
38
- x: number
39
- width: number
40
- /** Pixel y of the base: the baseline when grouped, the running stack top when stacked. */
32
+ center: number
33
+ thickness: number
34
+ /** The base: the baseline when grouped, the running stack end when stacked. */
41
35
  from: number
42
- /** Pixel y of the far end at full growth. */
36
+ /** The far end at full growth. */
43
37
  to: number
44
- /** Whether the far end takes the corner radius (the stack's cap). */
38
+ /** Whether the far end takes the corner radius (the stack's last segment on its side). */
45
39
  round: boolean
46
- /** The value plotted, for the printed figure. */
47
- value: number
48
- /** The printed figure, written as the value axis writes its ticks; absent, the bare marker reading of `value`. */
49
- caption?: string
40
+ /** The figure printed by the bar; empty for none. */
41
+ caption: string
50
42
  }
51
43
 
52
- /** Gap between a grouped column's end and the figure printed over it, in px. */
53
- const VALUE_GAP = 10
54
- /** A segment shorter than this many multiples of the type size prints no figure. */
55
- const VALUE_FIT = 1.6
56
- /** Fallback type size when the theme's value style leaves it unset or autofit. */
57
- const VALUE_SIZE = 24
44
+ /** A figure inside a bar needs this much room around it, in multiples of the type size. */
45
+ const INSIDE_ROOM = 0.6
58
46
 
59
- interface Columns {
47
+ interface Bars {
60
48
  readonly bars: readonly BarGeometry[]
61
49
  readonly baseline: number
50
+ readonly horizontal: boolean
62
51
  readonly growth: number
63
52
  readonly dims: readonly number[]
64
53
  readonly enabled: boolean
54
+ readonly values: "none" | "end" | "inside"
65
55
  readonly valueStyle: TextStyle
66
56
  }
67
57
 
68
- /**
69
- * Column `i`'s dimming: the category spotlight, then the series switch. Captions
70
- * read this because they are child nodes, which a `render` guard would leave on
71
- * screen over a series that is not there.
72
- */
73
- function dimAt(s: Columns, index: number): number {
58
+ /** Bar `i`'s dimming: the category spotlight, then the series switch. */
59
+ function dimAt(s: Bars, index: number): number {
74
60
  if (!s.enabled) return 0
75
61
  return clamp(s.dims[index] ?? 1, 0, 1)
76
62
  }
77
63
 
78
- function endsAt(s: Columns, index: number): { base: number; end: number } | null {
64
+ /** Bar `i`'s two ends as grown so far, along the value axis. */
65
+ function endsAt(s: Bars, index: number): { base: number; end: number } | null {
79
66
  const bar = s.bars[index]
80
67
  if (!bar) return null
81
68
  const g = clamp(s.growth, 0, 1)
82
69
  return { base: s.baseline + (bar.from - s.baseline) * g, end: s.baseline + (bar.to - s.baseline) * g }
83
70
  }
84
71
 
85
- function valueSize(s: Columns): number {
86
- const size = s.valueStyle.fontSize
87
- return typeof size === "number" ? size : VALUE_SIZE
72
+ function sizeOf(style: TextStyle): number {
73
+ return typeof style.fontSize === "number" ? style.fontSize : 24
88
74
  }
89
75
 
90
- function insideAt(s: Columns, index: number): boolean {
91
- const bar = s.bars[index]
92
- return bar !== undefined && Math.abs(bar.from - s.baseline) > 0.5
76
+ /** Where bar `i`'s figure sits along the value axis: past its end, or in its middle. */
77
+ function captionAlong(s: Bars, index: number, extent: number): number {
78
+ const ends = endsAt(s, index)
79
+ if (!ends) return 0
80
+ if (s.values === "inside") return (ends.base + ends.end) / 2
81
+ const direction = ends.end >= ends.base ? 1 : -1
82
+ return ends.end + direction * (sizeOf(s.valueStyle) * 0.4 + extent / 2)
93
83
  }
94
84
 
95
- /** Column `i`'s figure, or `""` where there is no room. Blanking rather than dropping the node keeps captions index-aligned, so a rescale into more room prints again. */
96
- function captionAt(s: Columns, index: number): string {
85
+ /** Whether bar `i` has room for a figure `extent` long inside it. */
86
+ function fitsInside(s: Bars, index: number, extent: number, across: number): boolean {
97
87
  const bar = s.bars[index]
98
- if (!bar || bar.value === 0) return ""
99
- // An interior segment prints inside itself and must hold a line of type; a grouped column prints past its end.
100
- if (insideAt(s, index)) {
101
- const ends = endsAt(s, index)
102
- if (!ends) return ""
103
- if (Math.abs(ends.end - ends.base) < valueSize(s) * VALUE_FIT) return ""
104
- }
105
- return bar.caption ?? formatMarker(bar.value)
106
- }
107
-
108
- function captionY(s: Columns, index: number): number {
109
88
  const ends = endsAt(s, index)
110
- if (!ends) return 0
111
- if (insideAt(s, index)) return (ends.base + ends.end) / 2
112
- // Text is centred on `y`: clear the end by the gap plus half a line, on the side the column reached.
113
- const direction = ends.end >= ends.base ? 1 : -1
114
- return ends.end + direction * (VALUE_GAP + valueSize(s) / 2)
89
+ if (!bar || !ends) return false
90
+ const room = sizeOf(s.valueStyle) * INSIDE_ROOM
91
+ return Math.abs(ends.end - ends.base) >= extent + room && bar.thickness >= across
115
92
  }
116
93
 
117
94
  /**
118
- * One series of a {@link BarChart}: its column in every category plus the
119
- * printed figures. A node per series, like `ChartLine`, so `showSeries` and
95
+ * One series of a {@link BarChart}: its bar in every category plus the printed
96
+ * figures. A node per series, like `ChartLine`, so `showSeries` and
120
97
  * `spotlightSeries` mean the same on both: enable the node or tween its opacity.
121
98
  * Positions come from the parent, which owns the slot arithmetic.
122
- *
123
- * The figures are {@link Text} children, so they take the theme's type style,
124
- * are measured by layout, and hide with the series.
125
99
  */
126
100
  export const ChartBars = component2d({
127
101
  key: "chartBars",
128
102
  state: {
129
103
  bars: fresh<BarGeometry[]>(() => []),
130
- /** Pixel y of value 0, where every column grows from. */
104
+ /** Pixel position of value 0 along the value axis, where every bar grows from. */
131
105
  baseline: 0,
106
+ /** Bars grow along x from a category column rather than along y from a row. */
107
+ horizontal: false,
132
108
  fill: Types.custom<Fill, FillResolved[]>([], { resolve: fillOps.resolve, lerp: fillOps.lerp }),
133
- barStroke: Types.custom<Stroke, StrokeResolved[]>(DEFAULT_BAR_CHART_THEME.bar.stroke, { resolve: strokeOps.resolve, lerp: strokeOps.lerp }),
134
- barShadow: Types.custom<Shadow, ShadowResolved[]>(DEFAULT_BAR_CHART_THEME.bar.shadow, { resolve: shadowOps.resolve, lerp: shadowOps.lerp }),
135
- cornerRadius: DEFAULT_BAR_CHART_THEME.bar.cornerRadius,
109
+ cornerRadius: 8,
136
110
  /**
137
- * Rise of every column in `[0, 1]` about the baseline, not each column's own
138
- * base: every segment travels by the same factor, so a stack stays flush
139
- * mid-animation and a negative column grows down.
111
+ * Rise of every bar in `[0, 1]` about the baseline, not each bar's own base:
112
+ * every segment travels by the same factor, so a stack stays flush
113
+ * mid-animation and a negative bar grows the other way.
140
114
  */
141
115
  growth: 1,
142
- /** Per-column opacity by row index, how the category spotlight reaches one column. Missing reads as 1. */
116
+ /** Per-bar opacity by row index, how the category spotlight reaches one bar. Missing reads as 1. */
143
117
  dims: fresh<number[]>(() => []),
144
- showValues: false,
145
- valueStyle: Types.custom<TextStyle>(DEFAULT_BAR_CHART_THEME.valueStyle),
146
- /** The wholesale switch behind `showSeries`/`hideSeries`; off skips the columns and their figures. */
118
+ /** Read as built: whether each bar prints its figure, and where. */
119
+ values: Types.custom<"none" | "end" | "inside">("none"),
120
+ valueStyle: Types.custom<TextStyle>({ fontSize: 24, fill: "white" }),
121
+ /** The wholesale switch behind `showSeries`/`hideSeries`; off skips the bars and their figures. */
147
122
  enabled: true,
148
123
  },
149
124
  // A node made of its figures would otherwise hug them.
150
125
  defaults: { width: "fill", height: "fill" },
151
- // One caption per column, built once (the row count is fixed at construction)
152
- // and bound to the live geometry so it travels with its column.
126
+ // One figure per bar, built once (the row count is fixed at construction)
127
+ // and bound to the live geometry so it travels with its bar.
153
128
  children: (state) => {
154
- if (!state.showValues) return []
155
- return state.bars.map((_, i) =>
156
- Text({
157
- ...textOps.styleProps(state.valueStyle),
158
- text: () => captionAt(state, i),
159
- x: () => state.bars[i]?.x ?? 0,
160
- y: () => captionY(state, i),
161
- opacity: () => dimAt(state, i),
162
- })
163
- )
129
+ if (state.values === "none") return []
130
+ return asParts(state.bars.map((_, i) => {
131
+ const caption: Node2D = new Text({
132
+ ...boundText(() => ({ ...state.valueStyle, ...(state.values === "inside" && { fill: inkOn(state.fill) }) })),
133
+ text: () => state.bars[i]?.caption ?? "",
134
+ x: () => (state.horizontal ? captionAlong(state, i, caption.layoutBounds.width) : (state.bars[i]?.center ?? 0)),
135
+ y: () => (state.horizontal ? (state.bars[i]?.center ?? 0) : captionAlong(state, i, caption.layoutBounds.height)),
136
+ opacity: () => {
137
+ const box = caption.layoutBounds
138
+ const along = state.horizontal ? box.width : box.height
139
+ const across = state.horizontal ? box.height : box.width
140
+ if (state.values === "inside" && !fitsInside(state, i, along, across)) return 0
141
+ // A figure past the end rises in with its bar's last stretch.
142
+ return dimAt(state, i) * clamp((state.growth - 0.6) / 0.4, 0, 1)
143
+ },
144
+ }) as unknown as Node2D
145
+ return caption
146
+ }))
164
147
  },
165
- // The stroke and shadow sit past the conventional slots, so they are named here.
166
- load: (loader, state) =>
167
- loadPaints(loader, { fills: [state.fill], shadows: [state.barShadow], strokes: [state.barStroke] }),
148
+ load: (loader, state) => loadPaints(loader, { fills: [state.fill] }),
168
149
  commands: {
169
- /** Raise every column from the baseline (`growth` 0 to 1). The resting state is the finished one; the chart arms it back to 0. */
150
+ /** Raise every bar from the baseline (`growth` 0 to 1). The resting state is the finished one; the chart arms it back to 0. */
170
151
  enter: (timeline) => {
171
152
  const ease = frameEase(timeline)
172
153
  return timeline.animate((t) => ({ growth: ease(t) }))
@@ -177,29 +158,23 @@ export const ChartBars = component2d({
177
158
  for (let i = 0; i < state.bars.length; i++) {
178
159
  const bar = state.bars[i]
179
160
  const ends = endsAt(state, i)
180
- if (!ends || bar.width <= 0) continue
181
- const height = Math.abs(ends.end - ends.base)
182
- if (height <= 0) continue
183
-
184
- const radius = bar.round ? Math.min(state.cornerRadius, height) : 0
185
- // One `Graphics2D` per column: opacity belongs to the command list, and the spotlight dims single columns.
186
- const g = new Graphics2D()
187
- const rect = g.rect({
188
- x: bar.x,
189
- y: (ends.base + ends.end) / 2,
190
- width: bar.width,
191
- height,
192
- // Corner keys are visual (y-up): a column reaching up rounds `top*`, one reaching down `bottom*`. Only the far end rounds.
193
- cornerRadius:
194
- radius > 0
195
- ? ends.end >= ends.base
196
- ? { topLeft: radius, topRight: radius, bottomLeft: 0, bottomRight: 0 }
197
- : { bottomLeft: radius, bottomRight: radius, topLeft: 0, topRight: 0 }
198
- : 0,
199
- })
200
- rect.shadow(state.barShadow)
201
- rect.fill(state.fill)
202
- rect.stroke(state.barStroke)
161
+ if (!ends || bar.thickness <= 0) continue
162
+ // A segment inside a stack reaches a pixel under the next, so no seam of background shows between them.
163
+ const seam = bar.round ? 0 : 1
164
+ const length = Math.abs(ends.end - ends.base) + seam
165
+ if (length <= seam) continue
166
+ const radius = bar.round ? Math.max(0, Math.min(state.cornerRadius, length, bar.thickness / 2)) : 0
167
+ const forward = ends.end >= ends.base
168
+ const mid = (ends.base + ends.end) / 2 + (forward ? seam : -seam) / 2
169
+ // Corner keys are visual (y-up): only the far end rounds.
170
+ const far = state.horizontal
171
+ ? (forward ? { topRight: radius, bottomRight: radius, topLeft: 0, bottomLeft: 0 } : { topLeft: radius, bottomLeft: radius, topRight: 0, bottomRight: 0 })
172
+ : (forward ? { topLeft: radius, topRight: radius, bottomLeft: 0, bottomRight: 0 } : { bottomLeft: radius, bottomRight: radius, topLeft: 0, topRight: 0 })
173
+ // One `Graphics2D` per bar: opacity belongs to the command list, and the spotlight dims single bars.
174
+ const g = new Graphics2D().rect(state.horizontal
175
+ ? { x: mid, y: bar.center, width: length, height: bar.thickness, cornerRadius: radius > 0 ? far : 0 }
176
+ : { x: bar.center, y: mid, width: bar.thickness, height: length, cornerRadius: radius > 0 ? far : 0 })
177
+ g.fill(state.fill)
203
178
  ctx.draw(g.opacity(dimAt(state, i)))
204
179
  }
205
180
  }
@@ -208,3 +183,8 @@ export const ChartBars = component2d({
208
183
  })
209
184
 
210
185
  export type ChartBars = NodeOf<typeof ChartBars>
186
+
187
+ /** A figure as the bar prints it when the chart gives no caption. */
188
+ export function barCaption(value: number): string {
189
+ return value === 0 ? "" : formatMarker(value)
190
+ }
@@ -1,23 +1,20 @@
1
1
  export { BarChart, barChartDomain } from "./bar-chart"
2
- export type { BarChartDomain, BarChartValues } from "./bar-chart"
2
+ export type { BarChartDomain, BarChartStyle, BarChartValues } from "./bar-chart"
3
3
  export { ChartBars } from "./chart-bars"
4
4
  export type { BarGeometry } from "./chart-bars"
5
- export { CategoryAxis } from "./category-axis"
5
+ export { CategoryAxis, slotCentre } from "./category-axis"
6
6
  export {
7
- DEFAULT_BAR_CHART_THEME,
7
+ BAR_LABELS,
8
+ BAR_MARKS,
8
9
  barCategories,
9
10
  barSpans,
10
11
  barValueExtent,
11
- lerpBarChartTheme,
12
- resolveBarChartTheme,
13
- resolveBarFill,
12
+ inkOn,
14
13
  resolveBarScale,
15
- stackCapIndex,
16
14
  } from "./shared"
17
15
  export type {
18
- BarChartConfig,
19
- BarChartTheme,
16
+ BarOrientation,
20
17
  BarSeries,
21
18
  BarSpan,
22
- BarStyleConfig,
19
+ BarStack,
23
20
  } from "./shared"