@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.
- package/CHANGELOG.md +19 -0
- package/dist/bar-chart/bar-chart.d.ts +22 -93
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +48 -121
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +11 -38
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +15 -38
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +43 -74
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +31 -59
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -10
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +0 -7
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +27 -90
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +15 -54
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-4SMRCU24.js +2 -0
- package/dist/browser/chunks/chunk-4SMRCU24.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/engine.d.ts +4 -16
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4 -16
- package/dist/engine.js.map +1 -1
- package/dist/kit/cartesian.d.ts +8 -28
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +7 -19
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-border.d.ts +9 -18
- package/dist/kit/chart-border.d.ts.map +1 -1
- package/dist/kit/chart-border.js +4 -9
- package/dist/kit/chart-border.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +25 -36
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +13 -13
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +14 -61
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +17 -65
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/kit/entries.d.ts +2 -0
- package/dist/kit/entries.d.ts.map +1 -1
- package/dist/kit/entries.js +2 -0
- package/dist/kit/entries.js.map +1 -1
- package/dist/kit/motion.d.ts +11 -3
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +7 -1
- package/dist/kit/motion.js.map +1 -1
- package/dist/kit/shared.d.ts +60 -175
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +40 -157
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/vertical-axis.d.ts +16 -45
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +17 -42
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +16 -41
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +14 -43
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +14 -27
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +8 -22
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/horizontal-axis.d.ts +16 -43
- package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
- package/dist/line-chart/horizontal-axis.js +13 -42
- package/dist/line-chart/horizontal-axis.js.map +1 -1
- package/dist/line-chart/index.d.ts +1 -13
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +0 -12
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +23 -109
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +45 -141
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +33 -107
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +28 -81
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +14 -55
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +29 -111
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +0 -12
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +0 -12
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +1 -12
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +0 -9
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +37 -71
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +31 -65
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-legend.d.ts +11 -26
- package/dist/pie-chart/pie-legend.d.ts.map +1 -1
- package/dist/pie-chart/pie-legend.js +14 -39
- package/dist/pie-chart/pie-legend.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +50 -192
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +75 -247
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +45 -131
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +29 -72
- package/dist/pie-chart/shared.js.map +1 -1
- package/package.json +4 -4
- package/src/bar-chart/bar-chart.ts +51 -165
- package/src/bar-chart/category-axis.ts +16 -57
- package/src/bar-chart/chart-bars.ts +43 -120
- package/src/bar-chart/index.ts +2 -10
- package/src/bar-chart/shared.ts +29 -97
- package/src/engine.ts +4 -16
- package/src/kit/cartesian.ts +11 -26
- package/src/kit/chart-border.ts +6 -20
- package/src/kit/chart-canvas.ts +15 -40
- package/src/kit/chart-legend.ts +17 -86
- package/src/kit/entries.ts +4 -1
- package/src/kit/motion.ts +11 -2
- package/src/kit/shared.ts +58 -219
- package/src/kit/vertical-axis.ts +18 -71
- package/src/line-chart/chart-line.ts +16 -44
- package/src/line-chart/chart-region.ts +10 -23
- package/src/line-chart/horizontal-axis.ts +13 -42
- package/src/line-chart/index.ts +1 -13
- package/src/line-chart/line-chart.ts +50 -184
- package/src/line-chart/shared.ts +51 -145
- package/src/line-chart/x-scale.ts +31 -121
- package/src/nodes.ts +0 -12
- package/src/pie-chart/index.ts +1 -12
- package/src/pie-chart/pie-chart.ts +34 -102
- package/src/pie-chart/pie-legend.ts +14 -55
- package/src/pie-chart/pie-ring.ts +80 -307
- package/src/pie-chart/shared.ts +50 -139
- package/dist/browser/chunks/chunk-TJNHLQGV.js +0 -2
- package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
package/src/line-chart/shared.ts
CHANGED
|
@@ -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
|
|
73
|
-
*
|
|
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
|
-
/**
|
|
50
|
+
/** Omit to span the full plot width. */
|
|
82
51
|
x?: Range
|
|
83
|
-
/**
|
|
52
|
+
/** Omit to span the full plot height. */
|
|
84
53
|
y?: Range
|
|
85
54
|
}
|
|
86
55
|
|
|
87
56
|
/**
|
|
88
|
-
* A {@link PlotRegion}
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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`
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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:
|
|
129
|
+
points: Vector2[],
|
|
178
130
|
box: RegionBox
|
|
179
|
-
):
|
|
131
|
+
): Vector2[][] {
|
|
180
132
|
if (points.length < 2 || !hasRegion(box)) return []
|
|
181
133
|
|
|
182
|
-
const runs:
|
|
183
|
-
let run:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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:
|
|
217
|
-
b:
|
|
164
|
+
a: Vector2,
|
|
165
|
+
b: Vector2,
|
|
218
166
|
box: RegionBox
|
|
219
|
-
): [
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
203
|
+
/** Marks each bounded edge. */
|
|
261
204
|
stroke: Stroke
|
|
262
|
-
/**
|
|
205
|
+
/** Drawn under the series lines. */
|
|
263
206
|
fill: Fill
|
|
264
|
-
/** Opacity the stretch of every series
|
|
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
|
|
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
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
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
|
-
*
|
|
443
|
-
*
|
|
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
|
|
464
|
-
*
|
|
465
|
-
*
|
|
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
|
|
3
|
-
*
|
|
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
|
|
6
|
-
*
|
|
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
|
|
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`
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
-
|
|
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
|
|
213
|
-
*
|
|
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
|
|
254
|
-
*
|
|
255
|
-
* would
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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]
|
|
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];
|