pond-ts 0.28.0 → 0.30.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 +62 -2
- package/dist/batch/by-column.d.ts +1 -0
- package/dist/batch/by-column.js +43 -15
- package/dist/batch/rolling-by-column.d.ts +9 -2
- package/dist/batch/rolling-by-column.js +44 -0
- package/dist/batch/time-series.d.ts +26 -3
- package/dist/batch/time-series.js +42 -5
- package/dist/reducers/index.js +5 -0
- package/dist/schema/aggregate.d.ts +3 -3
- package/dist/schema/events.d.ts +3 -1
- package/dist/schema/reduce.d.ts +1 -1
- package/dist/schema/rolling.d.ts +1 -1
- package/dist/schema/series.d.ts +19 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
7
7
|
file covers both packages. Pre-1.0: minor bumps may include new features and
|
|
8
8
|
type-level changes; patch bumps are strictly additive.
|
|
9
9
|
|
|
10
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.
|
|
10
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...HEAD
|
|
11
|
+
[0.30.0]: https://github.com/pjm17971/pond-ts/compare/v0.29.0...v0.30.0
|
|
12
|
+
[0.29.0]: https://github.com/pjm17971/pond-ts/compare/v0.28.0...v0.29.0
|
|
11
13
|
[0.28.0]: https://github.com/pjm17971/pond-ts/compare/v0.27.0...v0.28.0
|
|
12
14
|
[0.27.0]: https://github.com/pjm17971/pond-ts/compare/v0.26.0...v0.27.0
|
|
13
15
|
[0.26.0]: https://github.com/pjm17971/pond-ts/compare/v0.25.0...v0.26.0
|
|
@@ -20,7 +22,65 @@ type-level changes; patch bumps are strictly additive.
|
|
|
20
22
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
21
23
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
22
24
|
|
|
23
|
-
## [
|
|
25
|
+
## [0.30.0] — 2026-06-17
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`rollingByColumn(col, { radius, at }, mapping)` — evaluate at explicit
|
|
30
|
+
centers.** `at` takes a **non-decreasing** array of center values (e.g. a chart's
|
|
31
|
+
coarse display grid) and returns **one record per center**, instead of the
|
|
32
|
+
default one-per-row. A center with no rows within `±radius` yields each
|
|
33
|
+
reducer's empty value. Same O(n + centers) two-pointer. Closes the
|
|
34
|
+
evaluate-at-grid gap surfaced adopting `rollingByColumn` for a chart variance
|
|
35
|
+
band. (estela F-rolling-by-row.)
|
|
36
|
+
- **`smooth(col, 'movingAverage' | 'loess', { …, missing: 'skip' })` —
|
|
37
|
+
validity-respecting smoothing.** By default (`missing: 'bridge'`) a cell whose
|
|
38
|
+
own value is missing is still assigned a smoothed value from its present
|
|
39
|
+
neighbours — the line is drawn _across_ the hole. `missing: 'skip'` keeps a
|
|
40
|
+
missing cell **missing** in the output, so a sustained dropout (a coast, a
|
|
41
|
+
sensor gap) is preserved as a break rather than fabricated through. Present
|
|
42
|
+
cells smooth over only the present values in their window either way. `ema`
|
|
43
|
+
takes no `missing` option (it is causal and never fabricates across a gap). A
|
|
44
|
+
`maxGap` hard segment boundary is a deferred follow-on. (estela
|
|
45
|
+
F-smooth-interactive.)
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **`byColumn(…, { inclusive: '(]' })` floor edge is now inclusive.** Under
|
|
50
|
+
`'(]'`, interior bins stay upper-inclusive (`(eᵢ, eᵢ₊₁]`) but the **floor `e₀`
|
|
51
|
+
is inclusive** (bin 0 is `[e₀, e₁]`), so a value at exactly the minimum edge —
|
|
52
|
+
e.g. a `0` W coast/stop sample at a zone floor of 0 — lands in bin 0 instead of
|
|
53
|
+
being dropped (the `include_lowest` convention). Previously the floor was
|
|
54
|
+
exclusive. (estela F-inclusive-floor.)
|
|
55
|
+
|
|
56
|
+
## [0.29.0] — 2026-06-17
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **`byColumn({ edges, inclusive })`** — `inclusive: '(]'` makes edge bins
|
|
61
|
+
upper-inclusive (`(eᵢ, eᵢ₊₁]`), for Coggan power / HR zones where a sample on a
|
|
62
|
+
zone's top edge belongs to the lower zone (the first edge becomes an exclusive
|
|
63
|
+
floor). Defaults to `'[)'` (unchanged — lower-inclusive `[eᵢ, eᵢ₊₁)`). (estela
|
|
64
|
+
F-geo-2 zone inclusivity.)
|
|
65
|
+
- **`'mean'` reducer alias for `'avg'`** — `'mean'` is now an accepted built-in
|
|
66
|
+
reducer name across `aggregate` / `rolling` / `byColumn` / `rollingByColumn` /
|
|
67
|
+
`reduce` (and the live equivalents), at **both runtime and the type level**: it
|
|
68
|
+
resolves to the `avg` kernel and classifies as numeric output
|
|
69
|
+
(`number | undefined`), exactly like `'avg'`. Matches the column API's
|
|
70
|
+
`Float64Column.mean()`. (estela F-reducer-naming.)
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- **`RowForSchema` honors `required: false`** — a **value** column declared
|
|
75
|
+
`required: false` now accepts `undefined` in its tuple-row cell at the type
|
|
76
|
+
level (matching the runtime, which records it as missing), so optional cells no
|
|
77
|
+
longer need an `as never` cast. The **key (first) column stays required** even
|
|
78
|
+
if marked `required: false` (the constructor always requires it). `null` is
|
|
79
|
+
still not admitted for tuple rows (only the JSON object-row path takes `null`).
|
|
80
|
+
Correspondingly, **`.rows` / `toRows()` now type an optional cell as
|
|
81
|
+
`… | undefined`** (`NormalizedRowForSchema`), so reading a possibly-missing
|
|
82
|
+
cell is no longer unsoundly typed as present — a type tightening on output for
|
|
83
|
+
schemas that use `required: false`. (estela F-geo-row-optional; Codex-hardened.)
|
|
24
84
|
|
|
25
85
|
## [0.28.0] — 2026-06-17
|
|
26
86
|
|
package/dist/batch/by-column.js
CHANGED
|
@@ -45,21 +45,49 @@ export function computeByColumn(store, binColName, spec, columnSpecs) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
const last = edges.length - 1;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
// `inclusive` picks the bin a value exactly on an interior edge falls into.
|
|
49
|
+
// `'[)'` (default): bins are `[eᵢ, eᵢ₊₁)`, lower-inclusive — a boundary value
|
|
50
|
+
// goes to the bin ABOVE; range is `[e₀, eₙ)`. `'(]'`: interior bins are
|
|
51
|
+
// `(eᵢ, eᵢ₊₁]`, upper-inclusive (Coggan power/HR zones — a sample at exactly
|
|
52
|
+
// a zone's top edge is the LOWER zone) — a boundary value goes to the bin
|
|
53
|
+
// BELOW. The **floor `e₀` is inclusive** (bin 0 is `[e₀, e₁]`), so a value at
|
|
54
|
+
// exactly the minimum edge — e.g. a `0` W coast/stop sample — lands in bin 0
|
|
55
|
+
// rather than being dropped (the `include_lowest` convention; estela
|
|
56
|
+
// F-inclusive-floor). Range is `[e₀, eₙ]`.
|
|
57
|
+
binOf =
|
|
58
|
+
spec.inclusive === '(]'
|
|
59
|
+
? (v) => {
|
|
60
|
+
if (v < edges[0] || v > edges[last])
|
|
61
|
+
return NaN; // out of [e₀, eₙ]
|
|
62
|
+
// rightmost edge STRICTLY less than v (so an interior edge goes to
|
|
63
|
+
// the bin below), clamped to [0, last); v === e₀ → bin 0 (floor
|
|
64
|
+
// inclusive).
|
|
65
|
+
let lo = 0;
|
|
66
|
+
let hi = last;
|
|
67
|
+
while (lo < hi) {
|
|
68
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
69
|
+
if (edges[mid] < v)
|
|
70
|
+
lo = mid;
|
|
71
|
+
else
|
|
72
|
+
hi = mid - 1;
|
|
73
|
+
}
|
|
74
|
+
return lo;
|
|
75
|
+
}
|
|
76
|
+
: (v) => {
|
|
77
|
+
if (v < edges[0] || v >= edges[last])
|
|
78
|
+
return NaN; // out of [e₀, eₙ)
|
|
79
|
+
// rightmost edge <= v, clamped to a valid bin [0, last)
|
|
80
|
+
let lo = 0;
|
|
81
|
+
let hi = last;
|
|
82
|
+
while (lo < hi) {
|
|
83
|
+
const mid = (lo + hi + 1) >>> 1;
|
|
84
|
+
if (edges[mid] <= v)
|
|
85
|
+
lo = mid;
|
|
86
|
+
else
|
|
87
|
+
hi = mid - 1;
|
|
88
|
+
}
|
|
89
|
+
return lo;
|
|
90
|
+
};
|
|
63
91
|
rangeOf = (i) => ({ start: edges[i], end: edges[i + 1] });
|
|
64
92
|
}
|
|
65
93
|
else {
|
|
@@ -3,12 +3,19 @@ import type { ColumnValue } from '../schema/index.js';
|
|
|
3
3
|
import type { AggregateColumnSpec } from './aggregate-columns.js';
|
|
4
4
|
/**
|
|
5
5
|
* Window spec for {@link TimeSeries.rollingByColumn}: a **centered** window of
|
|
6
|
-
* half-width `radius`, in the axis column's own units.
|
|
7
|
-
*
|
|
6
|
+
* half-width `radius`, in the axis column's own units.
|
|
7
|
+
*
|
|
8
|
+
* By default a window is evaluated at **every input row** (one record per row).
|
|
9
|
+
* Pass `at` — a **non-decreasing** array of explicit center values (e.g. a chart's
|
|
10
|
+
* coarse display grid) — to evaluate at those centers instead, returning one
|
|
11
|
+
* record per center. Each center's window is the rows whose axis value lies
|
|
12
|
+
* within `±radius` of it; a center need not coincide with any row, and a center
|
|
13
|
+
* with no rows in range yields each reducer's empty value. See
|
|
8
14
|
* `docs/notes/rolling-by-column.md`.
|
|
9
15
|
*/
|
|
10
16
|
export type WindowSpec = {
|
|
11
17
|
radius: number;
|
|
18
|
+
at?: readonly number[];
|
|
12
19
|
};
|
|
13
20
|
/** One windowed record: the mapped aggregates over the window centered at a row. */
|
|
14
21
|
export type WindowRecord = Record<string, ColumnValue | undefined>;
|
|
@@ -55,6 +55,50 @@ export function computeRollingByColumn(store, axisColName, spec, columnSpecs) {
|
|
|
55
55
|
// the whole sweep — this is what makes it O(n) rather than O(n · window).
|
|
56
56
|
const states = columnSpecs.map((s) => rollingStateFor(s.reducer));
|
|
57
57
|
const specCount = columnSpecs.length;
|
|
58
|
+
// `at` mode (estela F-rolling-by-row): evaluate at explicit ascending centers
|
|
59
|
+
// (e.g. a chart's display grid) rather than at every input row. One record per
|
|
60
|
+
// center; the two-pointer sweeps the same shared states across the centers, so
|
|
61
|
+
// it stays O(n + at.length). A center with no rows in `±radius` empties the
|
|
62
|
+
// window and yields each reducer's empty value.
|
|
63
|
+
if (spec.at !== undefined) {
|
|
64
|
+
const at = spec.at;
|
|
65
|
+
for (let k = 0; k < at.length; k += 1) {
|
|
66
|
+
if (!Number.isFinite(at[k])) {
|
|
67
|
+
throw new RangeError(`rollingByColumn: at[${k}] is not finite`);
|
|
68
|
+
}
|
|
69
|
+
if (k > 0 && at[k] < at[k - 1]) {
|
|
70
|
+
throw new RangeError('rollingByColumn: at centers must be non-decreasing');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const atOut = new Array(at.length);
|
|
74
|
+
let alo = 0;
|
|
75
|
+
let ahi = 0; // window holds compact positions [alo, ahi)
|
|
76
|
+
for (let k = 0; k < at.length; k += 1) {
|
|
77
|
+
const c = at[k];
|
|
78
|
+
const wlo = c - radius;
|
|
79
|
+
const whi = c + radius;
|
|
80
|
+
while (ahi < m && ax[ahi] <= whi) {
|
|
81
|
+
const r = idx[ahi];
|
|
82
|
+
for (let cc = 0; cc < specCount; cc += 1) {
|
|
83
|
+
states[cc].add(r, sourceCols[cc].read(r));
|
|
84
|
+
}
|
|
85
|
+
ahi += 1;
|
|
86
|
+
}
|
|
87
|
+
while (alo < ahi && ax[alo] < wlo) {
|
|
88
|
+
const r = idx[alo];
|
|
89
|
+
for (let cc = 0; cc < specCount; cc += 1) {
|
|
90
|
+
states[cc].remove(r, sourceCols[cc].read(r));
|
|
91
|
+
}
|
|
92
|
+
alo += 1;
|
|
93
|
+
}
|
|
94
|
+
const rec = {};
|
|
95
|
+
for (let cc = 0; cc < specCount; cc += 1) {
|
|
96
|
+
rec[columnSpecs[cc].output] = states[cc].snapshot();
|
|
97
|
+
}
|
|
98
|
+
atOut[k] = rec;
|
|
99
|
+
}
|
|
100
|
+
return atOut;
|
|
101
|
+
}
|
|
58
102
|
const out = new Array(n);
|
|
59
103
|
let lo = 0;
|
|
60
104
|
let hi = 0; // the window currently holds compact positions [lo, hi)
|
|
@@ -539,8 +539,13 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
539
539
|
* source (cumulative distance / work) yields contiguous ranges (per-km
|
|
540
540
|
* splits, elevation-vs-distance profile); a non-monotonic source (power)
|
|
541
541
|
* yields a histogram (distribution).
|
|
542
|
-
* - `{ edges }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins
|
|
543
|
-
* `[
|
|
542
|
+
* - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
|
|
543
|
+
* `inclusive` defaults to `'[)'` (bin `i` = `[eᵢ, eᵢ₊₁)`, lower-inclusive);
|
|
544
|
+
* pass `'(]'` for upper-inclusive interior bins (`(eᵢ, eᵢ₊₁]`) — Coggan power
|
|
545
|
+
* / HR zones, where a sample exactly on a zone's top edge belongs to the
|
|
546
|
+
* lower zone. The floor `e₀` stays **inclusive** (bin 0 is `[e₀, e₁]`), so a
|
|
547
|
+
* minimum-edge value (e.g. a `0` W coast sample) lands in bin 0 rather than
|
|
548
|
+
* being dropped. Always emits all `n` bins.
|
|
544
549
|
*
|
|
545
550
|
* A row whose bin value is missing / non-finite (or, for `edges`, outside
|
|
546
551
|
* `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
|
|
@@ -574,7 +579,12 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
574
579
|
* reducer non-finite policy still applies to the *source* columns.
|
|
575
580
|
*
|
|
576
581
|
* The window is centered and inclusive (`col[i] − radius ≤ col[j] ≤ col[i] +
|
|
577
|
-
* radius`); a single O(n) two-pointer sweep maintains the window.
|
|
582
|
+
* radius`); a single O(n) two-pointer sweep maintains the window.
|
|
583
|
+
*
|
|
584
|
+
* Pass `{ radius, at }` — a **non-decreasing** array of explicit center values
|
|
585
|
+
* (e.g. a chart's coarse display grid) — to evaluate at those centers instead
|
|
586
|
+
* of at every row, returning **one record per center** (a center with no rows
|
|
587
|
+
* in range yields each reducer's empty value). See
|
|
578
588
|
* `docs/notes/rolling-by-column.md`.
|
|
579
589
|
*/
|
|
580
590
|
rollingByColumn<const Mapping extends ValidatedAggregateMap<S, Mapping>>(col: NumericColumnNameForSchema<S>, spec: WindowSpec, mapping: Mapping): Array<ReduceResult<S, Mapping>>;
|
|
@@ -995,6 +1005,17 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
995
1005
|
* When `output` is omitted, the smoothed values replace the target column. When `output` is
|
|
996
1006
|
* supplied, the smoothed values are appended as a new optional numeric column.
|
|
997
1007
|
*
|
|
1008
|
+
* **Missing cells** (`movingAverage` / `loess`): by default (`missing:
|
|
1009
|
+
* 'bridge'`) a cell whose own value is missing is still assigned a smoothed
|
|
1010
|
+
* value computed from its present neighbours — i.e. the line is drawn *across*
|
|
1011
|
+
* the hole. Pass `missing: 'skip'` to keep a missing cell **missing** in the
|
|
1012
|
+
* output, so a sustained dropout (a coast, a sensor gap) is preserved as a
|
|
1013
|
+
* break rather than fabricated through. Present cells already smooth over only
|
|
1014
|
+
* the present values in their window either way. `ema` is causal and never
|
|
1015
|
+
* fabricates across a gap, so it takes no `missing` option. (estela
|
|
1016
|
+
* F-smooth-interactive; a `maxGap` hard segment boundary is a deferred
|
|
1017
|
+
* follow-on.)
|
|
1018
|
+
*
|
|
998
1019
|
* **Multi-entity series:** the smoothing window pulls values from
|
|
999
1020
|
* every entity into each smoothed point — `host-A`'s smoothed value
|
|
1000
1021
|
* is blended with `host-B`'s and `host-C`'s. On a series carrying
|
|
@@ -1010,9 +1031,11 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1010
1031
|
window: DurationInput;
|
|
1011
1032
|
alignment?: RollingAlignment;
|
|
1012
1033
|
output?: Output;
|
|
1034
|
+
missing?: 'skip' | 'bridge';
|
|
1013
1035
|
} | {
|
|
1014
1036
|
span: number;
|
|
1015
1037
|
output?: Output;
|
|
1038
|
+
missing?: 'skip' | 'bridge';
|
|
1016
1039
|
}): TimeSeries<Output extends string ? SmoothAppendSchema<S, Output> : SmoothSchema<S, Target>>;
|
|
1017
1040
|
/** Example: `series.slice(0, 10)`. Returns a positional half-open slice of the series. */
|
|
1018
1041
|
slice(beginIndex?: number, endIndex?: number): TimeSeries<S>;
|
|
@@ -872,6 +872,10 @@ export class TimeSeries {
|
|
|
872
872
|
}
|
|
873
873
|
/** Example: `series.rows`. Returns the normalized row view of the series. */
|
|
874
874
|
get rows() {
|
|
875
|
+
// `toRows` returns runtime-normalized rows (Time/Interval keys, `undefined`
|
|
876
|
+
// for missing cells); the double cast is the existing trust point — the
|
|
877
|
+
// `RowForSchema`→`NormalizedRowForSchema` conditional types no longer overlap
|
|
878
|
+
// structurally for a direct cast now that both honor `required: false`.
|
|
875
879
|
return toRows(this.schema, this.events);
|
|
876
880
|
}
|
|
877
881
|
/** Example: `series.toRows()`. Returns normalized row arrays using `Time`/`TimeRange`/`Interval` keys and `undefined` for missing payload values. */
|
|
@@ -1391,8 +1395,13 @@ export class TimeSeries {
|
|
|
1391
1395
|
* source (cumulative distance / work) yields contiguous ranges (per-km
|
|
1392
1396
|
* splits, elevation-vs-distance profile); a non-monotonic source (power)
|
|
1393
1397
|
* yields a histogram (distribution).
|
|
1394
|
-
* - `{ edges }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins
|
|
1395
|
-
* `[
|
|
1398
|
+
* - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
|
|
1399
|
+
* `inclusive` defaults to `'[)'` (bin `i` = `[eᵢ, eᵢ₊₁)`, lower-inclusive);
|
|
1400
|
+
* pass `'(]'` for upper-inclusive interior bins (`(eᵢ, eᵢ₊₁]`) — Coggan power
|
|
1401
|
+
* / HR zones, where a sample exactly on a zone's top edge belongs to the
|
|
1402
|
+
* lower zone. The floor `e₀` stays **inclusive** (bin 0 is `[e₀, e₁]`), so a
|
|
1403
|
+
* minimum-edge value (e.g. a `0` W coast sample) lands in bin 0 rather than
|
|
1404
|
+
* being dropped. Always emits all `n` bins.
|
|
1396
1405
|
*
|
|
1397
1406
|
* A row whose bin value is missing / non-finite (or, for `edges`, outside
|
|
1398
1407
|
* `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
|
|
@@ -1426,7 +1435,12 @@ export class TimeSeries {
|
|
|
1426
1435
|
* reducer non-finite policy still applies to the *source* columns.
|
|
1427
1436
|
*
|
|
1428
1437
|
* The window is centered and inclusive (`col[i] − radius ≤ col[j] ≤ col[i] +
|
|
1429
|
-
* radius`); a single O(n) two-pointer sweep maintains the window.
|
|
1438
|
+
* radius`); a single O(n) two-pointer sweep maintains the window.
|
|
1439
|
+
*
|
|
1440
|
+
* Pass `{ radius, at }` — a **non-decreasing** array of explicit center values
|
|
1441
|
+
* (e.g. a chart's coarse display grid) — to evaluate at those centers instead
|
|
1442
|
+
* of at every row, returning **one record per center** (a center with no rows
|
|
1443
|
+
* in range yields each reducer's empty value). See
|
|
1430
1444
|
* `docs/notes/rolling-by-column.md`.
|
|
1431
1445
|
*/
|
|
1432
1446
|
rollingByColumn(col, spec, mapping) {
|
|
@@ -2204,6 +2218,17 @@ export class TimeSeries {
|
|
|
2204
2218
|
* When `output` is omitted, the smoothed values replace the target column. When `output` is
|
|
2205
2219
|
* supplied, the smoothed values are appended as a new optional numeric column.
|
|
2206
2220
|
*
|
|
2221
|
+
* **Missing cells** (`movingAverage` / `loess`): by default (`missing:
|
|
2222
|
+
* 'bridge'`) a cell whose own value is missing is still assigned a smoothed
|
|
2223
|
+
* value computed from its present neighbours — i.e. the line is drawn *across*
|
|
2224
|
+
* the hole. Pass `missing: 'skip'` to keep a missing cell **missing** in the
|
|
2225
|
+
* output, so a sustained dropout (a coast, a sensor gap) is preserved as a
|
|
2226
|
+
* break rather than fabricated through. Present cells already smooth over only
|
|
2227
|
+
* the present values in their window either way. `ema` is causal and never
|
|
2228
|
+
* fabricates across a gap, so it takes no `missing` option. (estela
|
|
2229
|
+
* F-smooth-interactive; a `maxGap` hard segment boundary is a deferred
|
|
2230
|
+
* follow-on.)
|
|
2231
|
+
*
|
|
2207
2232
|
* **Multi-entity series:** the smoothing window pulls values from
|
|
2208
2233
|
* every entity into each smoothed point — `host-A`'s smoothed value
|
|
2209
2234
|
* is blended with `host-B`'s and `host-C`'s. On a series carrying
|
|
@@ -2290,8 +2315,14 @@ export class TimeSeries {
|
|
|
2290
2315
|
loessValues.push(value);
|
|
2291
2316
|
}
|
|
2292
2317
|
}
|
|
2318
|
+
// `missing: 'skip'` — a cell whose own value is missing stays missing
|
|
2319
|
+
// (don't fit a fabricated value across the hole from its present
|
|
2320
|
+
// neighbours). Default `'bridge'` is the prior behaviour (fit everywhere).
|
|
2321
|
+
const skipMissing = options.missing === 'skip';
|
|
2293
2322
|
const resultRows = this.events.map((event, index) => {
|
|
2294
|
-
const smoothed =
|
|
2323
|
+
const smoothed = skipMissing && sourceValues[index] === undefined
|
|
2324
|
+
? undefined
|
|
2325
|
+
: loessAt(anchors[index], loessAnchors, loessValues, span);
|
|
2295
2326
|
const nextEvent = output === undefined
|
|
2296
2327
|
? event.set(column, smoothed)
|
|
2297
2328
|
: event.merge({ [output]: smoothed });
|
|
@@ -2314,6 +2345,7 @@ export class TimeSeries {
|
|
|
2314
2345
|
const window = options.window;
|
|
2315
2346
|
const windowMs = parseDuration(window);
|
|
2316
2347
|
const alignment = options.alignment ?? 'trailing';
|
|
2348
|
+
const skipMissing = options.missing === 'skip';
|
|
2317
2349
|
const resultValues = new Array(this.events.length);
|
|
2318
2350
|
let windowStart = 0;
|
|
2319
2351
|
let windowEnd = 0;
|
|
@@ -2380,7 +2412,12 @@ export class TimeSeries {
|
|
|
2380
2412
|
}
|
|
2381
2413
|
const smoothed = snapshot();
|
|
2382
2414
|
for (let index = groupStart; index < groupEnd; index++) {
|
|
2383
|
-
|
|
2415
|
+
// `missing: 'skip'` — a cell whose own value is missing stays missing
|
|
2416
|
+
// (don't fabricate it from the window average). Default `'bridge'` fills.
|
|
2417
|
+
resultValues[index] =
|
|
2418
|
+
skipMissing && sourceValues[index] === undefined
|
|
2419
|
+
? undefined
|
|
2420
|
+
: smoothed;
|
|
2384
2421
|
}
|
|
2385
2422
|
groupStart = groupEnd;
|
|
2386
2423
|
}
|
package/dist/reducers/index.js
CHANGED
|
@@ -33,6 +33,11 @@ export function resolveReducer(operation) {
|
|
|
33
33
|
const r = registry[operation];
|
|
34
34
|
if (r)
|
|
35
35
|
return r;
|
|
36
|
+
// `'mean'` is an accepted alias for `'avg'` in aggregate/rolling mappings,
|
|
37
|
+
// matching the column API (`Float64Column.mean()` → the `'avg'` kernel). Keeps
|
|
38
|
+
// the registry free of a duplicate entry. (estela F-reducer-naming.)
|
|
39
|
+
if (operation === 'mean')
|
|
40
|
+
return avg;
|
|
36
41
|
const q = parsePercentile(operation);
|
|
37
42
|
if (q !== undefined)
|
|
38
43
|
return percentileReducer(q);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppendColumn, ArrayColumnNameForSchema, ColumnDef, ColumnValue, OptionalizeColumns, ReplaceColumnKind, ScalarKind, SeriesSchema, ValueColumn, ValueColumnsForSchema } from './series.js';
|
|
2
|
-
export type AggregateFunction = 'sum' | 'avg' | 'min' | 'max' | 'count' | 'first' | 'last' | 'median' | 'stdev' | 'difference' | 'keep' | 'unique' | 'samples' | `p${number}` | `top${number}`;
|
|
2
|
+
export type AggregateFunction = 'sum' | 'avg' | 'mean' | 'min' | 'max' | 'count' | 'first' | 'last' | 'median' | 'stdev' | 'difference' | 'keep' | 'unique' | 'samples' | `p${number}` | `top${number}`;
|
|
3
3
|
/**
|
|
4
4
|
* Custom aggregate reducers receive every value in a bucket (including
|
|
5
5
|
* `undefined`) and return a single result. The return type is widened to
|
|
@@ -152,7 +152,7 @@ type UnifiedOutputKind<Columns extends readonly ValueColumn[], K extends string,
|
|
|
152
152
|
* reducer function (or otherwise unrecognized) — the spec branch passes
|
|
153
153
|
* `ScalarKind`, the shorthand branch passes the source column's kind.
|
|
154
154
|
*/
|
|
155
|
-
type ReducerOutputKind<Columns extends readonly ValueColumn[], From extends string, Using, Fallback extends ScalarKind> = Using extends 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? From extends Columns[number]['name'] ? ColumnKindByName<Columns, From> : ScalarKind : Fallback;
|
|
155
|
+
type ReducerOutputKind<Columns extends readonly ValueColumn[], From extends string, Using, Fallback extends ScalarKind> = Using extends 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? From extends Columns[number]['name'] ? ColumnKindByName<Columns, From> : ScalarKind : Fallback;
|
|
156
156
|
/**
|
|
157
157
|
* Union of typed `ColumnDef`s — one per **output key** in the mapping
|
|
158
158
|
* (not per source column). Used as the `...Rest` of the schema tuple;
|
|
@@ -209,7 +209,7 @@ export type AggregateOutputMapResultSchema<S extends SeriesSchema, Mapping> = Ag
|
|
|
209
209
|
* Aggregate functions that always produce a numeric result regardless of
|
|
210
210
|
* source column kind. Matches the reducer registry's `outputKind: 'number'`.
|
|
211
211
|
*/
|
|
212
|
-
type NumericAggregateFunction = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}`;
|
|
212
|
+
type NumericAggregateFunction = 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}`;
|
|
213
213
|
/**
|
|
214
214
|
* Output column kind for `arrayAggregate(col, reducer, { kind? })`.
|
|
215
215
|
* Numeric reducers → `'number'`, `'unique'` → `'array'`, `'first'`/`'last'`/
|
package/dist/schema/events.d.ts
CHANGED
|
@@ -20,7 +20,9 @@ export type TimeSeriesInput<S extends SeriesSchema> = {
|
|
|
20
20
|
sort?: boolean;
|
|
21
21
|
};
|
|
22
22
|
export type NormalizedRowForSchema<S extends readonly ColumnDef<string, string>[]> = {
|
|
23
|
-
[I in keyof S]: S[I] extends ColumnDef<any, infer K> ? NormalizedValueForKind<K> :
|
|
23
|
+
[I in keyof S]: S[I] extends ColumnDef<any, infer K> ? I extends '0' ? NormalizedValueForKind<K> : S[I] extends {
|
|
24
|
+
required: false;
|
|
25
|
+
} ? NormalizedValueForKind<K> | undefined : NormalizedValueForKind<K> : never;
|
|
24
26
|
};
|
|
25
27
|
type DataValueForColumn<C extends ColumnDef<string, string>> = C extends ColumnDef<any, infer K> ? C['required'] extends false ? NormalizedValueForKind<K> | undefined : NormalizedValueForKind<K> : never;
|
|
26
28
|
type NormalizedDataValueForColumn<C extends ColumnDef<string, string>> = C extends ColumnDef<any, infer K> ? K extends FirstColKind ? EventKeyForKind<K> : C['required'] extends false ? NormalizedValueForKind<K> | undefined : NormalizedValueForKind<K> : never;
|
package/dist/schema/reduce.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ export type ReduceResult<S extends SeriesSchema, Mapping> = {
|
|
|
73
73
|
* and shorthand branches. `From` is the spec's `from` or the shorthand
|
|
74
74
|
* key; `Using` is the reducer.
|
|
75
75
|
*/
|
|
76
|
-
type ReduceValueForReducer<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? number | undefined : Using extends 'unique' | 'samples' | `top${number}` ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? ReadonlyArray<number> | undefined : ColumnByName<S, From>['kind'] extends 'string' ? ReadonlyArray<string> | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? ReadonlyArray<boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : Using extends 'first' | 'last' | 'keep' ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? number | undefined : ColumnByName<S, From>['kind'] extends 'string' ? string | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? boolean | undefined : ColumnValue | undefined : ColumnValue | undefined : ColumnValue | undefined;
|
|
76
|
+
type ReduceValueForReducer<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? number | undefined : Using extends 'unique' | 'samples' | `top${number}` ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? ReadonlyArray<number> | undefined : ColumnByName<S, From>['kind'] extends 'string' ? ReadonlyArray<string> | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? ReadonlyArray<boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : ReadonlyArray<string | number | boolean> | undefined : Using extends 'first' | 'last' | 'keep' ? From extends ValueColumnsForSchema<S>[number]['name'] ? ColumnByName<S, From>['kind'] extends 'number' ? number | undefined : ColumnByName<S, From>['kind'] extends 'string' ? string | undefined : ColumnByName<S, From>['kind'] extends 'boolean' ? boolean | undefined : ColumnValue | undefined : ColumnValue | undefined : ColumnValue | undefined;
|
|
77
77
|
/**
|
|
78
78
|
* Value type for an explicit `kind` on a reduce spec. Mirrors
|
|
79
79
|
* `NormalizedValueForKind` but stays inline here for the same TS2394
|
package/dist/schema/rolling.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ type FusedColumnKind<S extends SeriesSchema, Key extends string, Value> = Value
|
|
|
90
90
|
* Reducer-string-to-output-kind dispatch, shared by both AggregateMap
|
|
91
91
|
* and AggregateOutputMap entry shapes.
|
|
92
92
|
*/
|
|
93
|
-
type FusedReducerKind<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? FromColumnKind<S, From> : ScalarKind;
|
|
93
|
+
type FusedReducerKind<S extends SeriesSchema, From extends string, Using> = Using extends 'sum' | 'avg' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'stdev' | 'difference' | `p${number}` ? 'number' : Using extends 'unique' | 'samples' | `top${number}` ? 'array' : Using extends 'first' | 'last' | 'keep' ? FromColumnKind<S, From> : ScalarKind;
|
|
94
94
|
/**
|
|
95
95
|
* Look up the kind of a value column by name. Used by source-
|
|
96
96
|
* preserving reducers (`first` / `last` / `keep`) where the output
|
package/dist/schema/series.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export type ColumnDef<Name extends string, Kind extends string> = {
|
|
|
22
22
|
kind: Kind;
|
|
23
23
|
required?: boolean;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* The key (first) column of a schema. Its **name must equal its kind** —
|
|
27
|
+
* `time` / `timeRange` / `interval`. So `{ name: 'time', kind: 'time' }` is the
|
|
28
|
+
* only valid time key; `{ name: 'at', kind: 'time' }` does **not** typecheck
|
|
29
|
+
* (the error surfaces as a name/literal-type mismatch, e.g. `'"at"' is not
|
|
30
|
+
* assignable to '"time"'` — read it as "the key column must be named for its
|
|
31
|
+
* kind", not as a value error). Value columns, by contrast, take any name.
|
|
32
|
+
*/
|
|
25
33
|
export type FirstColumn = ColumnDef<'time', 'time'> | ColumnDef<'interval', 'interval'> | ColumnDef<'timeRange', 'timeRange'>;
|
|
26
34
|
export type ValueColumn<Name extends string = string> = ColumnDef<Name, ScalarKind>;
|
|
27
35
|
export type SeriesSchema = readonly [FirstColumn, ...ValueColumn[]];
|
|
@@ -32,8 +40,18 @@ export type ValueColumnsForSchema<S extends SeriesSchema> = S extends readonly [
|
|
|
32
40
|
export type ValueForKind<K extends string> = K extends 'time' ? TimestampInput | Time : K extends 'interval' ? IntervalInput | Interval : K extends 'timeRange' ? TimeRangeInput | TimeRange : K extends 'number' ? number : K extends 'string' ? string : K extends 'boolean' ? boolean : K extends 'array' ? ArrayValue : never;
|
|
33
41
|
export type NormalizedValueForKind<K extends string> = K extends 'time' ? Time : K extends 'timeRange' ? TimeRange : K extends 'interval' ? Interval : K extends 'number' ? number : K extends 'string' ? string : K extends 'boolean' ? boolean : K extends 'array' ? ArrayValue : never;
|
|
34
42
|
export type KindForValue<V extends ScalarValue> = V extends number ? 'number' : V extends string ? 'string' : 'boolean';
|
|
43
|
+
/**
|
|
44
|
+
* Tuple-row input type for a schema. A column declared `required: false`
|
|
45
|
+
* accepts `undefined` in its cell (a missing value — the constructor records
|
|
46
|
+
* it in the validity bitmap), matching the runtime's intake. `null` is **not**
|
|
47
|
+
* admitted: the tuple-row constructor rejects it (only the JSON object-row path
|
|
48
|
+
* accepts `null`), so the type stays honest to what intake actually takes —
|
|
49
|
+
* pass `undefined` for a missing tuple cell. (estela F-geo-row-optional.)
|
|
50
|
+
*/
|
|
35
51
|
export type RowForSchema<S extends readonly ColumnDef<string, string>[]> = {
|
|
36
|
-
[I in keyof S]: S[I] extends ColumnDef<any, infer K> ? ValueForKind<K> :
|
|
52
|
+
[I in keyof S]: S[I] extends ColumnDef<any, infer K> ? I extends '0' ? ValueForKind<K> : S[I] extends {
|
|
53
|
+
required: false;
|
|
54
|
+
} ? ValueForKind<K> | undefined : ValueForKind<K> : never;
|
|
37
55
|
};
|
|
38
56
|
export type NumericColumnNameForSchema<S extends SeriesSchema> = Extract<ValueColumnsForSchema<S>[number], ColumnDef<string, 'number'>>['name'];
|
|
39
57
|
/**
|