@pond-ts/charts 0.31.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 +3254 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/AreaChart.d.ts +85 -0
- package/dist/AreaChart.js +119 -0
- package/dist/BandChart.d.ts +55 -0
- package/dist/BandChart.js +93 -0
- package/dist/BarChart.d.ts +72 -0
- package/dist/BarChart.js +137 -0
- package/dist/BoxPlot.d.ts +77 -0
- package/dist/BoxPlot.js +137 -0
- package/dist/Canvas.d.ts +37 -0
- package/dist/Canvas.js +39 -0
- package/dist/ChartContainer.d.ts +106 -0
- package/dist/ChartContainer.js +306 -0
- package/dist/ChartRow.d.ts +29 -0
- package/dist/ChartRow.js +215 -0
- package/dist/Layers.d.ts +22 -0
- package/dist/Layers.js +399 -0
- package/dist/LineChart.d.ts +60 -0
- package/dist/LineChart.js +105 -0
- package/dist/ScatterChart.d.ts +84 -0
- package/dist/ScatterChart.js +139 -0
- package/dist/TimeAxis.d.ts +9 -0
- package/dist/TimeAxis.js +12 -0
- package/dist/XAxis.d.ts +39 -0
- package/dist/XAxis.js +84 -0
- package/dist/YAxis.d.ts +42 -0
- package/dist/YAxis.js +86 -0
- package/dist/annotations.d.ts +110 -0
- package/dist/annotations.js +459 -0
- package/dist/area.d.ts +54 -0
- package/dist/area.js +186 -0
- package/dist/band.d.ts +31 -0
- package/dist/band.js +57 -0
- package/dist/bars.d.ts +96 -0
- package/dist/bars.js +171 -0
- package/dist/box.d.ts +59 -0
- package/dist/box.js +140 -0
- package/dist/chip.d.ts +23 -0
- package/dist/chip.js +43 -0
- package/dist/cjs-fallback.cjs +16 -0
- package/dist/context.d.ts +362 -0
- package/dist/context.js +5 -0
- package/dist/curve.d.ts +22 -0
- package/dist/curve.js +13 -0
- package/dist/data.d.ts +154 -0
- package/dist/data.js +197 -0
- package/dist/domain.d.ts +19 -0
- package/dist/domain.js +61 -0
- package/dist/encoding.d.ts +89 -0
- package/dist/encoding.js +144 -0
- package/dist/format.d.ts +53 -0
- package/dist/format.js +47 -0
- package/dist/gaps.d.ts +146 -0
- package/dist/gaps.js +209 -0
- package/dist/grid.d.ts +11 -0
- package/dist/grid.js +29 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +34 -0
- package/dist/line.d.ts +46 -0
- package/dist/line.js +88 -0
- package/dist/range.d.ts +15 -0
- package/dist/range.js +27 -0
- package/dist/scatter.d.ts +70 -0
- package/dist/scatter.js +213 -0
- package/dist/select.d.ts +13 -0
- package/dist/select.js +23 -0
- package/dist/slots.d.ts +48 -0
- package/dist/slots.js +64 -0
- package/dist/theme.d.ts +224 -0
- package/dist/theme.js +232 -0
- package/dist/tracker.d.ts +30 -0
- package/dist/tracker.js +47 -0
- package/dist/use-slot-key.d.ts +21 -0
- package/dist/use-slot-key.js +25 -0
- package/dist/viewport.d.ts +20 -0
- package/dist/viewport.js +30 -0
- package/package.json +67 -0
package/dist/data.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { SeriesSchema, TimeSeries, ValueSeries, ValueSeriesSchema } from 'pond-ts';
|
|
2
|
+
/**
|
|
3
|
+
* A chart-ready columnar view of a series: parallel typed arrays for the time
|
|
4
|
+
* (x) and value (y) axes, plus the logical row count.
|
|
5
|
+
*
|
|
6
|
+
* Missing / non-finite values are `NaN` in `y` — the gap signal the draw layers
|
|
7
|
+
* break the line on (`Number.isFinite`, never `!= null`; see
|
|
8
|
+
* `docs/rfcs/charts.md` trap #2).
|
|
9
|
+
*
|
|
10
|
+
* Both arrays are length `length`. `x` is a zero-copy view of the key column's
|
|
11
|
+
* `begin` buffer (immutable by contract — do not mutate); `y` is the value
|
|
12
|
+
* column materialized to a `Float64Array`.
|
|
13
|
+
*/
|
|
14
|
+
export interface ChartSeries {
|
|
15
|
+
readonly x: Float64Array;
|
|
16
|
+
readonly y: Float64Array;
|
|
17
|
+
readonly length: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A chart-ready view of a band (variance envelope): the time axis plus a paired
|
|
21
|
+
* `lower`/`upper` edge per sample. A sample is part of the filled band only
|
|
22
|
+
* where **both** edges are finite; either edge `NaN` is a gap (the fill breaks,
|
|
23
|
+
* it does not bridge — same contract as {@link ChartSeries}).
|
|
24
|
+
*/
|
|
25
|
+
export interface BandSeries {
|
|
26
|
+
readonly x: Float64Array;
|
|
27
|
+
readonly lower: Float64Array;
|
|
28
|
+
readonly upper: Float64Array;
|
|
29
|
+
readonly length: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A chart-ready view of a box-and-whisker series ({@link BoxPlot}): the
|
|
33
|
+
* interval-keyed time axis (`x` = key `begin`, `xEnd` = key `end`, the box's
|
|
34
|
+
* horizontal span) plus the five quantile edges per key —
|
|
35
|
+
* `lower`/`q1`/`median`/`q3`/`upper`. The quantiles are pre-computed columns
|
|
36
|
+
* (a `rolling`/`aggregate` percentile pass upstream); the chart only reads them.
|
|
37
|
+
*
|
|
38
|
+
* A key is drawn only where **all five** quantiles are finite; any one `NaN` is
|
|
39
|
+
* a gap (the box draws nothing — same gap contract as {@link BandSeries}).
|
|
40
|
+
*
|
|
41
|
+
* `x` and `xEnd` are zero-copy views of the key column's `begin`/`end` buffers
|
|
42
|
+
* (immutable by contract — do not mutate). For a point-in-time key the column's
|
|
43
|
+
* `end` coincides with `begin`, so `xEnd === x` and the box collapses to a
|
|
44
|
+
* minimum-width mark via `barSpanPx`; an interval key gives the box real width.
|
|
45
|
+
*/
|
|
46
|
+
export interface BoxSeries {
|
|
47
|
+
readonly x: Float64Array;
|
|
48
|
+
readonly xEnd: Float64Array;
|
|
49
|
+
readonly lower: Float64Array;
|
|
50
|
+
readonly q1: Float64Array;
|
|
51
|
+
readonly median: Float64Array;
|
|
52
|
+
readonly q3: Float64Array;
|
|
53
|
+
readonly upper: Float64Array;
|
|
54
|
+
readonly length: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A chart-ready view of an interval-keyed series for bars: each mark spans
|
|
58
|
+
* `[begin[i], end[i]]` (the key's range) with height `y[i]`. Unlike
|
|
59
|
+
* {@link ChartSeries} (a single `x` point per row), a bar needs **both** key
|
|
60
|
+
* endpoints to know its x-span, so the time axis is split into `begin`/`end`.
|
|
61
|
+
*
|
|
62
|
+
* Missing / non-finite values are `NaN` in `y` — the gap signal {@link drawBars}
|
|
63
|
+
* skips (no bar), same `Number.isFinite` contract as {@link ChartSeries}. For a
|
|
64
|
+
* **point-keyed** series (`begin === end`), `barsFromTimeSeries` derives a span
|
|
65
|
+
* from neighbour spacing so the bars still have width (see there).
|
|
66
|
+
*/
|
|
67
|
+
export interface BarSeries {
|
|
68
|
+
readonly begin: Float64Array;
|
|
69
|
+
readonly end: Float64Array;
|
|
70
|
+
readonly y: Float64Array;
|
|
71
|
+
readonly length: number;
|
|
72
|
+
}
|
|
73
|
+
/** The five quantile column names a {@link boxFromTimeSeries} reads, in order. */
|
|
74
|
+
export interface BoxColumns {
|
|
75
|
+
/** Lower whisker end (e.g. `p5` / `min`). */
|
|
76
|
+
readonly lower: string;
|
|
77
|
+
/** Box bottom — first quartile (e.g. `p25`). */
|
|
78
|
+
readonly q1: string;
|
|
79
|
+
/** Median line inside the box (e.g. `p50`). */
|
|
80
|
+
readonly median: string;
|
|
81
|
+
/** Box top — third quartile (e.g. `p75`). */
|
|
82
|
+
readonly q3: string;
|
|
83
|
+
/** Upper whisker end (e.g. `p95` / `max`). */
|
|
84
|
+
readonly upper: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build a {@link ChartSeries} from a pond `TimeSeries` by reading its columnar
|
|
88
|
+
* buffers directly — no per-event materialization. `column` names a numeric
|
|
89
|
+
* value column; the key column supplies the time axis (`begin`, in ms).
|
|
90
|
+
*
|
|
91
|
+
* @throws RangeError if `column` does not exist.
|
|
92
|
+
* @throws TypeError if `column` is not a numeric column.
|
|
93
|
+
*/
|
|
94
|
+
export declare function fromTimeSeries<S extends SeriesSchema>(series: TimeSeries<S>, column: string): ChartSeries;
|
|
95
|
+
/**
|
|
96
|
+
* Build a {@link ChartSeries} from a pond `ValueSeries` — the value-axis sibling
|
|
97
|
+
* of {@link fromTimeSeries}. The x axis is the series' monotonic value axis
|
|
98
|
+
* (`axisValues()`, e.g. cumulative distance) instead of time; `column` names a
|
|
99
|
+
* numeric value channel (HR, pace, …). The resulting `ChartSeries` is identical
|
|
100
|
+
* in shape — the chart draws it exactly as it draws a time series, only the x
|
|
101
|
+
* scale differs (a value scale rather than `scaleTime`).
|
|
102
|
+
*
|
|
103
|
+
* `x` is the axis key buffer zero-copy (immutable by contract — do not mutate);
|
|
104
|
+
* `y` is the channel materialized to a `Float64Array`, missing cells as `NaN`
|
|
105
|
+
* (the gap signal, same `Number.isFinite` contract as {@link fromTimeSeries}).
|
|
106
|
+
*
|
|
107
|
+
* @throws RangeError if `column` does not exist.
|
|
108
|
+
* @throws TypeError if `column` is not a numeric column.
|
|
109
|
+
*/
|
|
110
|
+
export declare function fromValueSeries<VS extends ValueSeriesSchema>(series: ValueSeries<VS>, column: string): ChartSeries;
|
|
111
|
+
/**
|
|
112
|
+
* Build a {@link BandSeries} from a pond `TimeSeries` — two numeric columns for
|
|
113
|
+
* the `lower`/`upper` edges sharing the series' time axis. The edge columns are
|
|
114
|
+
* typically `rollingByColumn` percentiles (e.g. p25/p75); a sample with either
|
|
115
|
+
* edge missing reads as a gap in the fill.
|
|
116
|
+
*
|
|
117
|
+
* @throws RangeError if `lower` or `upper` does not exist.
|
|
118
|
+
* @throws TypeError if `lower` or `upper` is not a numeric column.
|
|
119
|
+
*/
|
|
120
|
+
export declare function bandFromTimeSeries<S extends SeriesSchema>(series: TimeSeries<S>, lower: string, upper: string): BandSeries;
|
|
121
|
+
/**
|
|
122
|
+
* Build a {@link BoxSeries} from a pond `TimeSeries` — five numeric quantile
|
|
123
|
+
* columns (`lower`/`q1`/`median`/`q3`/`upper`) sharing the series' interval time
|
|
124
|
+
* axis (`begin`/`end`, the box's horizontal span). The quantile columns are
|
|
125
|
+
* typically `rolling`/`aggregate` percentiles (e.g. p5/p25/p50/p75/p95); a key
|
|
126
|
+
* with any quantile missing reads as a gap (the box draws nothing).
|
|
127
|
+
*
|
|
128
|
+
* @throws RangeError if any quantile column does not exist.
|
|
129
|
+
* @throws TypeError if any quantile column is not a numeric column.
|
|
130
|
+
*/
|
|
131
|
+
export declare function boxFromTimeSeries<S extends SeriesSchema>(series: TimeSeries<S>, columns: BoxColumns): BoxSeries;
|
|
132
|
+
/**
|
|
133
|
+
* Build a {@link BarSeries} from a pond `TimeSeries` — one bar per event, the
|
|
134
|
+
* key's `[begin, end]` as the x-span and `column` as the height.
|
|
135
|
+
*
|
|
136
|
+
* **Key-shape fallback (point-keyed series).** The primary form is
|
|
137
|
+
* interval / timeRange-keyed, where each key already carries a `[begin, end]`
|
|
138
|
+
* span. A **point-keyed** (`time`) series has `begin === end` (zero width), so
|
|
139
|
+
* this derives a span from neighbour spacing: each bar is centred on its
|
|
140
|
+
* timestamp and reaches **halfway to each neighbour** (a Voronoi cell on the
|
|
141
|
+
* time axis). The first/last bars mirror their single adjacent gap so the row's
|
|
142
|
+
* end bars match their interior width. A lone point (length 1) has no
|
|
143
|
+
* neighbour, so it keeps zero width and falls back to the renderer's `minWidth`.
|
|
144
|
+
*
|
|
145
|
+
* This makes a uniformly-sampled point series render as contiguous bars (the
|
|
146
|
+
* histogram look) without the caller pre-keying to intervals, while an
|
|
147
|
+
* interval-keyed series (e.g. an `aggregate`/`window` rollup) draws its true
|
|
148
|
+
* bucket spans. Detected by `keyColumn().kind === 'time'`.
|
|
149
|
+
*
|
|
150
|
+
* @throws RangeError if `column` does not exist.
|
|
151
|
+
* @throws TypeError if `column` is not a numeric column.
|
|
152
|
+
*/
|
|
153
|
+
export declare function barsFromTimeSeries<S extends SeriesSchema>(series: TimeSeries<S>, column: string): BarSeries;
|
|
154
|
+
//# sourceMappingURL=data.d.ts.map
|
package/dist/data.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a numeric column into a `Float64Array`, missing cells as `NaN`.
|
|
3
|
+
*
|
|
4
|
+
* Uses `read(i)` — a method on the column *class* — rather than the bulk
|
|
5
|
+
* `toFloat64Array()`. The bulk reader is mounted on the prototype by a
|
|
6
|
+
* side-effect import in pond-ts, which Vite/Rollup production builds tree-shake
|
|
7
|
+
* away (despite the package's `sideEffects` field), so it throws "not a
|
|
8
|
+
* function" in a bundled browser app. See `docs/notes/charts-m1-friction.md`.
|
|
9
|
+
*
|
|
10
|
+
* TODO(charts-perf): restore the bulk typed-array fast-path once the column-API
|
|
11
|
+
* augmentation is bundle-safe in core — that's the columnar throughput win.
|
|
12
|
+
*
|
|
13
|
+
* @throws RangeError if `column` does not exist.
|
|
14
|
+
* @throws TypeError if `column` is not a numeric column.
|
|
15
|
+
*/
|
|
16
|
+
function readNumericColumn(series, column) {
|
|
17
|
+
// Runtime-necessary even though it reads as dead code: `column()` returns
|
|
18
|
+
// `undefined` for an unknown name at runtime, but core's public overload
|
|
19
|
+
// currently types the result as non-`undefined` (see F-3 in the M1 friction
|
|
20
|
+
// note). Keep the guard — the "throws on unknown column" test exercises it.
|
|
21
|
+
const col = series.column(column);
|
|
22
|
+
if (col === undefined) {
|
|
23
|
+
throw new RangeError(`unknown column '${column}'`);
|
|
24
|
+
}
|
|
25
|
+
if (col.kind !== 'number') {
|
|
26
|
+
throw new TypeError(`column '${column}' must be numeric (got '${col.kind}')`);
|
|
27
|
+
}
|
|
28
|
+
const length = series.length;
|
|
29
|
+
const out = new Float64Array(length);
|
|
30
|
+
for (let i = 0; i < length; i += 1) {
|
|
31
|
+
const v = col.read(i);
|
|
32
|
+
out[i] = v === undefined ? NaN : v;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** The key column's `begin` buffer aligned to the logical length (zero-copy). */
|
|
37
|
+
function timeAxis(series) {
|
|
38
|
+
// `begin` may carry trailing capacity beyond the logical length; subarray so
|
|
39
|
+
// it lines up with the value arrays.
|
|
40
|
+
return series.keyColumn().begin.subarray(0, series.length);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The key column's `end` buffer aligned to the logical length (zero-copy). For a
|
|
44
|
+
* point-in-time key the column sets `end === begin`, so this returns the same
|
|
45
|
+
* timestamps as {@link timeAxis} — an interval key gives a distinct span.
|
|
46
|
+
*/
|
|
47
|
+
function timeEndAxis(series) {
|
|
48
|
+
return series.keyColumn().end.subarray(0, series.length);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a {@link ChartSeries} from a pond `TimeSeries` by reading its columnar
|
|
52
|
+
* buffers directly — no per-event materialization. `column` names a numeric
|
|
53
|
+
* value column; the key column supplies the time axis (`begin`, in ms).
|
|
54
|
+
*
|
|
55
|
+
* @throws RangeError if `column` does not exist.
|
|
56
|
+
* @throws TypeError if `column` is not a numeric column.
|
|
57
|
+
*/
|
|
58
|
+
export function fromTimeSeries(series, column) {
|
|
59
|
+
return {
|
|
60
|
+
x: timeAxis(series),
|
|
61
|
+
y: readNumericColumn(series, column),
|
|
62
|
+
length: series.length,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a {@link ChartSeries} from a pond `ValueSeries` — the value-axis sibling
|
|
67
|
+
* of {@link fromTimeSeries}. The x axis is the series' monotonic value axis
|
|
68
|
+
* (`axisValues()`, e.g. cumulative distance) instead of time; `column` names a
|
|
69
|
+
* numeric value channel (HR, pace, …). The resulting `ChartSeries` is identical
|
|
70
|
+
* in shape — the chart draws it exactly as it draws a time series, only the x
|
|
71
|
+
* scale differs (a value scale rather than `scaleTime`).
|
|
72
|
+
*
|
|
73
|
+
* `x` is the axis key buffer zero-copy (immutable by contract — do not mutate);
|
|
74
|
+
* `y` is the channel materialized to a `Float64Array`, missing cells as `NaN`
|
|
75
|
+
* (the gap signal, same `Number.isFinite` contract as {@link fromTimeSeries}).
|
|
76
|
+
*
|
|
77
|
+
* @throws RangeError if `column` does not exist.
|
|
78
|
+
* @throws TypeError if `column` is not a numeric column.
|
|
79
|
+
*/
|
|
80
|
+
export function fromValueSeries(series, column) {
|
|
81
|
+
const col = series.column(column);
|
|
82
|
+
if (col === undefined) {
|
|
83
|
+
throw new RangeError(`unknown column '${column}'`);
|
|
84
|
+
}
|
|
85
|
+
if (col.kind !== 'number') {
|
|
86
|
+
throw new TypeError(`column '${column}' must be numeric (got '${col.kind}')`);
|
|
87
|
+
}
|
|
88
|
+
const length = series.length;
|
|
89
|
+
const y = new Float64Array(length);
|
|
90
|
+
for (let i = 0; i < length; i += 1) {
|
|
91
|
+
const v = col.read(i);
|
|
92
|
+
y[i] = v === undefined ? NaN : v;
|
|
93
|
+
}
|
|
94
|
+
// axisValues() is the key buffer already trimmed to length (zero-copy).
|
|
95
|
+
return { x: series.axisValues(), y, length };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build a {@link BandSeries} from a pond `TimeSeries` — two numeric columns for
|
|
99
|
+
* the `lower`/`upper` edges sharing the series' time axis. The edge columns are
|
|
100
|
+
* typically `rollingByColumn` percentiles (e.g. p25/p75); a sample with either
|
|
101
|
+
* edge missing reads as a gap in the fill.
|
|
102
|
+
*
|
|
103
|
+
* @throws RangeError if `lower` or `upper` does not exist.
|
|
104
|
+
* @throws TypeError if `lower` or `upper` is not a numeric column.
|
|
105
|
+
*/
|
|
106
|
+
export function bandFromTimeSeries(series, lower, upper) {
|
|
107
|
+
return {
|
|
108
|
+
x: timeAxis(series),
|
|
109
|
+
lower: readNumericColumn(series, lower),
|
|
110
|
+
upper: readNumericColumn(series, upper),
|
|
111
|
+
length: series.length,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build a {@link BoxSeries} from a pond `TimeSeries` — five numeric quantile
|
|
116
|
+
* columns (`lower`/`q1`/`median`/`q3`/`upper`) sharing the series' interval time
|
|
117
|
+
* axis (`begin`/`end`, the box's horizontal span). The quantile columns are
|
|
118
|
+
* typically `rolling`/`aggregate` percentiles (e.g. p5/p25/p50/p75/p95); a key
|
|
119
|
+
* with any quantile missing reads as a gap (the box draws nothing).
|
|
120
|
+
*
|
|
121
|
+
* @throws RangeError if any quantile column does not exist.
|
|
122
|
+
* @throws TypeError if any quantile column is not a numeric column.
|
|
123
|
+
*/
|
|
124
|
+
export function boxFromTimeSeries(series, columns) {
|
|
125
|
+
return {
|
|
126
|
+
x: timeAxis(series),
|
|
127
|
+
xEnd: timeEndAxis(series),
|
|
128
|
+
lower: readNumericColumn(series, columns.lower),
|
|
129
|
+
q1: readNumericColumn(series, columns.q1),
|
|
130
|
+
median: readNumericColumn(series, columns.median),
|
|
131
|
+
q3: readNumericColumn(series, columns.q3),
|
|
132
|
+
upper: readNumericColumn(series, columns.upper),
|
|
133
|
+
length: series.length,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Per-row begin/end buffers for the key column, each aligned to the logical
|
|
138
|
+
* length (zero-copy views). For an interval / timeRange key these are the key's
|
|
139
|
+
* own endpoints; for a point (`time`) key `end === begin`, which
|
|
140
|
+
* {@link barsFromTimeSeries} then widens into a span.
|
|
141
|
+
*/
|
|
142
|
+
function keyBeginEnd(series) {
|
|
143
|
+
const key = series.keyColumn();
|
|
144
|
+
const n = series.length;
|
|
145
|
+
// `begin`/`end` may carry trailing capacity beyond the logical length; subarray
|
|
146
|
+
// so they line up with the value array. A `time` key's `end` aliases `begin`
|
|
147
|
+
// (point-in-time), which the caller's point-key fallback replaces.
|
|
148
|
+
return { begin: key.begin.subarray(0, n), end: key.end.subarray(0, n) };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build a {@link BarSeries} from a pond `TimeSeries` — one bar per event, the
|
|
152
|
+
* key's `[begin, end]` as the x-span and `column` as the height.
|
|
153
|
+
*
|
|
154
|
+
* **Key-shape fallback (point-keyed series).** The primary form is
|
|
155
|
+
* interval / timeRange-keyed, where each key already carries a `[begin, end]`
|
|
156
|
+
* span. A **point-keyed** (`time`) series has `begin === end` (zero width), so
|
|
157
|
+
* this derives a span from neighbour spacing: each bar is centred on its
|
|
158
|
+
* timestamp and reaches **halfway to each neighbour** (a Voronoi cell on the
|
|
159
|
+
* time axis). The first/last bars mirror their single adjacent gap so the row's
|
|
160
|
+
* end bars match their interior width. A lone point (length 1) has no
|
|
161
|
+
* neighbour, so it keeps zero width and falls back to the renderer's `minWidth`.
|
|
162
|
+
*
|
|
163
|
+
* This makes a uniformly-sampled point series render as contiguous bars (the
|
|
164
|
+
* histogram look) without the caller pre-keying to intervals, while an
|
|
165
|
+
* interval-keyed series (e.g. an `aggregate`/`window` rollup) draws its true
|
|
166
|
+
* bucket spans. Detected by `keyColumn().kind === 'time'`.
|
|
167
|
+
*
|
|
168
|
+
* @throws RangeError if `column` does not exist.
|
|
169
|
+
* @throws TypeError if `column` is not a numeric column.
|
|
170
|
+
*/
|
|
171
|
+
export function barsFromTimeSeries(series, column) {
|
|
172
|
+
const y = readNumericColumn(series, column);
|
|
173
|
+
const n = series.length;
|
|
174
|
+
const kind = series.keyColumn().kind;
|
|
175
|
+
if (kind !== 'time') {
|
|
176
|
+
// Interval / timeRange: the key's own endpoints are the bar span.
|
|
177
|
+
const { begin, end } = keyBeginEnd(series);
|
|
178
|
+
return { begin, end, y, length: n };
|
|
179
|
+
}
|
|
180
|
+
// Point key (begin === end): synthesize a span from neighbour spacing so the
|
|
181
|
+
// bars have width. Copy into fresh buffers — the key's begin buffer is shared
|
|
182
|
+
// (zero-copy) and must not be mutated.
|
|
183
|
+
const src = series.keyColumn().begin;
|
|
184
|
+
const begin = new Float64Array(n);
|
|
185
|
+
const end = new Float64Array(n);
|
|
186
|
+
for (let i = 0; i < n; i += 1) {
|
|
187
|
+
const t = src[i];
|
|
188
|
+
// Half-gap to the previous point (mirror the next gap at the left edge).
|
|
189
|
+
const prevGap = i > 0 ? t - src[i - 1] : i + 1 < n ? src[i + 1] - t : 0;
|
|
190
|
+
// Half-gap to the next point (mirror the previous gap at the right edge).
|
|
191
|
+
const nextGap = i + 1 < n ? src[i + 1] - t : i > 0 ? t - src[i - 1] : 0;
|
|
192
|
+
begin[i] = t - prevGap / 2;
|
|
193
|
+
end[i] = t + nextGap / 2;
|
|
194
|
+
}
|
|
195
|
+
return { begin, end, y, length: n };
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=data.js.map
|
package/dist/domain.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a y-axis `[lo, hi]` domain from its explicit bounds and the extents of
|
|
3
|
+
* the layers linked to it. An `undefined` bound auto-fits the data: with no
|
|
4
|
+
* finite data the domain is `[0, 1]`, a flat extent gets ±1 of headroom (so a
|
|
5
|
+
* constant line sits mid-row, not on an edge).
|
|
6
|
+
*
|
|
7
|
+
* A **fully auto-fit** domain (both bounds `undefined`) is rounded out to nice
|
|
8
|
+
* boundaries (d3 `.nice()`) — headroom so peaks / whisker caps don't sit on the
|
|
9
|
+
* plot edge, plus rounder tick values. An explicit bound (full or partial) is
|
|
10
|
+
* left **exact**: the caller's number is never nice'd or moved.
|
|
11
|
+
*
|
|
12
|
+
* Guarantees an **ascending, non-degenerate** domain whenever a bound was
|
|
13
|
+
* auto-fit — a partial explicit bound with no (or flat) data on the other side
|
|
14
|
+
* can otherwise invert it (e.g. `min=5` with no data would naively give
|
|
15
|
+
* `[5, 1]`). Two explicit bounds are returned as-is (an inverted explicit domain
|
|
16
|
+
* is a deliberate axis flip; we don't second-guess it).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveYDomain(min: number | undefined, max: number | undefined, extents: Iterable<readonly [number, number] | null>): [number, number];
|
|
19
|
+
//# sourceMappingURL=domain.d.ts.map
|
package/dist/domain.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { scaleLinear } from 'd3-scale';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a y-axis `[lo, hi]` domain from its explicit bounds and the extents of
|
|
4
|
+
* the layers linked to it. An `undefined` bound auto-fits the data: with no
|
|
5
|
+
* finite data the domain is `[0, 1]`, a flat extent gets ±1 of headroom (so a
|
|
6
|
+
* constant line sits mid-row, not on an edge).
|
|
7
|
+
*
|
|
8
|
+
* A **fully auto-fit** domain (both bounds `undefined`) is rounded out to nice
|
|
9
|
+
* boundaries (d3 `.nice()`) — headroom so peaks / whisker caps don't sit on the
|
|
10
|
+
* plot edge, plus rounder tick values. An explicit bound (full or partial) is
|
|
11
|
+
* left **exact**: the caller's number is never nice'd or moved.
|
|
12
|
+
*
|
|
13
|
+
* Guarantees an **ascending, non-degenerate** domain whenever a bound was
|
|
14
|
+
* auto-fit — a partial explicit bound with no (or flat) data on the other side
|
|
15
|
+
* can otherwise invert it (e.g. `min=5` with no data would naively give
|
|
16
|
+
* `[5, 1]`). Two explicit bounds are returned as-is (an inverted explicit domain
|
|
17
|
+
* is a deliberate axis flip; we don't second-guess it).
|
|
18
|
+
*/
|
|
19
|
+
export function resolveYDomain(min, max, extents) {
|
|
20
|
+
// Both bounds explicit: trust them verbatim (allows an intentional flip).
|
|
21
|
+
if (min !== undefined && max !== undefined)
|
|
22
|
+
return [min, max];
|
|
23
|
+
let dataMin = Infinity;
|
|
24
|
+
let dataMax = -Infinity;
|
|
25
|
+
for (const e of extents) {
|
|
26
|
+
if (e) {
|
|
27
|
+
if (e[0] < dataMin)
|
|
28
|
+
dataMin = e[0];
|
|
29
|
+
if (e[1] > dataMax)
|
|
30
|
+
dataMax = e[1];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (dataMin === Infinity) {
|
|
34
|
+
dataMin = 0; // no finite data yet
|
|
35
|
+
dataMax = 1;
|
|
36
|
+
}
|
|
37
|
+
else if (dataMin === dataMax) {
|
|
38
|
+
dataMin -= 1; // flat — give it room
|
|
39
|
+
dataMax += 1;
|
|
40
|
+
}
|
|
41
|
+
let lo = min ?? dataMin;
|
|
42
|
+
let hi = max ?? dataMax;
|
|
43
|
+
// A partial explicit bound can sit at/above the auto-fit other side (explicit
|
|
44
|
+
// min above empty-data's max, or explicit max below the data). Keep the axis
|
|
45
|
+
// ascending by moving the *auto-fit* side — never discard the caller's
|
|
46
|
+
// explicit bound. Exactly one side is explicit here: both-explicit returned
|
|
47
|
+
// early, and a both-auto domain can't invert after the empty/flat guards.
|
|
48
|
+
if (lo >= hi) {
|
|
49
|
+
if (min === undefined)
|
|
50
|
+
lo = hi - 1; // max is explicit → preserve it
|
|
51
|
+
else
|
|
52
|
+
hi = lo + 1; // min is explicit → preserve it
|
|
53
|
+
}
|
|
54
|
+
// Fully auto-fit → round the domain out for headroom + nicer ticks. A
|
|
55
|
+
// partial/full explicit bound is left exact (returned as-is below).
|
|
56
|
+
if (min === undefined && max === undefined) {
|
|
57
|
+
return scaleLinear().domain([lo, hi]).nice().domain();
|
|
58
|
+
}
|
|
59
|
+
return [lo, hi];
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=domain.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-driven point encoding for {@link ScatterChart} — the **deliberate,
|
|
3
|
+
* signed-off exception** to the package's single-styling-channel rule.
|
|
4
|
+
*
|
|
5
|
+
* Every other layer takes one styling input: a semantic `as` token the theme
|
|
6
|
+
* maps to a style. That discipline exists because react-timeseries-charts' free
|
|
7
|
+
* per-component style accessor (a function from a datum to a style object) bred
|
|
8
|
+
* a class of styling bugs. Scatter keeps the base mark on that same channel
|
|
9
|
+
* (`theme.scatter[as]` → the default radius + colour) but **adds** size and
|
|
10
|
+
* colour driven *from the data*: a column value run through a scale. The
|
|
11
|
+
* difference that keeps this safe is that the input is a **column name + a
|
|
12
|
+
* range**, not an arbitrary callback — there is no place to hide a styling bug
|
|
13
|
+
* in `{ column: 'velocity', range: [2, 12] }`.
|
|
14
|
+
*
|
|
15
|
+
* Both encodings build a *linear* scale over the **finite extent** of the named
|
|
16
|
+
* column (NaN / missing cells ignored, so a gap doesn't drag the domain) and map
|
|
17
|
+
* it into the requested output range. The scale is precomputed once per resolve;
|
|
18
|
+
* the returned `radiusAt` / `colorAt` are O(1) per point.
|
|
19
|
+
*
|
|
20
|
+
* Pure + unit-tested (`test/encoding.test.ts`); scatter is the sole consumer.
|
|
21
|
+
*/
|
|
22
|
+
import type { ChartSeries } from './data.js';
|
|
23
|
+
/**
|
|
24
|
+
* Per-point **radius** encoding. Either:
|
|
25
|
+
* - a fixed radius in CSS px (every point the same size — the common case), or
|
|
26
|
+
* - `{ column, range }`: map the column's finite extent linearly onto
|
|
27
|
+
* `[minR, maxR]` px. A point whose radius column is non-finite falls back to
|
|
28
|
+
* the base radius (it still draws, at the default size).
|
|
29
|
+
*
|
|
30
|
+
* **Omitted ⇒ the base radius** from the style (`theme.scatter[as].radius`).
|
|
31
|
+
*/
|
|
32
|
+
export type RadiusEncoding = number | {
|
|
33
|
+
/** Numeric column whose value drives each point's radius. */
|
|
34
|
+
readonly column: string;
|
|
35
|
+
/** Output radius range in px, `[atColumnMin, atColumnMax]`. */
|
|
36
|
+
readonly range: readonly [number, number];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Per-point **colour** encoding — `{ column, range }`: map the column's finite
|
|
40
|
+
* extent linearly onto a two-stop colour ramp `[atMin, atMax]` (interpolated in
|
|
41
|
+
* sRGB). A point whose colour column is non-finite falls back to the base colour
|
|
42
|
+
* (the single styling channel — `theme.scatter[as].color`).
|
|
43
|
+
*
|
|
44
|
+
* **Omitted ⇒ the base colour** for every point. The `range` stops must be CSS
|
|
45
|
+
* hex (`#rgb` / `#rrggbb`); a non-hex stop disables interpolation for that point
|
|
46
|
+
* (falls back to the base colour) rather than guessing.
|
|
47
|
+
*/
|
|
48
|
+
export interface ColorEncoding {
|
|
49
|
+
/** Numeric column whose value drives each point's colour. */
|
|
50
|
+
readonly column: string;
|
|
51
|
+
/** Two-stop colour ramp, `[atColumnMin, atColumnMax]` — CSS hex. */
|
|
52
|
+
readonly range: readonly [string, string];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A resolved encoding: O(1) per-point accessors over a {@link ChartSeries}'
|
|
56
|
+
* index. `radiusAt`/`colorAt` are indexed by the *same* row position as the
|
|
57
|
+
* scatter's `x`/`y` arrays — a point with a non-finite encoding value (or no
|
|
58
|
+
* encoding configured) gets the base radius / colour.
|
|
59
|
+
*/
|
|
60
|
+
export interface ResolvedEncoding {
|
|
61
|
+
/** This point's radius in px (base radius if unencoded / non-finite). */
|
|
62
|
+
radiusAt(i: number): number;
|
|
63
|
+
/** This point's fill colour (base colour if unencoded / non-finite). */
|
|
64
|
+
colorAt(i: number): string;
|
|
65
|
+
}
|
|
66
|
+
/** A numeric column read into a `Float64Array` (gaps as NaN), by name. */
|
|
67
|
+
export type ColumnReader = (column: string) => Float64Array;
|
|
68
|
+
/**
|
|
69
|
+
* The `[min, max]` of the **finite** values in `col`, or `null` if none are
|
|
70
|
+
* finite. (Same gap-aware contract as `yExtent`; duplicated here so encoding
|
|
71
|
+
* stays a leaf module with no draw-layer dependency.)
|
|
72
|
+
*/
|
|
73
|
+
export declare function finiteExtent(col: Float64Array): [number, number] | null;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a scatter's data-driven encoding into O(1) per-point accessors.
|
|
76
|
+
*
|
|
77
|
+
* @param cs the scatter's columnar view (its `length` bounds the indices)
|
|
78
|
+
* @param baseRadius the style's base radius (the fixed-size fallback)
|
|
79
|
+
* @param baseColor the style's base colour (the single-styling-channel colour)
|
|
80
|
+
* @param radius the {@link RadiusEncoding} (a number, a `{column,range}`, or omitted)
|
|
81
|
+
* @param color the {@link ColorEncoding} (a `{column,range}`, or omitted)
|
|
82
|
+
* @param readColumn reads a named numeric column to a `Float64Array` (gaps NaN)
|
|
83
|
+
*
|
|
84
|
+
* A `{column}` encoding whose column is entirely non-finite degrades to the base
|
|
85
|
+
* value (no scale to build); an individual non-finite cell likewise falls back,
|
|
86
|
+
* so every point always resolves to a finite radius and a concrete colour.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveEncoding(cs: ChartSeries, baseRadius: number, baseColor: string, radius: RadiusEncoding | undefined, color: ColorEncoding | undefined, readColumn: ColumnReader): ResolvedEncoding;
|
|
89
|
+
//# sourceMappingURL=encoding.d.ts.map
|
package/dist/encoding.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-driven point encoding for {@link ScatterChart} — the **deliberate,
|
|
3
|
+
* signed-off exception** to the package's single-styling-channel rule.
|
|
4
|
+
*
|
|
5
|
+
* Every other layer takes one styling input: a semantic `as` token the theme
|
|
6
|
+
* maps to a style. That discipline exists because react-timeseries-charts' free
|
|
7
|
+
* per-component style accessor (a function from a datum to a style object) bred
|
|
8
|
+
* a class of styling bugs. Scatter keeps the base mark on that same channel
|
|
9
|
+
* (`theme.scatter[as]` → the default radius + colour) but **adds** size and
|
|
10
|
+
* colour driven *from the data*: a column value run through a scale. The
|
|
11
|
+
* difference that keeps this safe is that the input is a **column name + a
|
|
12
|
+
* range**, not an arbitrary callback — there is no place to hide a styling bug
|
|
13
|
+
* in `{ column: 'velocity', range: [2, 12] }`.
|
|
14
|
+
*
|
|
15
|
+
* Both encodings build a *linear* scale over the **finite extent** of the named
|
|
16
|
+
* column (NaN / missing cells ignored, so a gap doesn't drag the domain) and map
|
|
17
|
+
* it into the requested output range. The scale is precomputed once per resolve;
|
|
18
|
+
* the returned `radiusAt` / `colorAt` are O(1) per point.
|
|
19
|
+
*
|
|
20
|
+
* Pure + unit-tested (`test/encoding.test.ts`); scatter is the sole consumer.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The `[min, max]` of the **finite** values in `col`, or `null` if none are
|
|
24
|
+
* finite. (Same gap-aware contract as `yExtent`; duplicated here so encoding
|
|
25
|
+
* stays a leaf module with no draw-layer dependency.)
|
|
26
|
+
*/
|
|
27
|
+
export function finiteExtent(col) {
|
|
28
|
+
let min = Infinity;
|
|
29
|
+
let max = -Infinity;
|
|
30
|
+
for (let i = 0; i < col.length; i += 1) {
|
|
31
|
+
const v = col[i];
|
|
32
|
+
if (Number.isFinite(v)) {
|
|
33
|
+
if (v < min)
|
|
34
|
+
min = v;
|
|
35
|
+
if (v > max)
|
|
36
|
+
max = v;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return min === Infinity ? null : [min, max];
|
|
40
|
+
}
|
|
41
|
+
/** Linear interpolate `t` (0..1) across `[a, b]`. */
|
|
42
|
+
function lerp(a, b, t) {
|
|
43
|
+
return a + (b - a) * t;
|
|
44
|
+
}
|
|
45
|
+
/** Parse a CSS hex colour to `[r, g, b]` (0–255), or `null` if not hex. */
|
|
46
|
+
function parseHex(color) {
|
|
47
|
+
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
|
|
48
|
+
if (m === null)
|
|
49
|
+
return null;
|
|
50
|
+
const h = m[1];
|
|
51
|
+
if (h.length === 3) {
|
|
52
|
+
return [
|
|
53
|
+
parseInt(h[0] + h[0], 16),
|
|
54
|
+
parseInt(h[1] + h[1], 16),
|
|
55
|
+
parseInt(h[2] + h[2], 16),
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
parseInt(h.slice(0, 2), 16),
|
|
60
|
+
parseInt(h.slice(2, 4), 16),
|
|
61
|
+
parseInt(h.slice(4, 6), 16),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
/** Interpolate two hex colours in sRGB at `t` (0..1) → `rgb(r, g, b)`. */
|
|
65
|
+
function mixHex(from, to, t) {
|
|
66
|
+
const a = parseHex(from);
|
|
67
|
+
const b = parseHex(to);
|
|
68
|
+
if (a === null || b === null)
|
|
69
|
+
return from; // non-hex stop: leave it to caller
|
|
70
|
+
const r = Math.round(lerp(a[0], b[0], t));
|
|
71
|
+
const g = Math.round(lerp(a[1], b[1], t));
|
|
72
|
+
const bl = Math.round(lerp(a[2], b[2], t));
|
|
73
|
+
return `rgb(${r}, ${g}, ${bl})`;
|
|
74
|
+
}
|
|
75
|
+
/** Position of `v` within `[min, max]` as a clamped 0..1 fraction (0 if flat). */
|
|
76
|
+
function normalize(v, min, max) {
|
|
77
|
+
if (max - min < 1e-12)
|
|
78
|
+
return 0; // degenerate extent → everything at the low stop
|
|
79
|
+
const t = (v - min) / (max - min);
|
|
80
|
+
return t < 0 ? 0 : t > 1 ? 1 : t;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a scatter's data-driven encoding into O(1) per-point accessors.
|
|
84
|
+
*
|
|
85
|
+
* @param cs the scatter's columnar view (its `length` bounds the indices)
|
|
86
|
+
* @param baseRadius the style's base radius (the fixed-size fallback)
|
|
87
|
+
* @param baseColor the style's base colour (the single-styling-channel colour)
|
|
88
|
+
* @param radius the {@link RadiusEncoding} (a number, a `{column,range}`, or omitted)
|
|
89
|
+
* @param color the {@link ColorEncoding} (a `{column,range}`, or omitted)
|
|
90
|
+
* @param readColumn reads a named numeric column to a `Float64Array` (gaps NaN)
|
|
91
|
+
*
|
|
92
|
+
* A `{column}` encoding whose column is entirely non-finite degrades to the base
|
|
93
|
+
* value (no scale to build); an individual non-finite cell likewise falls back,
|
|
94
|
+
* so every point always resolves to a finite radius and a concrete colour.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveEncoding(cs, baseRadius, baseColor, radius, color, readColumn) {
|
|
97
|
+
// --- radius ---
|
|
98
|
+
let radiusAt;
|
|
99
|
+
if (radius === undefined || typeof radius === 'number') {
|
|
100
|
+
const r = radius === undefined ? baseRadius : radius;
|
|
101
|
+
radiusAt = () => r;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const col = readColumn(radius.column);
|
|
105
|
+
const extent = finiteExtent(col);
|
|
106
|
+
const [minR, maxR] = radius.range;
|
|
107
|
+
if (extent === null) {
|
|
108
|
+
radiusAt = () => baseRadius; // column has no finite values
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const [lo, hi] = extent;
|
|
112
|
+
radiusAt = (i) => {
|
|
113
|
+
const v = col[i];
|
|
114
|
+
if (!Number.isFinite(v))
|
|
115
|
+
return baseRadius;
|
|
116
|
+
return lerp(minR, maxR, normalize(v, lo, hi));
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// --- colour ---
|
|
121
|
+
let colorAt;
|
|
122
|
+
if (color === undefined) {
|
|
123
|
+
colorAt = () => baseColor;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const col = readColumn(color.column);
|
|
127
|
+
const extent = finiteExtent(col);
|
|
128
|
+
const [from, to] = color.range;
|
|
129
|
+
if (extent === null) {
|
|
130
|
+
colorAt = () => baseColor;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const [lo, hi] = extent;
|
|
134
|
+
colorAt = (i) => {
|
|
135
|
+
const v = col[i];
|
|
136
|
+
if (!Number.isFinite(v))
|
|
137
|
+
return baseColor;
|
|
138
|
+
return mixHex(from, to, normalize(v, lo, hi));
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { radiusAt, colorAt };
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=encoding.js.map
|