@motionscript/charts 0.0.0-stage → 0.1.0-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 +5 -0
- package/LICENSE +201 -0
- package/dist/bar-chart/bar-chart.d.ts +286 -0
- package/dist/bar-chart/bar-chart.d.ts.map +1 -0
- package/dist/bar-chart/bar-chart.js +610 -0
- package/dist/bar-chart/bar-chart.js.map +1 -0
- package/dist/bar-chart/category-axis.d.ts +77 -0
- package/dist/bar-chart/category-axis.d.ts.map +1 -0
- package/dist/bar-chart/category-axis.js +141 -0
- package/dist/bar-chart/category-axis.js.map +1 -0
- package/dist/bar-chart/chart-bars.d.ts +149 -0
- package/dist/bar-chart/chart-bars.d.ts.map +1 -0
- package/dist/bar-chart/chart-bars.js +236 -0
- package/dist/bar-chart/chart-bars.js.map +1 -0
- package/dist/bar-chart/index.d.ts +16 -0
- package/dist/bar-chart/index.d.ts.map +1 -0
- package/dist/bar-chart/index.js +12 -0
- package/dist/bar-chart/index.js.map +1 -0
- package/dist/bar-chart/shared.d.ts +146 -0
- package/dist/bar-chart/shared.d.ts.map +1 -0
- package/dist/bar-chart/shared.js +182 -0
- package/dist/bar-chart/shared.js.map +1 -0
- package/dist/browser/chunks/chunk-ZMBOBHYL.js +2 -0
- package/dist/browser/chunks/chunk-ZMBOBHYL.js.map +7 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/kit.js +2 -0
- package/dist/browser/kit.js.map +7 -0
- package/dist/browser/manifest.json +12 -0
- package/dist/engine.d.ts +27 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/chart-border.d.ts +28 -0
- package/dist/kit/chart-border.d.ts.map +1 -0
- package/dist/kit/chart-border.js +45 -0
- package/dist/kit/chart-border.js.map +1 -0
- package/dist/kit/chart-canvas.d.ts +56 -0
- package/dist/kit/chart-canvas.d.ts.map +1 -0
- package/dist/kit/chart-canvas.js +125 -0
- package/dist/kit/chart-canvas.js.map +1 -0
- package/dist/kit/chart-legend.d.ts +126 -0
- package/dist/kit/chart-legend.d.ts.map +1 -0
- package/dist/kit/chart-legend.js +212 -0
- package/dist/kit/chart-legend.js.map +1 -0
- package/dist/kit/shared.d.ts +251 -0
- package/dist/kit/shared.d.ts.map +1 -0
- package/dist/kit/shared.js +401 -0
- package/dist/kit/shared.js.map +1 -0
- package/dist/kit/vertical-axis.d.ts +99 -0
- package/dist/kit/vertical-axis.d.ts.map +1 -0
- package/dist/kit/vertical-axis.js +176 -0
- package/dist/kit/vertical-axis.js.map +1 -0
- package/dist/line-chart/chart-line.d.ts +91 -0
- package/dist/line-chart/chart-line.d.ts.map +1 -0
- package/dist/line-chart/chart-line.js +156 -0
- package/dist/line-chart/chart-line.js.map +1 -0
- package/dist/line-chart/chart-region.d.ts +47 -0
- package/dist/line-chart/chart-region.d.ts.map +1 -0
- package/dist/line-chart/chart-region.js +101 -0
- package/dist/line-chart/chart-region.js.map +1 -0
- package/dist/line-chart/horizontal-axis.d.ts +85 -0
- package/dist/line-chart/horizontal-axis.d.ts.map +1 -0
- package/dist/line-chart/horizontal-axis.js +158 -0
- package/dist/line-chart/horizontal-axis.js.map +1 -0
- package/dist/line-chart/index.d.ts +22 -0
- package/dist/line-chart/index.d.ts.map +1 -0
- package/dist/line-chart/index.js +19 -0
- package/dist/line-chart/index.js.map +1 -0
- package/dist/line-chart/line-chart.d.ts +347 -0
- package/dist/line-chart/line-chart.d.ts.map +1 -0
- package/dist/line-chart/line-chart.js +701 -0
- package/dist/line-chart/line-chart.js.map +1 -0
- package/dist/line-chart/shared.d.ts +171 -0
- package/dist/line-chart/shared.d.ts.map +1 -0
- package/dist/line-chart/shared.js +270 -0
- package/dist/line-chart/shared.js.map +1 -0
- package/dist/line-chart/x-scale.d.ts +84 -0
- package/dist/line-chart/x-scale.d.ts.map +1 -0
- package/dist/line-chart/x-scale.js +302 -0
- package/dist/line-chart/x-scale.js.map +1 -0
- package/dist/nodes.d.ts +20 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +20 -0
- package/dist/nodes.js.map +1 -0
- package/dist/pie-chart/index.d.ts +18 -0
- package/dist/pie-chart/index.d.ts.map +1 -0
- package/dist/pie-chart/index.js +14 -0
- package/dist/pie-chart/index.js.map +1 -0
- package/dist/pie-chart/pie-chart.d.ts +170 -0
- package/dist/pie-chart/pie-chart.d.ts.map +1 -0
- package/dist/pie-chart/pie-chart.js +291 -0
- package/dist/pie-chart/pie-chart.js.map +1 -0
- package/dist/pie-chart/pie-legend.d.ts +60 -0
- package/dist/pie-chart/pie-legend.d.ts.map +1 -0
- package/dist/pie-chart/pie-legend.js +131 -0
- package/dist/pie-chart/pie-legend.js.map +1 -0
- package/dist/pie-chart/pie-ring.d.ts +266 -0
- package/dist/pie-chart/pie-ring.d.ts.map +1 -0
- package/dist/pie-chart/pie-ring.js +708 -0
- package/dist/pie-chart/pie-ring.js.map +1 -0
- package/dist/pie-chart/shared.d.ts +210 -0
- package/dist/pie-chart/shared.d.ts.map +1 -0
- package/dist/pie-chart/shared.js +240 -0
- package/dist/pie-chart/shared.js.map +1 -0
- package/package.json +69 -3
- package/registry.json +23 -0
- package/src/bar-chart/bar-chart.ts +804 -0
- package/src/bar-chart/category-axis.ts +177 -0
- package/src/bar-chart/chart-bars.ts +310 -0
- package/src/bar-chart/index.ts +31 -0
- package/src/bar-chart/shared.ts +354 -0
- package/src/engine.ts +26 -0
- package/src/index.ts +5 -0
- package/src/kit/chart-border.ts +61 -0
- package/src/kit/chart-canvas.ts +157 -0
- package/src/kit/chart-legend.ts +250 -0
- package/src/kit/shared.ts +675 -0
- package/src/kit/vertical-axis.ts +224 -0
- package/src/line-chart/chart-line.ts +208 -0
- package/src/line-chart/chart-region.ts +135 -0
- package/src/line-chart/horizontal-axis.ts +202 -0
- package/src/line-chart/index.ts +39 -0
- package/src/line-chart/line-chart.ts +894 -0
- package/src/line-chart/shared.ts +459 -0
- package/src/line-chart/x-scale.ts +367 -0
- package/src/nodes.ts +20 -0
- package/src/pie-chart/index.ts +37 -0
- package/src/pie-chart/pie-chart.ts +374 -0
- package/src/pie-chart/pie-legend.ts +159 -0
- package/src/pie-chart/pie-ring.ts +903 -0
- package/src/pie-chart/shared.ts +490 -0
- package/README.md +0 -4
|
@@ -0,0 +1,459 @@
|
|
|
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 `nodes/chart-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 the studio's own inspector
|
|
14
|
+
* mapping (`line-chart-props.ts`) can build a partial {@link ChartTheme} without
|
|
15
|
+
* pulling the node classes in.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
Fills,
|
|
20
|
+
fillOps,
|
|
21
|
+
lerpNumber,
|
|
22
|
+
strokeOps,
|
|
23
|
+
type DataRecord,
|
|
24
|
+
type Fill,
|
|
25
|
+
type FillResolved,
|
|
26
|
+
type Stroke,
|
|
27
|
+
type StrokeResolved,
|
|
28
|
+
} from "@motionscript/core"
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
CHART_PALETTE,
|
|
32
|
+
DEFAULT_LEGEND,
|
|
33
|
+
DEFAULT_PLOT_AREA,
|
|
34
|
+
DEFAULT_SERIES_STROKES,
|
|
35
|
+
DEFAULT_X_AXIS,
|
|
36
|
+
DEFAULT_Y_AXIS,
|
|
37
|
+
SPOTLIGHT_DIM,
|
|
38
|
+
lerpAxisConfig,
|
|
39
|
+
lerpLegendConfig,
|
|
40
|
+
lerpPlotArea,
|
|
41
|
+
lerpSeriesStrokes,
|
|
42
|
+
niceStep,
|
|
43
|
+
num,
|
|
44
|
+
resolveAxisConfig,
|
|
45
|
+
resolveLegendConfig,
|
|
46
|
+
resolvePlotArea,
|
|
47
|
+
resolveSeriesStrokes,
|
|
48
|
+
type AxisConfig,
|
|
49
|
+
type ChartSeries,
|
|
50
|
+
type LegendConfig,
|
|
51
|
+
type LegendTheme,
|
|
52
|
+
type PlotAreaConfig,
|
|
53
|
+
type Range,
|
|
54
|
+
} from "../kit/shared"
|
|
55
|
+
import type { XScale } from "./x-scale"
|
|
56
|
+
|
|
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
|
+
*/
|
|
67
|
+
export type LineSeries = ChartSeries
|
|
68
|
+
|
|
69
|
+
// ---- Plot regions -------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
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.
|
|
79
|
+
*/
|
|
80
|
+
export interface PlotRegion {
|
|
81
|
+
/** Horizontal span in x-axis data units. Omit to span the full plot width. */
|
|
82
|
+
x?: Range
|
|
83
|
+
/** Vertical span in y-axis data units. Omit to span the full plot height. */
|
|
84
|
+
y?: Range
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
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.
|
|
94
|
+
*/
|
|
95
|
+
export interface RegionBox {
|
|
96
|
+
left: number
|
|
97
|
+
right: number
|
|
98
|
+
bottom: number
|
|
99
|
+
top: number
|
|
100
|
+
/** The author gave an x range — the left/right edges are real data bounds. */
|
|
101
|
+
xBounded: boolean
|
|
102
|
+
/** The author gave a y range — the bottom/top edges are real data bounds. */
|
|
103
|
+
yBounded: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The empty box, used as the "no region" resting value. */
|
|
107
|
+
export const EMPTY_REGION_BOX: RegionBox = {
|
|
108
|
+
left: 0,
|
|
109
|
+
right: 0,
|
|
110
|
+
bottom: 0,
|
|
111
|
+
top: 0,
|
|
112
|
+
xBounded: false,
|
|
113
|
+
yBounded: false,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Is `box` a real, non-degenerate region — i.e. is there anything to spotlight? */
|
|
117
|
+
export function hasRegion(box: RegionBox): boolean {
|
|
118
|
+
return box.right > box.left && box.top > box.bottom
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
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.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveRegionBox(
|
|
131
|
+
region: PlotRegion | null | undefined,
|
|
132
|
+
plot: { left: number; right: number; bottom: number; top: number },
|
|
133
|
+
xScale: Range,
|
|
134
|
+
yScale: Range
|
|
135
|
+
): RegionBox {
|
|
136
|
+
if (!region || (region.x === undefined && region.y === undefined)) {
|
|
137
|
+
return EMPTY_REGION_BOX
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const span = (
|
|
141
|
+
range: Range | undefined,
|
|
142
|
+
[min, max]: Range,
|
|
143
|
+
lo: number,
|
|
144
|
+
hi: number
|
|
145
|
+
): [number, number, boolean] => {
|
|
146
|
+
if (range === undefined || max <= min) return [lo, hi, false]
|
|
147
|
+
const a = Math.min(range[0], range[1])
|
|
148
|
+
const b = Math.max(range[0], range[1])
|
|
149
|
+
const toPx = (v: number) => lo + ((v - min) / (max - min)) * (hi - lo)
|
|
150
|
+
return [
|
|
151
|
+
Math.max(lo, Math.min(hi, toPx(a))),
|
|
152
|
+
Math.max(lo, Math.min(hi, toPx(b))),
|
|
153
|
+
true,
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const [left, right, xBounded] = span(region.x, xScale, plot.left, plot.right)
|
|
158
|
+
const [bottom, top, yBounded] = span(region.y, yScale, plot.bottom, plot.top)
|
|
159
|
+
return { left, right, bottom, top, xBounded, yBounded }
|
|
160
|
+
}
|
|
161
|
+
|
|
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
|
+
/**
|
|
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.
|
|
175
|
+
*/
|
|
176
|
+
export function clipPolylineToBox(
|
|
177
|
+
points: Vector2Like[],
|
|
178
|
+
box: RegionBox
|
|
179
|
+
): Vector2Like[][] {
|
|
180
|
+
if (points.length < 2 || !hasRegion(box)) return []
|
|
181
|
+
|
|
182
|
+
const runs: Vector2Like[][] = []
|
|
183
|
+
let run: Vector2Like[] = []
|
|
184
|
+
|
|
185
|
+
for (let i = 1; i < points.length; i++) {
|
|
186
|
+
const seg = clipSegment(points[i - 1], points[i], box)
|
|
187
|
+
if (!seg) {
|
|
188
|
+
// Segment misses the box entirely — close whatever run was open.
|
|
189
|
+
if (run.length >= 2) runs.push(run)
|
|
190
|
+
run = []
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
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.
|
|
196
|
+
if (run.length > 0 && samePoint(run[run.length - 1], a)) run.push(b)
|
|
197
|
+
else {
|
|
198
|
+
if (run.length >= 2) runs.push(run)
|
|
199
|
+
run = [a, b]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (run.length >= 2) runs.push(run)
|
|
203
|
+
return runs
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Points within half a pixel are the same point, for run-joining purposes. */
|
|
207
|
+
function samePoint(a: Vector2Like, b: Vector2Like): boolean {
|
|
208
|
+
return Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5
|
|
209
|
+
}
|
|
210
|
+
|
|
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
|
+
*/
|
|
215
|
+
function clipSegment(
|
|
216
|
+
a: Vector2Like,
|
|
217
|
+
b: Vector2Like,
|
|
218
|
+
box: RegionBox
|
|
219
|
+
): [Vector2Like, Vector2Like] | null {
|
|
220
|
+
const dx = b.x - a.x
|
|
221
|
+
const dy = b.y - a.y
|
|
222
|
+
let t0 = 0
|
|
223
|
+
let t1 = 1
|
|
224
|
+
|
|
225
|
+
// Each edge as (p, q): the segment survives where p·t <= q.
|
|
226
|
+
const edges: [number, number][] = [
|
|
227
|
+
[-dx, a.x - box.left],
|
|
228
|
+
[dx, box.right - a.x],
|
|
229
|
+
[-dy, a.y - box.bottom],
|
|
230
|
+
[dy, box.top - a.y],
|
|
231
|
+
]
|
|
232
|
+
for (const [p, q] of edges) {
|
|
233
|
+
if (p === 0) {
|
|
234
|
+
// Parallel to this edge: outside it means the whole segment is out.
|
|
235
|
+
if (q < 0) return null
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
const r = q / p
|
|
239
|
+
if (p < 0) {
|
|
240
|
+
if (r > t1) return null
|
|
241
|
+
if (r > t0) t0 = r
|
|
242
|
+
} else {
|
|
243
|
+
if (r < t0) return null
|
|
244
|
+
if (r < t1) t1 = r
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return [
|
|
248
|
+
{ x: a.x + t0 * dx, y: a.y + t0 * dy },
|
|
249
|
+
{ x: a.x + t1 * dx, y: a.y + t1 * dy },
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
|
|
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
|
+
*/
|
|
259
|
+
export interface RegionConfig {
|
|
260
|
+
/** The dashed rule marking each bounded edge. */
|
|
261
|
+
stroke: Stroke
|
|
262
|
+
/** Wash filling the region, drawn under the series lines. */
|
|
263
|
+
fill: Fill
|
|
264
|
+
/** Opacity the stretch of every series *outside* the region fades to. */
|
|
265
|
+
dimOpacity: number
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A {@link LineChart}'s full, resolved visual theme. */
|
|
269
|
+
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
|
+
*/
|
|
274
|
+
seriesStyles: Stroke[]
|
|
275
|
+
xAxis: AxisConfig
|
|
276
|
+
yAxis: AxisConfig
|
|
277
|
+
/** The plot panel: border, fill, and drop shadow. */
|
|
278
|
+
plotArea: PlotAreaConfig
|
|
279
|
+
legend: LegendConfig
|
|
280
|
+
/** The spotlit-region annotation — see {@link RegionConfig}. */
|
|
281
|
+
region: RegionConfig
|
|
282
|
+
}
|
|
283
|
+
|
|
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
|
+
*/
|
|
288
|
+
export type ChartTheme = {
|
|
289
|
+
seriesStyles?: Stroke[]
|
|
290
|
+
xAxis?: Partial<AxisConfig>
|
|
291
|
+
yAxis?: Partial<AxisConfig>
|
|
292
|
+
plotArea?: Partial<PlotAreaConfig>
|
|
293
|
+
legend?: LegendTheme
|
|
294
|
+
region?: Partial<RegionConfig>
|
|
295
|
+
}
|
|
296
|
+
|
|
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
|
+
*/
|
|
303
|
+
const LINE_CHART_LEGEND: LegendConfig = {
|
|
304
|
+
...DEFAULT_LEGEND,
|
|
305
|
+
markerStyle: {
|
|
306
|
+
...DEFAULT_LEGEND.markerStyle,
|
|
307
|
+
width: 20,
|
|
308
|
+
height: 20,
|
|
309
|
+
borderRadius: 6,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The chart's out-of-the-box look. */
|
|
314
|
+
export const DEFAULT_CHART_THEME: ChartConfig = {
|
|
315
|
+
seriesStyles: DEFAULT_SERIES_STROKES,
|
|
316
|
+
xAxis: DEFAULT_X_AXIS,
|
|
317
|
+
yAxis: DEFAULT_Y_AXIS,
|
|
318
|
+
plotArea: DEFAULT_PLOT_AREA,
|
|
319
|
+
legend: LINE_CHART_LEGEND,
|
|
320
|
+
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.
|
|
323
|
+
stroke: {
|
|
324
|
+
weight: 3,
|
|
325
|
+
fill: CHART_PALETTE.regionColor,
|
|
326
|
+
dash: [14, 12],
|
|
327
|
+
cap: "butt",
|
|
328
|
+
},
|
|
329
|
+
// A wash barely above the panel — enough to tint the band, not enough to
|
|
330
|
+
// mute the series drawn over it.
|
|
331
|
+
fill: Fills.color(CHART_PALETTE.regionColor, { opacity: 0.08 }),
|
|
332
|
+
dimOpacity: SPOTLIGHT_DIM,
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function resolveRegionConfig(
|
|
337
|
+
region: Partial<RegionConfig> | undefined,
|
|
338
|
+
previous: RegionConfig
|
|
339
|
+
): RegionConfig {
|
|
340
|
+
return {
|
|
341
|
+
stroke: strokeOps.resolve(region?.stroke ?? previous.stroke),
|
|
342
|
+
fill: fillOps.resolve(region?.fill ?? previous.fill),
|
|
343
|
+
dimOpacity: region?.dimOpacity ?? previous.dimOpacity,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function lerpRegionConfig(
|
|
348
|
+
from: RegionConfig,
|
|
349
|
+
to: RegionConfig,
|
|
350
|
+
t: number
|
|
351
|
+
): RegionConfig {
|
|
352
|
+
return {
|
|
353
|
+
stroke: strokeOps.lerp(
|
|
354
|
+
from.stroke as StrokeResolved[],
|
|
355
|
+
to.stroke as StrokeResolved[],
|
|
356
|
+
t
|
|
357
|
+
),
|
|
358
|
+
fill: fillOps.lerp(
|
|
359
|
+
from.fill as FillResolved[],
|
|
360
|
+
to.fill as FillResolved[],
|
|
361
|
+
t
|
|
362
|
+
),
|
|
363
|
+
dimOpacity: lerpNumber(from.dimOpacity, to.dimOpacity, t),
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* {@link LineChart}'s `theme` @property mapper: 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
|
+
*/
|
|
372
|
+
export function resolveChartTheme(
|
|
373
|
+
theme: ChartTheme | undefined,
|
|
374
|
+
previous?: ChartConfig
|
|
375
|
+
): ChartConfig {
|
|
376
|
+
const base = previous ?? DEFAULT_CHART_THEME
|
|
377
|
+
return {
|
|
378
|
+
seriesStyles: resolveSeriesStrokes(theme?.seriesStyles, base.seriesStyles),
|
|
379
|
+
xAxis: resolveAxisConfig(theme?.xAxis, base.xAxis),
|
|
380
|
+
yAxis: resolveAxisConfig(theme?.yAxis, base.yAxis),
|
|
381
|
+
plotArea: resolvePlotArea(theme?.plotArea, base.plotArea),
|
|
382
|
+
legend: resolveLegendConfig(theme?.legend, base.legend),
|
|
383
|
+
region: resolveRegionConfig(theme?.region, base.region),
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** {@link LineChart}'s `theme` @property tween — leaf by leaf. */
|
|
388
|
+
export function lerpChartTheme(
|
|
389
|
+
from: ChartConfig,
|
|
390
|
+
to: ChartConfig,
|
|
391
|
+
t: number
|
|
392
|
+
): ChartConfig {
|
|
393
|
+
return {
|
|
394
|
+
seriesStyles: lerpSeriesStrokes(from.seriesStyles, to.seriesStyles, t),
|
|
395
|
+
xAxis: lerpAxisConfig(from.xAxis, to.xAxis, t),
|
|
396
|
+
yAxis: lerpAxisConfig(from.yAxis, to.yAxis, t),
|
|
397
|
+
plotArea: lerpPlotArea(from.plotArea, to.plotArea, t),
|
|
398
|
+
legend: lerpLegendConfig(from.legend, to.legend, t),
|
|
399
|
+
region: lerpRegionConfig(from.region, to.region, t),
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---- Scales --------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/**
|
|
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`.
|
|
417
|
+
*/
|
|
418
|
+
export function seriesPoints(
|
|
419
|
+
data: DataRecord[],
|
|
420
|
+
x: XScale,
|
|
421
|
+
series: LineSeries
|
|
422
|
+
): [x: number, y: number][] {
|
|
423
|
+
return data
|
|
424
|
+
.map((row, i): [number, number] => [x.at(i), num(row, series.field)])
|
|
425
|
+
.sort((a, b) => a[0] - b[0])
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Min/max across every series' column, the fallback when unset. */
|
|
429
|
+
export function yExtent(data: DataRecord[], series: LineSeries[]): Range {
|
|
430
|
+
let lo = Infinity
|
|
431
|
+
let hi = -Infinity
|
|
432
|
+
for (const row of data)
|
|
433
|
+
for (const s of series) {
|
|
434
|
+
const v = num(row, s.field)
|
|
435
|
+
if (v < lo) lo = v
|
|
436
|
+
if (v > hi) hi = v
|
|
437
|
+
}
|
|
438
|
+
return lo <= hi ? [lo, hi] : [0, 1]
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
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.
|
|
447
|
+
*/
|
|
448
|
+
export function resolveYScale(
|
|
449
|
+
data: DataRecord[],
|
|
450
|
+
series: LineSeries[],
|
|
451
|
+
range: Range | undefined,
|
|
452
|
+
divisions: number
|
|
453
|
+
): Range {
|
|
454
|
+
if (range) return range
|
|
455
|
+
const [lo, hi] = yExtent(data, series)
|
|
456
|
+
const segments = Math.max(1, Math.round(divisions))
|
|
457
|
+
const step = niceStep((hi - lo) / segments || 1)
|
|
458
|
+
return [Math.floor(lo / step) * step, Math.ceil(hi / step) * step]
|
|
459
|
+
}
|