@motionscript/charts 0.1.0 → 0.1.1-alpha.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 (145) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bar-chart/bar-chart.d.ts +22 -93
  3. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  4. package/dist/bar-chart/bar-chart.js +48 -121
  5. package/dist/bar-chart/bar-chart.js.map +1 -1
  6. package/dist/bar-chart/category-axis.d.ts +11 -38
  7. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  8. package/dist/bar-chart/category-axis.js +15 -38
  9. package/dist/bar-chart/category-axis.js.map +1 -1
  10. package/dist/bar-chart/chart-bars.d.ts +43 -74
  11. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  12. package/dist/bar-chart/chart-bars.js +31 -59
  13. package/dist/bar-chart/chart-bars.js.map +1 -1
  14. package/dist/bar-chart/index.d.ts +2 -10
  15. package/dist/bar-chart/index.d.ts.map +1 -1
  16. package/dist/bar-chart/index.js +0 -7
  17. package/dist/bar-chart/index.js.map +1 -1
  18. package/dist/bar-chart/shared.d.ts +27 -90
  19. package/dist/bar-chart/shared.d.ts.map +1 -1
  20. package/dist/bar-chart/shared.js +15 -54
  21. package/dist/bar-chart/shared.js.map +1 -1
  22. package/dist/browser/chunks/chunk-4SMRCU24.js +2 -0
  23. package/dist/browser/chunks/chunk-4SMRCU24.js.map +7 -0
  24. package/dist/browser/index.js +1 -1
  25. package/dist/browser/index.js.map +3 -3
  26. package/dist/browser/kit.js +1 -1
  27. package/dist/browser/manifest.json +1 -1
  28. package/dist/engine.d.ts +4 -16
  29. package/dist/engine.d.ts.map +1 -1
  30. package/dist/engine.js +4 -16
  31. package/dist/engine.js.map +1 -1
  32. package/dist/kit/cartesian.d.ts +8 -28
  33. package/dist/kit/cartesian.d.ts.map +1 -1
  34. package/dist/kit/cartesian.js +7 -19
  35. package/dist/kit/cartesian.js.map +1 -1
  36. package/dist/kit/chart-border.d.ts +9 -18
  37. package/dist/kit/chart-border.d.ts.map +1 -1
  38. package/dist/kit/chart-border.js +4 -9
  39. package/dist/kit/chart-border.js.map +1 -1
  40. package/dist/kit/chart-canvas.d.ts +25 -36
  41. package/dist/kit/chart-canvas.d.ts.map +1 -1
  42. package/dist/kit/chart-canvas.js +13 -13
  43. package/dist/kit/chart-canvas.js.map +1 -1
  44. package/dist/kit/chart-legend.d.ts +14 -61
  45. package/dist/kit/chart-legend.d.ts.map +1 -1
  46. package/dist/kit/chart-legend.js +17 -65
  47. package/dist/kit/chart-legend.js.map +1 -1
  48. package/dist/kit/entries.d.ts +2 -0
  49. package/dist/kit/entries.d.ts.map +1 -1
  50. package/dist/kit/entries.js +2 -0
  51. package/dist/kit/entries.js.map +1 -1
  52. package/dist/kit/motion.d.ts +11 -3
  53. package/dist/kit/motion.d.ts.map +1 -1
  54. package/dist/kit/motion.js +7 -1
  55. package/dist/kit/motion.js.map +1 -1
  56. package/dist/kit/shared.d.ts +60 -175
  57. package/dist/kit/shared.d.ts.map +1 -1
  58. package/dist/kit/shared.js +40 -157
  59. package/dist/kit/shared.js.map +1 -1
  60. package/dist/kit/vertical-axis.d.ts +16 -45
  61. package/dist/kit/vertical-axis.d.ts.map +1 -1
  62. package/dist/kit/vertical-axis.js +17 -42
  63. package/dist/kit/vertical-axis.js.map +1 -1
  64. package/dist/line-chart/chart-line.d.ts +16 -41
  65. package/dist/line-chart/chart-line.d.ts.map +1 -1
  66. package/dist/line-chart/chart-line.js +14 -43
  67. package/dist/line-chart/chart-line.js.map +1 -1
  68. package/dist/line-chart/chart-region.d.ts +14 -27
  69. package/dist/line-chart/chart-region.d.ts.map +1 -1
  70. package/dist/line-chart/chart-region.js +8 -22
  71. package/dist/line-chart/chart-region.js.map +1 -1
  72. package/dist/line-chart/horizontal-axis.d.ts +16 -43
  73. package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
  74. package/dist/line-chart/horizontal-axis.js +13 -42
  75. package/dist/line-chart/horizontal-axis.js.map +1 -1
  76. package/dist/line-chart/index.d.ts +1 -13
  77. package/dist/line-chart/index.d.ts.map +1 -1
  78. package/dist/line-chart/index.js +0 -12
  79. package/dist/line-chart/index.js.map +1 -1
  80. package/dist/line-chart/line-chart.d.ts +23 -109
  81. package/dist/line-chart/line-chart.d.ts.map +1 -1
  82. package/dist/line-chart/line-chart.js +45 -141
  83. package/dist/line-chart/line-chart.js.map +1 -1
  84. package/dist/line-chart/shared.d.ts +33 -107
  85. package/dist/line-chart/shared.d.ts.map +1 -1
  86. package/dist/line-chart/shared.js +28 -81
  87. package/dist/line-chart/shared.js.map +1 -1
  88. package/dist/line-chart/x-scale.d.ts +14 -55
  89. package/dist/line-chart/x-scale.d.ts.map +1 -1
  90. package/dist/line-chart/x-scale.js +29 -111
  91. package/dist/line-chart/x-scale.js.map +1 -1
  92. package/dist/nodes.d.ts +0 -12
  93. package/dist/nodes.d.ts.map +1 -1
  94. package/dist/nodes.js +0 -12
  95. package/dist/nodes.js.map +1 -1
  96. package/dist/pie-chart/index.d.ts +1 -12
  97. package/dist/pie-chart/index.d.ts.map +1 -1
  98. package/dist/pie-chart/index.js +0 -9
  99. package/dist/pie-chart/index.js.map +1 -1
  100. package/dist/pie-chart/pie-chart.d.ts +37 -71
  101. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  102. package/dist/pie-chart/pie-chart.js +31 -65
  103. package/dist/pie-chart/pie-chart.js.map +1 -1
  104. package/dist/pie-chart/pie-legend.d.ts +11 -26
  105. package/dist/pie-chart/pie-legend.d.ts.map +1 -1
  106. package/dist/pie-chart/pie-legend.js +14 -39
  107. package/dist/pie-chart/pie-legend.js.map +1 -1
  108. package/dist/pie-chart/pie-ring.d.ts +50 -192
  109. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  110. package/dist/pie-chart/pie-ring.js +75 -247
  111. package/dist/pie-chart/pie-ring.js.map +1 -1
  112. package/dist/pie-chart/shared.d.ts +45 -131
  113. package/dist/pie-chart/shared.d.ts.map +1 -1
  114. package/dist/pie-chart/shared.js +29 -72
  115. package/dist/pie-chart/shared.js.map +1 -1
  116. package/package.json +4 -4
  117. package/src/bar-chart/bar-chart.ts +51 -165
  118. package/src/bar-chart/category-axis.ts +16 -57
  119. package/src/bar-chart/chart-bars.ts +43 -120
  120. package/src/bar-chart/index.ts +2 -10
  121. package/src/bar-chart/shared.ts +29 -97
  122. package/src/engine.ts +4 -16
  123. package/src/kit/cartesian.ts +11 -26
  124. package/src/kit/chart-border.ts +6 -20
  125. package/src/kit/chart-canvas.ts +15 -40
  126. package/src/kit/chart-legend.ts +17 -86
  127. package/src/kit/entries.ts +4 -1
  128. package/src/kit/motion.ts +11 -2
  129. package/src/kit/shared.ts +58 -219
  130. package/src/kit/vertical-axis.ts +18 -71
  131. package/src/line-chart/chart-line.ts +16 -44
  132. package/src/line-chart/chart-region.ts +10 -23
  133. package/src/line-chart/horizontal-axis.ts +13 -42
  134. package/src/line-chart/index.ts +1 -13
  135. package/src/line-chart/line-chart.ts +50 -184
  136. package/src/line-chart/shared.ts +51 -145
  137. package/src/line-chart/x-scale.ts +31 -121
  138. package/src/nodes.ts +0 -12
  139. package/src/pie-chart/index.ts +1 -12
  140. package/src/pie-chart/pie-chart.ts +34 -102
  141. package/src/pie-chart/pie-legend.ts +14 -55
  142. package/src/pie-chart/pie-ring.ts +80 -307
  143. package/src/pie-chart/shared.ts +50 -139
  144. package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
  145. package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
@@ -1,20 +1,3 @@
1
- /**
2
- * Types and pure helpers specific to the {@link LineChart} family: the traced
3
- * polylines, the spotlit region they dim outside of, and the theme those two
4
- * add to the shared chart vocabulary.
5
- *
6
- * Everything a bar or a pie chart would want too — what a series is, how an axis
7
- * is scaled and formatted, the theme leaf resolvers, the legend and plot-panel
8
- * branches — lives in `../kit/shared.ts` and is imported here. What's
9
- * left below is what only a line chart has: points sorted along an x column,
10
- * a region clipped out of a polyline, and the `region` branch of the theme.
11
- *
12
- * Kept renderer-agnostic (no `Node` imports) so every part shares one
13
- * scale/format/palette/theme vocabulary, and so a host's own inspector
14
- * mapping can build a partial {@link ChartTheme} without
15
- * pulling the components in.
16
- */
17
-
18
1
  import {
19
2
  Fills,
20
3
  fillOps,
@@ -25,7 +8,9 @@ import {
25
8
  type FillResolved,
26
9
  type Stroke,
27
10
  type StrokeResolved,
11
+ type Vector2,
28
12
  } from "@motionscript/sdk"
13
+ import { niceStep } from "@motionscript/sdk/component"
29
14
 
30
15
  import {
31
16
  CHART_PALETTE,
@@ -39,7 +24,6 @@ import {
39
24
  lerpLegendConfig,
40
25
  lerpPlotArea,
41
26
  lerpSeriesStrokes,
42
- niceStep,
43
27
  num,
44
28
  resolveAxisConfig,
45
29
  resolveLegendConfig,
@@ -54,56 +38,36 @@ import {
54
38
  } from "../kit/shared"
55
39
  import type { XScale } from "./x-scale"
56
40
 
57
- /**
58
- * One series in a {@link LineChart}: a polyline traced through the values in one
59
- * column of the chart's `data`, drawn in `fill` (or the theme's `seriesStyles`
60
- * palette when omitted) and captioned by `label` in the legend.
61
- *
62
- * The family-wide {@link ChartSeries} under a name that says what it draws here;
63
- * the chart's own `xField` prop names the column carrying the shared x values,
64
- * so every series is read off the same rows — one row per sample, one column per
65
- * series.
66
- */
41
+ /** One polyline of a {@link LineChart}: {@link ChartSeries} under the name for what it draws here. */
67
42
  export type LineSeries = ChartSeries
68
43
 
69
- // ---- Plot regions -------------------------------------------------------
70
-
71
44
  /**
72
- * A rectangular slice of the plot, in **data units** — the author-facing input
73
- * to {@link LineChart.spotlightRegion} / {@link LineChart.zoomTo}.
74
- *
75
- * Either axis may be omitted, and that's the point: `{ x: [60, 100] }` is a
76
- * vertical band spanning the full height, `{ y: [8, 10] }` a horizontal band
77
- * spanning the full width, and both together a box. A reversed range is read
78
- * the same as the forward one — a region is a span, not a direction.
45
+ * A rectangular slice of the plot in data units, the input to `spotlightRegion`
46
+ * and `zoomTo`. Either axis may be omitted: `{ x: [60, 100] }` is a full-height
47
+ * band. A reversed range reads the same as a forward one.
79
48
  */
80
49
  export interface PlotRegion {
81
- /** Horizontal span in x-axis data units. Omit to span the full plot width. */
50
+ /** Omit to span the full plot width. */
82
51
  x?: Range
83
- /** Vertical span in y-axis data units. Omit to span the full plot height. */
52
+ /** Omit to span the full plot height. */
84
53
  y?: Range
85
54
  }
86
55
 
87
56
  /**
88
- * A {@link PlotRegion} resolved into the plot's **centred pixel space** (origin
89
- * at the plot centre, y-up) — the same local frame the series points live in,
90
- * so a line can be clipped against it directly.
91
- *
92
- * `xBounded`/`yBounded` carry which axes the author actually constrained: an
93
- * unbounded axis has its edges pinned to the plot's own and draws no rule.
57
+ * A {@link PlotRegion} in the plot's centred pixel space (origin at the centre,
58
+ * y-up), the frame the series points live in. An axis the author left unbounded
59
+ * has its edges pinned to the plot's own and draws no rule.
94
60
  */
95
61
  export interface RegionBox {
96
62
  left: number
97
63
  right: number
98
64
  bottom: number
99
65
  top: number
100
- /** The author gave an x range — the left/right edges are real data bounds. */
101
66
  xBounded: boolean
102
- /** The author gave a y range — the bottom/top edges are real data bounds. */
103
67
  yBounded: boolean
104
68
  }
105
69
 
106
- /** The empty box, used as the "no region" resting value. */
70
+ /** The "no region" resting value. */
107
71
  export const EMPTY_REGION_BOX: RegionBox = {
108
72
  left: 0,
109
73
  right: 0,
@@ -113,19 +77,15 @@ export const EMPTY_REGION_BOX: RegionBox = {
113
77
  yBounded: false,
114
78
  }
115
79
 
116
- /** Is `box` a real, non-degenerate region — i.e. is there anything to spotlight? */
80
+ /** Whether `box` is non-degenerate, so there is something to spotlight. */
117
81
  export function hasRegion(box: RegionBox): boolean {
118
82
  return box.right > box.left && box.top > box.bottom
119
83
  }
120
84
 
121
85
  /**
122
- * Resolve a {@link PlotRegion} against the plot's pixel box and the chart's
123
- * scales. `plot` is the plot rectangle in centred pixel space, and
124
- * `xScale`/`yScale` the data ranges those edges span.
125
- *
126
- * An omitted axis takes the plot's own edges, so the region spans it. A given
127
- * axis is clamped to the plot, so a range running off the scale still produces
128
- * a box inside the panel.
86
+ * Resolve a {@link PlotRegion} against the plot rectangle (centred pixel space)
87
+ * and the data ranges its edges span. An omitted axis takes the plot's own
88
+ * edges; a given one is clamped to the plot.
129
89
  */
130
90
  export function resolveRegionBox(
131
91
  region: PlotRegion | null | undefined,
@@ -159,40 +119,31 @@ export function resolveRegionBox(
159
119
  return { left, right, bottom, top, xBounded, yBounded }
160
120
  }
161
121
 
162
- /** A bare `{x, y}` — declared locally so this module stays free of node imports. */
163
- interface Vector2Like {
164
- x: number
165
- y: number
166
- }
167
-
168
122
  /**
169
- * The parts of `points` that fall **inside** `box`, as a list of contiguous
170
- * sub-polylines — the geometry behind the region spotlight: a series is drawn
171
- * dimmed in full, then these runs are re-drawn over it at full strength.
172
- *
173
- * Segments are clipped against the box rather than filtered by endpoint, so a
174
- * line entering the region mid-segment is cut exactly on the boundary.
123
+ * The parts of `points` inside `box`, as contiguous sub-polylines: a series is
124
+ * drawn dimmed in full, then these runs are re-drawn at full strength. Segments
125
+ * are clipped, not filtered by endpoint, so a line entering mid-segment is cut
126
+ * exactly on the boundary.
175
127
  */
176
128
  export function clipPolylineToBox(
177
- points: Vector2Like[],
129
+ points: Vector2[],
178
130
  box: RegionBox
179
- ): Vector2Like[][] {
131
+ ): Vector2[][] {
180
132
  if (points.length < 2 || !hasRegion(box)) return []
181
133
 
182
- const runs: Vector2Like[][] = []
183
- let run: Vector2Like[] = []
134
+ const runs: Vector2[][] = []
135
+ let run: Vector2[] = []
184
136
 
185
137
  for (let i = 1; i < points.length; i++) {
186
138
  const seg = clipSegment(points[i - 1], points[i], box)
187
139
  if (!seg) {
188
- // Segment misses the box entirely — close whatever run was open.
140
+ // Misses the box: close the open run.
189
141
  if (run.length >= 2) runs.push(run)
190
142
  run = []
191
143
  continue
192
144
  }
193
145
  const [a, b] = seg
194
- // Continue the open run when this clipped segment starts where the last one
195
- // ended; otherwise the line left the box and came back, which is a new run.
146
+ // Continues the open run if it starts where the last ended; otherwise the line left and came back.
196
147
  if (run.length > 0 && samePoint(run[run.length - 1], a)) run.push(b)
197
148
  else {
198
149
  if (run.length >= 2) runs.push(run)
@@ -204,25 +155,22 @@ export function clipPolylineToBox(
204
155
  }
205
156
 
206
157
  /** Points within half a pixel are the same point, for run-joining purposes. */
207
- function samePoint(a: Vector2Like, b: Vector2Like): boolean {
158
+ function samePoint(a: Vector2, b: Vector2): boolean {
208
159
  return Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5
209
160
  }
210
161
 
211
- /**
212
- * Liang–Barsky clip of one segment against the axis-aligned `box`. Returns the
213
- * surviving `[start, end]`, or `null` when the segment lies wholly outside.
214
- */
162
+ /** Liang-Barsky clip of one segment against `box`: the surviving `[start, end]`, or `null` when wholly outside. */
215
163
  function clipSegment(
216
- a: Vector2Like,
217
- b: Vector2Like,
164
+ a: Vector2,
165
+ b: Vector2,
218
166
  box: RegionBox
219
- ): [Vector2Like, Vector2Like] | null {
167
+ ): [Vector2, Vector2] | null {
220
168
  const dx = b.x - a.x
221
169
  const dy = b.y - a.y
222
170
  let t0 = 0
223
171
  let t1 = 1
224
172
 
225
- // Each edge as (p, q): the segment survives where p·t <= q.
173
+ // Each edge as (p, q): the segment survives where p*t <= q.
226
174
  const edges: [number, number][] = [
227
175
  [-dx, a.x - box.left],
228
176
  [dx, box.right - a.x],
@@ -231,7 +179,7 @@ function clipSegment(
231
179
  ]
232
180
  for (const [p, q] of edges) {
233
181
  if (p === 0) {
234
- // Parallel to this edge: outside it means the whole segment is out.
182
+ // Parallel to this edge: outside it, the whole segment is out.
235
183
  if (q < 0) return null
236
184
  continue
237
185
  }
@@ -250,41 +198,28 @@ function clipSegment(
250
198
  ]
251
199
  }
252
200
 
253
- // ---- Theme ---------------------------------------------------------------
254
-
255
- /**
256
- * How a spotlit {@link PlotRegion} is drawn — the dashed rules on its bounded
257
- * edges, the wash filling it, and how far the series fade outside it.
258
- */
201
+ /** How a spotlit {@link PlotRegion} is drawn: rules on its bounded edges, a wash, and how far series fade outside it. */
259
202
  export interface RegionConfig {
260
- /** The dashed rule marking each bounded edge. */
203
+ /** Marks each bounded edge. */
261
204
  stroke: Stroke
262
- /** Wash filling the region, drawn under the series lines. */
205
+ /** Drawn under the series lines. */
263
206
  fill: Fill
264
- /** Opacity the stretch of every series *outside* the region fades to. */
207
+ /** Opacity the stretch of every series outside the region fades to. */
265
208
  dimOpacity: number
266
209
  }
267
210
 
268
211
  /** A {@link LineChart}'s full, resolved visual theme. */
269
212
  export interface ChartConfig {
270
- /**
271
- * Default per-series line stroke, cycled by series index (`i % length`). A
272
- * series' own `fill`, when set, overrides just the colour.
273
- */
213
+ /** Cycled by series index; a series' own `fill` overrides just the colour. */
274
214
  seriesStyles: Stroke[]
275
215
  xAxis: AxisConfig
276
216
  yAxis: AxisConfig
277
- /** The plot panel: border, fill, and drop shadow. */
278
217
  plotArea: PlotAreaConfig
279
218
  legend: LegendConfig
280
- /** The spotlit-region annotation — see {@link RegionConfig}. */
281
219
  region: RegionConfig
282
220
  }
283
221
 
284
- /**
285
- * Author-facing partial theme, passed as {@link LineChart}'s `theme` prop —
286
- * every branch optional and deep-merged onto {@link DEFAULT_CHART_THEME}.
287
- */
222
+ /** The author's partial theme, deep-merged onto {@link DEFAULT_CHART_THEME}. */
288
223
  export type ChartTheme = {
289
224
  seriesStyles?: Stroke[]
290
225
  xAxis?: Partial<AxisConfig>
@@ -294,12 +229,7 @@ export type ChartTheme = {
294
229
  region?: Partial<RegionConfig>
295
230
  }
296
231
 
297
- /**
298
- * The legend of a line chart: the shared one, with a small rounded-square
299
- * swatch in place of the wide chip. A bar's or a wedge's swatch stands in for
300
- * an area of paint and is drawn like one; a line's stands in for a colour, and
301
- * a square reads as the sample it is.
302
- */
232
+ /** The shared legend with a small square swatch: a line's swatch stands for a colour, where a bar's or wedge's stands for an area of paint. */
303
233
  const LINE_CHART_LEGEND: LegendConfig = {
304
234
  ...DEFAULT_LEGEND,
305
235
  markerStyle: {
@@ -310,7 +240,6 @@ const LINE_CHART_LEGEND: LegendConfig = {
310
240
  },
311
241
  }
312
242
 
313
- /** The chart's out-of-the-box look. */
314
243
  export const DEFAULT_CHART_THEME: ChartConfig = {
315
244
  seriesStyles: DEFAULT_SERIES_STROKES,
316
245
  xAxis: DEFAULT_X_AXIS,
@@ -318,16 +247,13 @@ export const DEFAULT_CHART_THEME: ChartConfig = {
318
247
  plotArea: DEFAULT_PLOT_AREA,
319
248
  legend: LINE_CHART_LEGEND,
320
249
  region: {
321
- // Brighter than the grid and dashed, so the boundary reads as a call-out
322
- // rather than another axis line; `butt` caps keep the dash lengths exact.
250
+ // Brighter and dashed so it reads as a call-out, not an axis line; `butt` caps keep dash lengths exact.
323
251
  stroke: {
324
252
  weight: 3,
325
253
  fill: CHART_PALETTE.regionColor,
326
254
  dash: [14, 12],
327
255
  cap: "butt",
328
256
  },
329
- // A wash barely above the panel — enough to tint the band, not enough to
330
- // mute the series drawn over it.
331
257
  fill: Fills.color(CHART_PALETTE.regionColor, { opacity: 0.08 }),
332
258
  dimOpacity: SPOTLIGHT_DIM,
333
259
  },
@@ -364,11 +290,7 @@ function lerpRegionConfig(
364
290
  }
365
291
  }
366
292
 
367
- /**
368
- * {@link LineChart}'s `theme` entry's resolve: merges the author's partial
369
- * {@link ChartTheme} onto `previous` (falling back to
370
- * {@link DEFAULT_CHART_THEME} on first resolution), fully resolving every leaf.
371
- */
293
+ /** The `theme` entry's resolve: merges a partial {@link ChartTheme} onto `previous` (first resolution: the default). */
372
294
  export function resolveChartTheme(
373
295
  theme: ChartTheme | undefined,
374
296
  previous?: ChartConfig
@@ -384,7 +306,6 @@ export function resolveChartTheme(
384
306
  }
385
307
  }
386
308
 
387
- /** {@link LineChart}'s `theme` entry's lerp — leaf by leaf. */
388
309
  export function lerpChartTheme(
389
310
  from: ChartConfig,
390
311
  to: ChartConfig,
@@ -400,20 +321,10 @@ export function lerpChartTheme(
400
321
  }
401
322
  }
402
323
 
403
- // ---- Scales --------------------------------------------------------------
404
-
405
324
  /**
406
- * Resolve one series' `[x, y]` points out of the chart's rows: each row's
407
- * position on the {@link XScale} paired with its own `field` column, sorted
408
- * along x so the polyline traces left-to-right whatever order the rows arrived
409
- * in.
410
- *
411
- * A row missing its y column contributes a 0 rather than a gap — these charts
412
- * draw continuous lines, so dropping the point would silently bridge the hole.
413
- *
414
- * Takes the resolved scale rather than the column *name*, because what a row's x
415
- * is depends on what kind of column it came out of: a number, an instant, or a
416
- * place in the file. See `x-scale.ts`.
325
+ * One series' `[x, y]` points, sorted along x so the polyline traces left to
326
+ * right. A row missing its y contributes 0 rather than a gap, since dropping it
327
+ * would silently bridge the hole.
417
328
  */
418
329
  export function seriesPoints(
419
330
  data: DataRecord[],
@@ -425,7 +336,6 @@ export function seriesPoints(
425
336
  .sort((a, b) => a[0] - b[0])
426
337
  }
427
338
 
428
- /** Min/max across every series' column, the fallback when unset. */
429
339
  export function yExtent(data: DataRecord[], series: LineSeries[]): Range {
430
340
  let lo = Infinity
431
341
  let hi = -Infinity
@@ -439,11 +349,9 @@ export function yExtent(data: DataRecord[], series: LineSeries[]): Range {
439
349
  }
440
350
 
441
351
  /**
442
- * Effective vertical scale: the explicit `range`, else the data's y extent
443
- * nudged out to tidy step bounds so markers read as round numbers.
444
- *
445
- * `divisions` is how many segments the auto-range is stepped into (one fewer
446
- * than a marker count) — it only shapes the tidy step when `range` is omitted.
352
+ * The explicit `range`, else the data's y extent nudged out to tidy step bounds.
353
+ * `divisions` (one fewer than the marker count) only shapes the step when
354
+ * `range` is omitted.
447
355
  */
448
356
  export function resolveYScale(
449
357
  data: DataRecord[],
@@ -460,11 +368,9 @@ export function resolveYScale(
460
368
  }
461
369
 
462
370
  /**
463
- * The scale for data that never moves. A zero-width scale puts every tick on the
464
- * same value and the line on the plot's floor, so it is widened to a few steps
465
- * either side of the value (the step sized to its magnitude, so `5` gets
466
- * `[3, 7]` and `5000` gets `[3000, 7000]`), or to `[0, 1]` for all zeros, as for
467
- * no data at all.
371
+ * The scale for constant data. A zero-width scale would put every tick on one
372
+ * value and the line on the floor, so it widens by a step sized to the
373
+ * magnitude (`5` gets `[3, 7]`), or `[0, 1]` for zero.
468
374
  */
469
375
  function constantScale(value: number, segments: number): Range {
470
376
  if (value === 0) return [0, 1]
@@ -1,29 +1,11 @@
1
1
  /**
2
- * What a line chart's x column *means* — the one question that has to be
3
- * answered before a row can be given a position along the bottom of the plot.
2
+ * What a line chart's x column means: a number (plotted as is), an instant
3
+ * (`Jan`, `2026-W12`; plotted by time, so a gap in the data reads as a gap), or
4
+ * anything else (the row's position, evenly spaced).
4
5
  *
5
- * A y column is always a magnitude. An x column is one of three things, and
6
- * which one is a property of the data rather than something the author states:
7
- *
8
- * - **numeric** — `0, 5, 10` — plot the number.
9
- * - **time** — `Jan`, `2026-03`, `2026-W12`, `2026-03-14` — plot the instant,
10
- * so samples a month apart sit a month apart and a gap in the data reads as a
11
- * gap.
12
- * - **ordinal** — anything else — plot the row's position, evenly spaced.
13
- *
14
- * Before this, every x column went through `num()`, which reads a non-numeric
15
- * cell as `0`. A `Month` column of `Jan…Dec` therefore stacked all twelve
16
- * points on one x and drew a vertical line — no error, no empty chart, just a
17
- * wrong picture. That is the failure this module exists to remove: every column
18
- * now plots as *something*, and the something is the reading a person would
19
- * give it.
20
- *
21
- * The captions come back off the rows rather than out of a formatter. A tick on
22
- * a time or ordinal scale is chosen *from the data* (see {@link XScale.ticks}),
23
- * so the label it wants is the cell the author wrote — `Jan`, not `1970-01-01`,
24
- * and `2026-W12`, not the Monday it resolves to. Only a tick that is not a
25
- * sample — an author-set range, or a `zoomTo` mid-flight — needs formatting,
26
- * and that is the one case {@link formatInstant} covers.
6
+ * A tick on a time or ordinal scale is a sample, so its caption is the cell the
7
+ * author wrote; only a tick that is not (an author-set range, a `zoomTo`
8
+ * mid-flight) is formatted, by {@link formatInstant}.
27
9
  */
28
10
 
29
11
  import type { DataRecord } from "@motionscript/sdk"
@@ -40,26 +22,18 @@ import { formatNumber } from "@motionscript/sdk/component"
40
22
  /** How an x column's cells become positions along the axis. */
41
23
  export type XScaleKind = "numeric" | "time" | "ordinal"
42
24
 
43
- /**
44
- * An x column resolved into positions, captions and ticks.
45
- *
46
- * The three kinds differ in all three answers, so they are three closures built
47
- * once per (data, column) rather than a kind tag every caller switches on.
48
- */
25
+ /** An x column resolved into positions, captions and ticks: three closures built once per (data, column), not a tag callers switch on. */
49
26
  export interface XScale {
50
27
  kind: XScaleKind
51
28
  /** Position of row `index` along the axis, in this scale's own units. */
52
29
  at(index: number): number
53
- /** `[min, max]` across the rows — the scale when no range is set. */
30
+ /** `[min, max]` across the rows: the scale when no range is set. */
54
31
  extent: Range
55
- /** The caption for a tick at `value`. */
56
32
  label(value: number): string
57
33
  /** `divisions + 1` tick values spanning `range`, ends included. */
58
34
  ticks(range: Range, divisions: number): number[]
59
35
  }
60
36
 
61
- // ---- Time parsing --------------------------------------------------------
62
-
63
37
  const DAY = 86_400_000
64
38
 
65
39
  const MONTHS = [
@@ -77,29 +51,15 @@ const MONTHS = [
77
51
  "december",
78
52
  ]
79
53
 
80
- /**
81
- * The year a cell that names no year is dated into.
82
- *
83
- * Any year would place `Jan…Dec` in the right order at the right spacing; a
84
- * leap year is the one that also spaces February correctly against a column
85
- * that *does* carry years, so the two read alike.
86
- */
54
+ /** The year a cell naming none is dated into; a leap year, so February spaces the same as in a column that carries years. */
87
55
  const NOMINAL_YEAR = 2024
88
56
 
89
- /**
90
- * Month index for any unambiguous abbreviation of a month name — `jan`, `sept`,
91
- * `january` — or -1.
92
- *
93
- * Three letters minimum, and then a prefix match, which is what lets `sept`
94
- * through without letting `janitor` through: a longer word has to still *be*
95
- * the month.
96
- */
57
+ /** Month index for `jan`, `sept` or `january`, else -1: three letters minimum, then a prefix match, so `sept` passes and `janitor` does not. */
97
58
  function monthIndex(word: string): number {
98
59
  if (word.length < 3) return -1
99
60
  return MONTHS.findIndex((month) => month.startsWith(word))
100
61
  }
101
62
 
102
- /** The Monday of ISO week `week` in `year`, as an epoch timestamp. */
103
63
  function isoWeekStart(year: number, week: number): number {
104
64
  // ISO week 1 is the week containing 4 January, and weeks start on Monday.
105
65
  const jan4 = Date.UTC(year, 0, 4)
@@ -108,14 +68,10 @@ function isoWeekStart(year: number, week: number): number {
108
68
  }
109
69
 
110
70
  /**
111
- * One cell read as an instant, or `null` when it isn't one.
112
- *
113
- * Deliberately **not** `Date.parse`. That accepts `"5"`, `"Q3"` and a good deal
114
- * else besides, differs between engines on anything non-ISO, and reads a bare
115
- * `2026-03-14` as UTC but `2026/03/14` as local — three ways for a column to be
116
- * silently misread as time. Every form below is matched explicitly, and an
117
- * ambiguous all-numeric date (`03/04/2026` — March or April, depending on where
118
- * the author lives) is deliberately not one of them.
71
+ * One cell read as an instant, or `null`. Not `Date.parse`, which accepts `"5"`
72
+ * and `"Q3"`, differs between engines off ISO, and reads `2026-03-14` as UTC but
73
+ * `2026/03/14` as local. Every form is matched explicitly, and an ambiguous
74
+ * all-numeric date (`03/04/2026`) is not one of them.
119
75
  */
120
76
  export function parseInstant(raw: unknown): number | null {
121
77
  if (typeof raw !== "string") return null
@@ -169,14 +125,7 @@ export function parseInstant(raw: unknown): number | null {
169
125
  return null
170
126
  }
171
127
 
172
- /**
173
- * An instant captioned at the coarsest granularity the axis's own span can tell
174
- * apart — a decade of samples reads as years, a year of them as months.
175
- *
176
- * Only reached for a tick that is *not* one of the rows (an author-set range, a
177
- * `zoomTo` part-way through). A tick that is a sample is captioned with the
178
- * cell the author wrote; see the note at the top of this file.
179
- */
128
+ /** An instant captioned at the coarsest granularity the span tells apart: a decade reads as years, a year as months. */
180
129
  export function formatInstant(value: number, span: number): string {
181
130
  const date = new Date(value)
182
131
  const year = date.getUTCFullYear()
@@ -187,16 +136,7 @@ export function formatInstant(value: number, span: number): string {
187
136
  return `${label} ${date.getUTCDate()}`
188
137
  }
189
138
 
190
- // ---- Column reading ------------------------------------------------------
191
-
192
- /**
193
- * Whether every non-empty cell of `field` holds a number.
194
- *
195
- * The same rule `numericFields` applies, asked of one column: a single text
196
- * cell in an otherwise numeric column takes the whole column off the numeric
197
- * scale, because plotting that row at 0 is the failure this module is here to
198
- * stop.
199
- */
139
+ /** `numericFields`' rule for one column: a single text cell takes it off the numeric scale rather than plot that row at 0. */
200
140
  function isNumericColumn(data: DataRecord[], field: string): boolean {
201
141
  let sawValue = false
202
142
  for (const row of data) {
@@ -209,13 +149,9 @@ function isNumericColumn(data: DataRecord[], field: string): boolean {
209
149
  }
210
150
 
211
151
  /**
212
- * Every non-empty cell of `field` parsed as an instant, or `null` if any one of
213
- * them isn't one — in which case the column isn't a time column at all.
214
- *
215
- * A blank cell keeps its slot, so the returned list stays index-aligned with
216
- * the rows, and takes the nearest known instant: a hole in an x column is a row
217
- * whose date the file didn't say, and stacking it on the epoch would drag the
218
- * whole scale back fifty years to make room for it.
152
+ * Every non-empty cell as an instant, or `null` if any is not one. A blank cell
153
+ * keeps its slot and takes the nearest known instant: stacking it on the epoch
154
+ * would drag the whole scale back fifty years.
219
155
  */
220
156
  function instantColumn(data: DataRecord[], field: string): number[] | null {
221
157
  const parsed: (number | null)[] = []
@@ -250,23 +186,12 @@ function extentOf(values: number[]): Range {
250
186
  }
251
187
 
252
188
  /**
253
- * Ticks spanning `range`, each one **a sample** — a row the author can point at
254
- * in their file — placed as near as it can be to where an evenly spaced tick
255
- * would have gone.
256
- *
257
- * Two things have to hold at once, and picking either alone gets it wrong.
258
- * Ticks must be *samples*, because a caption between two of them has nothing to
259
- * say (two-thirds of the way through March is not a month, and on a category
260
- * column it is not anything at all). And they must be *spread*, because the
261
- * labels sit side by side in a row: taking every nth sample instead puts five
262
- * dates in the two weeks a burst of rows happens to cover and prints them on
263
- * top of each other, which is what a column with a gap in it does.
264
- *
265
- * Walking the targets rather than the samples is what satisfies both. A run of
266
- * rows too close together to caption collapses to one tick — consecutive
267
- * targets pick the same sample and the duplicate is dropped — so a chart asked
268
- * for five ticks across a gappy column draws four honest ones instead of five
269
- * overlapping.
189
+ * Ticks spanning `range`, each a sample, nearest to where an evenly spaced tick
190
+ * would go. They must be samples (a caption between two has nothing to say) and
191
+ * spread (every nth sample would print five dates on top of each other across a
192
+ * burst of rows), so this walks the evenly spaced targets, not the samples.
193
+ * Targets that pick the same sample collapse to one tick, so a gappy column
194
+ * draws fewer honest ticks rather than overlapping ones.
270
195
  */
271
196
  function sampledTicks(
272
197
  positions: number[],
@@ -290,10 +215,7 @@ function sampledTicks(
290
215
  return out
291
216
  }
292
217
 
293
- /**
294
- * The caption a tick carries on a sample-based scale: the cell of the row it
295
- * came from, or `fallback` when the tick isn't a sample after all.
296
- */
218
+ /** The cell of the row a tick came from, or `fallback` when it is not a sample. */
297
219
  function labelBySample(
298
220
  positions: number[],
299
221
  captions: string[],
@@ -304,16 +226,10 @@ function labelBySample(
304
226
  return i >= 0 ? captions[i] : fallback(value)
305
227
  }
306
228
 
307
- /** Scales already resolved, by data array, then by field and pattern. */
229
+ /** Resolved scales by data array, then by field and pattern. */
308
230
  const scales = new WeakMap<DataRecord[], Map<string, XScale>>()
309
231
 
310
- /**
311
- * How `field` positions the rows: the number it holds, the instant it names, or
312
- * failing both, the row's own place in the file.
313
- *
314
- * Kept per `data` array: a chart's bound props ask a dozen times a frame, and
315
- * parsing every row's instant each time was most of a frame.
316
- */
232
+ /** Kept per `data` array: bound props ask a dozen times a frame, and parsing every row's instant each time was most of a frame. */
317
233
  export function resolveXScale(
318
234
  data: DataRecord[],
319
235
  field: string,
@@ -330,11 +246,7 @@ export function resolveXScale(
330
246
  function xScaleOf(
331
247
  data: DataRecord[],
332
248
  field: string,
333
- /**
334
- * The number-format pattern for a *numeric* column. Ignored by the other two
335
- * kinds, and correctly so: a tick on those is captioned with the cell the
336
- * author wrote, and `Jan` has no decimal places.
337
- */
249
+ /** Number-format pattern for a numeric column only; ticks on the other kinds are the author's cells. */
338
250
  pattern?: string
339
251
  ): XScale {
340
252
  if (field !== "" && isNumericColumn(data, field)) {
@@ -345,8 +257,7 @@ function xScaleOf(
345
257
  extent: extentOf(positions),
346
258
  label: (value) =>
347
259
  pattern === undefined ? formatMarker(value) : formatNumber(value, pattern),
348
- // Round numbers, evenly spaced — a continuous axis is read by its scale
349
- // rather than by which samples happen to be on it.
260
+ // A continuous axis is read by its scale, not by which samples are on it.
350
261
  ticks: markerValues,
351
262
  }
352
263
  }
@@ -373,8 +284,7 @@ function xScaleOf(
373
284
  return {
374
285
  kind: "ordinal",
375
286
  at: (i) => i,
376
- // `[0, n-1]` rather than the tidy `[0, n]`: the first and last rows sit on
377
- // the plot's edges, which is where a reader looks for them.
287
+ // `[0, n-1]`, not `[0, n]`: the first and last rows sit on the plot's edges.
378
288
  extent: positions.length > 0 ? [0, positions.length - 1] : [0, 1],
379
289
  label: (value) => captions[Math.round(value)] ?? "",
380
290
  ticks: (range, divisions) => sampledTicks(positions, range, divisions),
package/src/nodes.ts CHANGED
@@ -2,17 +2,5 @@ import { LineChart } from "./line-chart";
2
2
  import { BarChart } from "./bar-chart";
3
3
  import { PieChart } from "./pie-chart";
4
4
 
5
- /**
6
- * Every node type this package publishes.
7
- *
8
- * A host registers these by handing the array to an engine —
9
- * `new Engine(platform, { nodes: NODES })` — which reads each component's key.
10
- * Defining a component only *declares* that key: nothing is registered by
11
- * importing a module, so a registry holds exactly what its owner asked for.
12
- *
13
- * Listing the component **values** is also what survives bundling: this package
14
- * declares `sideEffects: false`, and a document names a node type by string, so
15
- * an import kept only for its side effect is exactly what gets shaken out.
16
- */
17
5
  // Typed by name, so the declaration names each component rather than spelling out its type through core.
18
6
  export const NODES: (typeof LineChart | typeof BarChart | typeof PieChart)[] = [LineChart, BarChart, PieChart];