@motionscript/charts 0.1.0-alpha.8 → 0.1.0

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 (76) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +201 -201
  3. package/NOTICE +49 -49
  4. package/README.md +99 -99
  5. package/dist/bar-chart/bar-chart.js.map +1 -1
  6. package/dist/bar-chart/category-axis.js.map +1 -1
  7. package/dist/bar-chart/chart-bars.js.map +1 -1
  8. package/dist/bar-chart/index.js.map +1 -1
  9. package/dist/bar-chart/shared.js.map +1 -1
  10. package/dist/browser/chunks/{chunk-XROBIRQK.js → chunk-TJNHLQGV.js} +1 -1
  11. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +7 -0
  12. package/dist/browser/index.js +1 -1
  13. package/dist/browser/index.js.map +1 -1
  14. package/dist/browser/kit.js +1 -1
  15. package/dist/browser/manifest.json +1 -1
  16. package/dist/engine.d.ts +1 -1
  17. package/dist/engine.js +1 -1
  18. package/dist/engine.js.map +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/kit/cartesian.js.map +1 -1
  21. package/dist/kit/chart-border.js.map +1 -1
  22. package/dist/kit/chart-canvas.js.map +1 -1
  23. package/dist/kit/chart-legend.js.map +1 -1
  24. package/dist/kit/entries.js.map +1 -1
  25. package/dist/kit/motion.js.map +1 -1
  26. package/dist/kit/paints.js.map +1 -1
  27. package/dist/kit/shared.js.map +1 -1
  28. package/dist/kit/vertical-axis.js.map +1 -1
  29. package/dist/line-chart/chart-line.js.map +1 -1
  30. package/dist/line-chart/chart-region.js.map +1 -1
  31. package/dist/line-chart/horizontal-axis.js.map +1 -1
  32. package/dist/line-chart/index.d.ts +1 -1
  33. package/dist/line-chart/index.js +1 -1
  34. package/dist/line-chart/index.js.map +1 -1
  35. package/dist/line-chart/line-chart.js.map +1 -1
  36. package/dist/line-chart/shared.js.map +1 -1
  37. package/dist/line-chart/x-scale.js.map +1 -1
  38. package/dist/nodes.js.map +1 -1
  39. package/dist/pie-chart/index.js.map +1 -1
  40. package/dist/pie-chart/pie-chart.js.map +1 -1
  41. package/dist/pie-chart/pie-legend.js.map +1 -1
  42. package/dist/pie-chart/pie-ring.js.map +1 -1
  43. package/dist/pie-chart/shared.js.map +1 -1
  44. package/package.json +4 -4
  45. package/registry.json +23 -23
  46. package/src/bar-chart/bar-chart.ts +542 -542
  47. package/src/bar-chart/category-axis.ts +162 -162
  48. package/src/bar-chart/chart-bars.ts +287 -287
  49. package/src/bar-chart/index.ts +31 -31
  50. package/src/bar-chart/shared.ts +354 -354
  51. package/src/engine.ts +28 -28
  52. package/src/index.ts +5 -5
  53. package/src/kit/cartesian.ts +81 -81
  54. package/src/kit/chart-border.ts +50 -50
  55. package/src/kit/chart-canvas.ts +122 -122
  56. package/src/kit/chart-legend.ts +229 -229
  57. package/src/kit/entries.ts +20 -20
  58. package/src/kit/motion.ts +90 -90
  59. package/src/kit/paints.ts +15 -15
  60. package/src/kit/shared.ts +782 -782
  61. package/src/kit/vertical-axis.ts +194 -194
  62. package/src/line-chart/chart-line.ts +156 -156
  63. package/src/line-chart/chart-region.ts +90 -90
  64. package/src/line-chart/horizontal-axis.ts +151 -151
  65. package/src/line-chart/index.ts +39 -39
  66. package/src/line-chart/line-chart.ts +598 -598
  67. package/src/line-chart/shared.ts +473 -473
  68. package/src/line-chart/x-scale.ts +382 -382
  69. package/src/nodes.ts +18 -18
  70. package/src/pie-chart/index.ts +37 -37
  71. package/src/pie-chart/pie-chart.ts +296 -296
  72. package/src/pie-chart/pie-legend.ts +169 -169
  73. package/src/pie-chart/pie-ring.ts +848 -848
  74. package/src/pie-chart/shared.ts +496 -496
  75. package/dist/browser/chunks/chunk-XROBIRQK.js.map +0 -7
  76. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -1,162 +1,162 @@
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>
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>