@motionscript/charts 0.1.0-alpha.3 → 0.1.0-alpha.6

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 (154) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +201 -201
  3. package/NOTICE +48 -0
  4. package/README.md +99 -0
  5. package/dist/bar-chart/bar-chart.d.ts +93 -231
  6. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  7. package/dist/bar-chart/bar-chart.js +378 -565
  8. package/dist/bar-chart/bar-chart.js.map +1 -1
  9. package/dist/bar-chart/category-axis.d.ts +24 -41
  10. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  11. package/dist/bar-chart/category-axis.js +93 -115
  12. package/dist/bar-chart/category-axis.js.map +1 -1
  13. package/dist/bar-chart/chart-bars.d.ts +36 -70
  14. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  15. package/dist/bar-chart/chart-bars.js +156 -210
  16. package/dist/bar-chart/chart-bars.js.map +1 -1
  17. package/dist/bar-chart/index.d.ts +2 -2
  18. package/dist/bar-chart/index.d.ts.map +1 -1
  19. package/dist/bar-chart/index.js +1 -1
  20. package/dist/bar-chart/index.js.map +1 -1
  21. package/dist/bar-chart/shared.d.ts +6 -6
  22. package/dist/bar-chart/shared.d.ts.map +1 -1
  23. package/dist/bar-chart/shared.js +6 -6
  24. package/dist/bar-chart/shared.js.map +1 -1
  25. package/dist/browser/chunks/chunk-XROBIRQK.js +2 -0
  26. package/dist/browser/chunks/chunk-XROBIRQK.js.map +7 -0
  27. package/dist/browser/index.js +1 -1
  28. package/dist/browser/index.js.map +3 -3
  29. package/dist/browser/kit.js +1 -1
  30. package/dist/browser/manifest.json +3 -2
  31. package/dist/engine.d.ts +8 -6
  32. package/dist/engine.d.ts.map +1 -1
  33. package/dist/engine.js +8 -6
  34. package/dist/engine.js.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/kit/cartesian.d.ts +44 -0
  37. package/dist/kit/cartesian.d.ts.map +1 -0
  38. package/dist/kit/cartesian.js +59 -0
  39. package/dist/kit/cartesian.js.map +1 -0
  40. package/dist/kit/chart-border.d.ts +8 -11
  41. package/dist/kit/chart-border.d.ts.map +1 -1
  42. package/dist/kit/chart-border.js +15 -32
  43. package/dist/kit/chart-border.js.map +1 -1
  44. package/dist/kit/chart-canvas.d.ts +23 -21
  45. package/dist/kit/chart-canvas.d.ts.map +1 -1
  46. package/dist/kit/chart-canvas.js +33 -90
  47. package/dist/kit/chart-canvas.js.map +1 -1
  48. package/dist/kit/chart-legend.d.ts +79 -88
  49. package/dist/kit/chart-legend.d.ts.map +1 -1
  50. package/dist/kit/chart-legend.js +137 -186
  51. package/dist/kit/chart-legend.js.map +1 -1
  52. package/dist/kit/entries.d.ts +17 -0
  53. package/dist/kit/entries.d.ts.map +1 -0
  54. package/dist/kit/entries.js +19 -0
  55. package/dist/kit/entries.js.map +1 -0
  56. package/dist/kit/motion.d.ts +48 -0
  57. package/dist/kit/motion.d.ts.map +1 -0
  58. package/dist/kit/motion.js +63 -0
  59. package/dist/kit/motion.js.map +1 -0
  60. package/dist/kit/paints.d.ts +10 -0
  61. package/dist/kit/paints.d.ts.map +1 -0
  62. package/dist/kit/paints.js +12 -0
  63. package/dist/kit/paints.js.map +1 -0
  64. package/dist/kit/shared.d.ts +59 -8
  65. package/dist/kit/shared.d.ts.map +1 -1
  66. package/dist/kit/shared.js +105 -10
  67. package/dist/kit/shared.js.map +1 -1
  68. package/dist/kit/vertical-axis.d.ts +36 -61
  69. package/dist/kit/vertical-axis.d.ts.map +1 -1
  70. package/dist/kit/vertical-axis.js +97 -132
  71. package/dist/kit/vertical-axis.js.map +1 -1
  72. package/dist/line-chart/chart-line.d.ts +27 -53
  73. package/dist/line-chart/chart-line.d.ts.map +1 -1
  74. package/dist/line-chart/chart-line.js +89 -120
  75. package/dist/line-chart/chart-line.js.map +1 -1
  76. package/dist/line-chart/chart-region.d.ts +14 -14
  77. package/dist/line-chart/chart-region.d.ts.map +1 -1
  78. package/dist/line-chart/chart-region.js +26 -64
  79. package/dist/line-chart/chart-region.js.map +1 -1
  80. package/dist/line-chart/horizontal-axis.d.ts +32 -56
  81. package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
  82. package/dist/line-chart/horizontal-axis.js +85 -123
  83. package/dist/line-chart/horizontal-axis.js.map +1 -1
  84. package/dist/line-chart/index.d.ts +2 -2
  85. package/dist/line-chart/index.d.ts.map +1 -1
  86. package/dist/line-chart/index.js +1 -1
  87. package/dist/line-chart/index.js.map +1 -1
  88. package/dist/line-chart/line-chart.d.ts +111 -311
  89. package/dist/line-chart/line-chart.d.ts.map +1 -1
  90. package/dist/line-chart/line-chart.js +403 -639
  91. package/dist/line-chart/line-chart.js.map +1 -1
  92. package/dist/line-chart/shared.d.ts +6 -6
  93. package/dist/line-chart/shared.d.ts.map +1 -1
  94. package/dist/line-chart/shared.js +22 -7
  95. package/dist/line-chart/shared.js.map +1 -1
  96. package/dist/line-chart/x-scale.d.ts +1 -1
  97. package/dist/line-chart/x-scale.js +1 -1
  98. package/dist/line-chart/x-scale.js.map +1 -1
  99. package/dist/nodes.d.ts +6 -9
  100. package/dist/nodes.d.ts.map +1 -1
  101. package/dist/nodes.js +7 -9
  102. package/dist/nodes.js.map +1 -1
  103. package/dist/pie-chart/index.d.ts +2 -2
  104. package/dist/pie-chart/index.d.ts.map +1 -1
  105. package/dist/pie-chart/index.js +1 -1
  106. package/dist/pie-chart/index.js.map +1 -1
  107. package/dist/pie-chart/pie-chart.d.ts +77 -134
  108. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  109. package/dist/pie-chart/pie-chart.js +199 -268
  110. package/dist/pie-chart/pie-chart.js.map +1 -1
  111. package/dist/pie-chart/pie-legend.d.ts +54 -31
  112. package/dist/pie-chart/pie-legend.d.ts.map +1 -1
  113. package/dist/pie-chart/pie-legend.js +93 -107
  114. package/dist/pie-chart/pie-legend.js.map +1 -1
  115. package/dist/pie-chart/pie-ring.d.ts +142 -122
  116. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  117. package/dist/pie-chart/pie-ring.js +333 -361
  118. package/dist/pie-chart/pie-ring.js.map +1 -1
  119. package/dist/pie-chart/shared.d.ts +3 -3
  120. package/dist/pie-chart/shared.js +3 -3
  121. package/dist/pie-chart/shared.js.map +1 -1
  122. package/package.json +6 -5
  123. package/registry.json +23 -23
  124. package/src/bar-chart/bar-chart.ts +534 -814
  125. package/src/bar-chart/category-axis.ts +162 -177
  126. package/src/bar-chart/chart-bars.ts +288 -310
  127. package/src/bar-chart/index.ts +31 -31
  128. package/src/bar-chart/shared.ts +354 -354
  129. package/src/engine.ts +28 -26
  130. package/src/index.ts +5 -5
  131. package/src/kit/cartesian.ts +81 -0
  132. package/src/kit/chart-border.ts +50 -61
  133. package/src/kit/chart-canvas.ts +122 -157
  134. package/src/kit/chart-legend.ts +229 -250
  135. package/src/kit/entries.ts +20 -0
  136. package/src/kit/motion.ts +90 -0
  137. package/src/kit/paints.ts +15 -0
  138. package/src/kit/shared.ts +782 -675
  139. package/src/kit/vertical-axis.ts +194 -224
  140. package/src/line-chart/chart-line.ts +181 -208
  141. package/src/line-chart/chart-region.ts +106 -135
  142. package/src/line-chart/horizontal-axis.ts +166 -202
  143. package/src/line-chart/index.ts +39 -39
  144. package/src/line-chart/line-chart.ts +585 -929
  145. package/src/line-chart/shared.ts +473 -459
  146. package/src/line-chart/x-scale.ts +367 -367
  147. package/src/nodes.ts +18 -20
  148. package/src/pie-chart/index.ts +37 -37
  149. package/src/pie-chart/pie-chart.ts +293 -381
  150. package/src/pie-chart/pie-legend.ts +169 -159
  151. package/src/pie-chart/pie-ring.ts +848 -903
  152. package/src/pie-chart/shared.ts +490 -490
  153. package/dist/browser/chunks/chunk-RMHQ6GYU.js +0 -2
  154. package/dist/browser/chunks/chunk-RMHQ6GYU.js.map +0 -7
@@ -1,177 +1,162 @@
1
- import {
2
- command,
3
- Node2D,
4
- Rect,
5
- Text,
6
- easeOut,
7
- property,
8
- insetsOps,
9
- textOps,
10
- type CommandArgs,
11
- type NodeConfig,
12
- type Node2DProps,
13
- } from "@motionscript/sdk"
14
-
15
- import {
16
- DEFAULT_X_AXIS,
17
- clampUnit,
18
- enterBudget,
19
- type AxisConfig,
20
- } from "../kit/shared"
21
- import { at, compose, hold, type Seekable } from "@motionscript/sdk/component"
22
-
23
- export interface CategoryAxisProps extends Node2DProps {
24
- /** One caption per category slot, left to right — the chart's rows, in order. */
25
- categories: string[]
26
- /** Caption/title text style and gutters — see {@link AxisConfig}. */
27
- axis: AxisConfig
28
- /**
29
- * Caption drawn centred in the gutter beneath the category row. Empty string
30
- * (the default) hides it.
31
- */
32
- label: string
33
- /**
34
- * Per-category opacity multiplier — how the chart's category spotlight reaches
35
- * the captions, so the name under a dimmed column dims with it. Missing
36
- * entries read as 1.
37
- */
38
- dims: number[]
39
- }
40
-
41
- /** How far a caption starts above its slot when it enters, in px. */
42
- const SLIDE = 24
43
-
44
- /**
45
- * The **categorical** horizontal axis of a {@link BarChart}: a row of
46
- * {@link Text} captions, one per row of the data, each centred on its slot
47
- * beneath the baseline.
48
- *
49
- * Not a variant of the line chart's `HorizontalAxis` and deliberately not
50
- * folded into it: that axis places markers by *value* across a numeric range,
51
- * this one divides its width into equal slots and centres a name in each. The
52
- * two look alike on screen and share nothing underneath — one reads a scale,
53
- * the other counts rows.
54
- *
55
- * The slot arithmetic is the same as the plot's, because this node is laid out
56
- * to exactly the plot's width (the chart reserves the axis gutter with an
57
- * invisible twin of the value axis, as the line chart does), so a caption lands
58
- * under the columns it names without either side being told about the other.
59
- *
60
- * {@link enter} staggers the captions in left-to-right, each lifted a little
61
- * above its slot and transparent; the resting state is the landed one, so a
62
- * chart that is never told to enter still reads as finished.
63
- */
64
- export class CategoryAxis extends Node2D<CategoryAxisProps> {
65
- @property({ default: () => [] as string[] }) declare categories: string[]
66
- @property({ default: DEFAULT_X_AXIS }) declare axis: AxisConfig
67
- @property({ default: "" }) declare label: string
68
- @property({ default: () => [] as number[] }) declare dims: number[]
69
-
70
- /** One caption per category, ordered left → right, for the staggered enter. */
71
- private captionNodes: Text[] = []
72
- /** The axis title, or null when `label` is empty. */
73
- private labelNode: Text | null = null
74
-
75
- /** Centre x of slot `i`, in the node's centred space. */
76
- private slotCenter(index: number): number {
77
- const n = this.categories.length
78
- if (n <= 0) return 0
79
- const width = this.layoutBounds.width
80
- return -width / 2 + (width / n) * (index + 0.5)
81
- }
82
-
83
- constructor(props?: NodeConfig<CategoryAxis, CategoryAxisProps>) {
84
- super(props)
85
-
86
- const tickPad = insetsOps.resolve(this.axis.tickPadding).top
87
- const titlePad = insetsOps.resolve(this.axis.titlePadding).top
88
-
89
- // Each caption sits in a hug box carrying the spotlight dim, so the
90
- // entrance can animate the text's own opacity without the two fighting over
91
- // one value — opacity multiplies down the tree, which is exactly the
92
- // composition wanted here.
93
- const slots = this.categories.map((caption, i) => {
94
- const text = new Text({
95
- ...textOps.styleProps(this.axis.labelStyle),
96
- text: caption,
97
- })
98
- this.captionNodes.push(text)
99
- return new Rect({
100
- width: "hug",
101
- height: "hug",
102
- flow: "freeform",
103
- opacity: () => clampUnit(this.dims[i] ?? 1),
104
- x: () => this.slotCenter(i),
105
- children: [text],
106
- })
107
- })
108
-
109
- const children: Node2D[] = [
110
- new Rect({
111
- width: "fill",
112
- height: "hug",
113
- padding: { top: tickPad },
114
- children: slots,
115
- }),
116
- ]
117
- if (this.label !== "") {
118
- this.labelNode = new Text({
119
- ...textOps.styleProps(this.axis.titleStyle),
120
- text: this.label,
121
- })
122
- children.push(this.labelNode)
123
- }
124
-
125
- this.add(new Rect({ flow: "vertical", gap: titlePad, height: "hug", width: "fill", children }))
126
- }
127
-
128
- /**
129
- * Primes every caption (and the title) at its pre-entrance state: lifted above
130
- * its slot and transparent. Idempotent, so the chart can arm its whole
131
- * composition before the first frame is drawn.
132
- */
133
- arm(slide = SLIDE): void {
134
- for (const caption of this.captionNodes) caption.set({ opacity: 0, y: slide })
135
- this.labelNode?.set({ opacity: 0, y: slide })
136
- }
137
-
138
- /**
139
- * Slide + fade every caption in from above, staggered left-to-right, then
140
- * bring the title down behind them.
141
- *
142
- * `args.duration` is the **whole** entrance — the stagger and the title are
143
- * carved out of it rather than added on top. See {@link enterBudget}.
144
- *
145
- * `args.data.stagger` is seconds between successive captions (derived when
146
- * omitted); `args.data.slide` is the starting upward offset in px (node space
147
- * is y-up, so a positive start slides *down* into place).
148
- */
149
- @command()
150
- enter(args: CommandArgs<{ stagger?: number; slide?: number }> & { duration: number }): Seekable {
151
- const { stagger, slide = SLIDE } = args.data ?? {}
152
- const easing = args.easing ?? easeOut()
153
- const nodes = this.captionNodes
154
- if (nodes.length === 0) return hold(args.duration)
155
- const { step, per, caption } = enterBudget(
156
- args.duration,
157
- nodes.length,
158
- this.labelNode !== null,
159
- stagger
160
- )
161
-
162
- this.arm(slide)
163
-
164
- // The stagger was `wait(i * step)` inside a generator, so its offsets only
165
- // existed while it ran. They were always `i * step`; placed, the same
166
- // entrance can be asked what it looks like partway through.
167
- const lastCaptionEnds = (nodes.length - 1) * step + per
168
- return compose([
169
- ...nodes.map((node, i) =>
170
- at(i * step, node.to({ data: { opacity: 1, y: 0 }, duration: per, easing }))
171
- ),
172
- ...(this.labelNode
173
- ? [at(lastCaptionEnds, this.labelNode.to({ data: { opacity: 1, y: 0 }, duration: caption, easing }))]
174
- : []),
175
- ])
176
- }
177
- }
1
+ import {
2
+ Rect,
3
+ Text,
4
+ Types,
5
+ component2d,
6
+ insetsOps,
7
+ textOps,
8
+ type NodeOf,
9
+ type Node2D,
10
+ type Node2DProps,
11
+ } from "@motionscript/sdk"
12
+
13
+ import { asParts, fresh } from "../kit/entries"
14
+ import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "../kit/motion"
15
+ import {
16
+ DEFAULT_X_AXIS,
17
+ clampUnit,
18
+ enterBudget,
19
+ type AxisConfig,
20
+ } from "../kit/shared"
21
+
22
+ export interface CategoryAxisProps extends Node2DProps {
23
+ /** One caption per category slot, left to right — the chart's rows, in order. */
24
+ categories: string[]
25
+ /** Caption/title text style and gutters — see {@link AxisConfig}. */
26
+ axis: AxisConfig
27
+ /**
28
+ * Caption drawn centred in the gutter beneath the category row. Empty string
29
+ * (the default) hides it.
30
+ */
31
+ label: string
32
+ /**
33
+ * Per-category opacity multiplier — how the chart's category spotlight reaches
34
+ * the captions, so the name under a dimmed column dims with it. Missing
35
+ * entries read as 1.
36
+ */
37
+ dims: number[]
38
+ }
39
+
40
+ /** How far a caption starts above its slot when it enters, in px. */
41
+ const SLIDE = 24
42
+
43
+ /**
44
+ * The **categorical** horizontal axis of a {@link BarChart}: a row of
45
+ * {@link Text} captions, one per row of the data, each centred on its slot
46
+ * beneath the baseline.
47
+ *
48
+ * Not a variant of the line chart's `HorizontalAxis` and deliberately not
49
+ * folded into it: that axis places markers by *value* across a numeric range,
50
+ * this one divides its width into equal slots and centres a name in each. The
51
+ * two look alike on screen and share nothing underneath — one reads a scale,
52
+ * the other counts rows.
53
+ *
54
+ * The slot arithmetic is the same as the plot's, because this node is laid out
55
+ * to exactly the plot's width (the chart reserves the axis gutter with an
56
+ * invisible twin of the value axis, as the line chart does), so a caption lands
57
+ * under the columns it names without either side being told about the other.
58
+ *
59
+ * `enter` staggers the captions in left-to-right, each lifted a little above
60
+ * its slot and transparent; the resting state is the landed one, so a chart
61
+ * that is never told to enter still reads as finished.
62
+ */
63
+ export const CategoryAxis = component2d({
64
+ key: "categoryAxis",
65
+ state: {
66
+ categories: fresh<string[]>(() => []),
67
+ axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
68
+ label: "",
69
+ dims: fresh<number[]>(() => []),
70
+ arrivals: arrivalsEntry(),
71
+ },
72
+ // A plain node filled its parent; one made of its captions would hug them.
73
+ defaults: { width: "fill", height: "fill" },
74
+ children: (state) => {
75
+ const tickPad = insetsOps.resolve(state.axis.tickPadding).top
76
+ const titlePad = insetsOps.resolve(state.axis.titlePadding).top
77
+
78
+ const column: Node2D[] = []
79
+ // The column's parent is the axis: its box is what the slots divide.
80
+ const width = () => (column[0]?.parent as Node2D | null | undefined)?.layoutBounds.width ?? 0
81
+ /** Centre x of slot `i`, in the node's centred space. */
82
+ const slotCenter = (index: number) => {
83
+ const n = state.categories.length
84
+ if (n <= 0) return 0
85
+ return -width() / 2 + (width() / n) * (index + 0.5)
86
+ }
87
+
88
+ // Each caption sits in a hug box carrying the spotlight dim, so the
89
+ // entrance can animate the text's own opacity without the two fighting over
90
+ // one value — opacity multiplies down the tree, which is exactly the
91
+ // composition wanted here.
92
+ const slots = state.categories.map((caption, i) =>
93
+ new Rect({
94
+ width: "hug",
95
+ height: "hug",
96
+ flow: "freeform",
97
+ opacity: () => clampUnit(state.dims[i] ?? 1),
98
+ x: () => slotCenter(i),
99
+ children: [
100
+ new Text({
101
+ ...textOps.styleProps(state.axis.labelStyle),
102
+ text: caption,
103
+ opacity: () => arrivalOf(state.arrivals, i).opacity,
104
+ y: () => arrivalOf(state.arrivals, i).shift,
105
+ }),
106
+ ],
107
+ })
108
+ )
109
+
110
+ const children: Node2D[] = [new Rect({ width: "fill", height: "hug", padding: { top: tickPad }, children: slots })]
111
+ if (state.label !== "") {
112
+ children.push(
113
+ new Text({
114
+ ...textOps.styleProps(state.axis.titleStyle),
115
+ text: state.label,
116
+ opacity: () => arrivalOf(state.arrivals, "label").opacity,
117
+ y: () => arrivalOf(state.arrivals, "label").shift,
118
+ })
119
+ )
120
+ }
121
+
122
+ column.push(new Rect({ flow: "vertical", gap: titlePad, height: "hug", width: "fill", children }))
123
+ return asParts(column)
124
+ },
125
+ commands: {
126
+ /**
127
+ * Slide + fade every caption in from above, staggered left-to-right, then
128
+ * bring the title down behind them. Its first frame arms them all: lifted
129
+ * by `slide` and transparent.
130
+ *
131
+ * The timeline's duration is the **whole** entrance — the stagger and the
132
+ * title are carved out of it rather than added on top. See {@link enterBudget}.
133
+ *
134
+ * `stagger` is seconds between successive captions (derived when omitted);
135
+ * `slide` is the starting upward offset in px (node space is y-up, so a
136
+ * positive start slides *down* into place).
137
+ */
138
+ enter: {
139
+ args: {
140
+ stagger: Types.custom<number | undefined>(undefined),
141
+ slide: Types.number(SLIDE),
142
+ },
143
+ run: (timeline, state) => {
144
+ const { stagger, slide } = timeline.args
145
+ const count = state.categories.length
146
+ const captioned = state.label !== ""
147
+ const from: Arrival = { opacity: 0, shift: slide }
148
+ const keys = [...state.categories.keys(), ...(captioned ? ["label"] : [])]
149
+ const arm = timeline.animate(() => ({ arrivals: armed(keys, from) }))
150
+ if (count === 0) return arm
151
+ const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
152
+ const ease = frameEase(timeline)
153
+ const lastCaptionEnds = (count - 1) * step + per
154
+ const captions = state.categories.map((_, i) => ({ at: i * step, duration: per, piece: arrival(timeline, i, from, ease) }))
155
+ const label = captioned ? [{ at: lastCaptionEnds, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
156
+ return timeline.together([arm, beats(timeline, [...captions, ...label])])
157
+ },
158
+ },
159
+ },
160
+ })
161
+
162
+ export type CategoryAxis = NodeOf<typeof CategoryAxis>