pond-ts 0.29.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
CHANGED
|
@@ -7,7 +7,8 @@ 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
|
|
11
12
|
[0.29.0]: https://github.com/pjm17971/pond-ts/compare/v0.28.0...v0.29.0
|
|
12
13
|
[0.28.0]: https://github.com/pjm17971/pond-ts/compare/v0.27.0...v0.28.0
|
|
13
14
|
[0.27.0]: https://github.com/pjm17971/pond-ts/compare/v0.26.0...v0.27.0
|
|
@@ -21,7 +22,36 @@ type-level changes; patch bumps are strictly additive.
|
|
|
21
22
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
22
23
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
23
24
|
|
|
24
|
-
## [
|
|
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.)
|
|
25
55
|
|
|
26
56
|
## [0.29.0] — 2026-06-17
|
|
27
57
|
|
package/dist/batch/by-column.js
CHANGED
|
@@ -47,17 +47,21 @@ export function computeByColumn(store, binColName, spec, columnSpecs) {
|
|
|
47
47
|
const last = edges.length - 1;
|
|
48
48
|
// `inclusive` picks the bin a value exactly on an interior edge falls into.
|
|
49
49
|
// `'[)'` (default): bins are `[eᵢ, eᵢ₊₁)`, lower-inclusive — a boundary value
|
|
50
|
-
// goes to the bin ABOVE; range is `[e₀, eₙ)`. `'(]'`: bins are
|
|
51
|
-
// upper-inclusive (Coggan power/HR zones — a sample at exactly
|
|
52
|
-
// edge is the LOWER zone) — a boundary value goes to the bin
|
|
53
|
-
// `
|
|
54
|
-
// minimum
|
|
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ₙ]`.
|
|
55
57
|
binOf =
|
|
56
58
|
spec.inclusive === '(]'
|
|
57
59
|
? (v) => {
|
|
58
|
-
if (v
|
|
59
|
-
return NaN; // out of
|
|
60
|
-
// rightmost edge STRICTLY less than 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).
|
|
61
65
|
let lo = 0;
|
|
62
66
|
let hi = last;
|
|
63
67
|
while (lo < hi) {
|
|
@@ -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)
|
|
@@ -541,9 +541,11 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
541
541
|
* yields a histogram (distribution).
|
|
542
542
|
* - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
|
|
543
543
|
* `inclusive` defaults to `'[)'` (bin `i` = `[eᵢ, eᵢ₊₁)`, lower-inclusive);
|
|
544
|
-
* pass `'(]'` for upper-inclusive bins (`(eᵢ, eᵢ₊₁]`) — Coggan power
|
|
545
|
-
* zones, where a sample exactly on a zone's top edge belongs to the
|
|
546
|
-
* zone
|
|
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.
|
|
547
549
|
*
|
|
548
550
|
* A row whose bin value is missing / non-finite (or, for `edges`, outside
|
|
549
551
|
* `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
|
|
@@ -577,7 +579,12 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
577
579
|
* reducer non-finite policy still applies to the *source* columns.
|
|
578
580
|
*
|
|
579
581
|
* The window is centered and inclusive (`col[i] − radius ≤ col[j] ≤ col[i] +
|
|
580
|
-
* 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
|
|
581
588
|
* `docs/notes/rolling-by-column.md`.
|
|
582
589
|
*/
|
|
583
590
|
rollingByColumn<const Mapping extends ValidatedAggregateMap<S, Mapping>>(col: NumericColumnNameForSchema<S>, spec: WindowSpec, mapping: Mapping): Array<ReduceResult<S, Mapping>>;
|
|
@@ -998,6 +1005,17 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
998
1005
|
* When `output` is omitted, the smoothed values replace the target column. When `output` is
|
|
999
1006
|
* supplied, the smoothed values are appended as a new optional numeric column.
|
|
1000
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
|
+
*
|
|
1001
1019
|
* **Multi-entity series:** the smoothing window pulls values from
|
|
1002
1020
|
* every entity into each smoothed point — `host-A`'s smoothed value
|
|
1003
1021
|
* is blended with `host-B`'s and `host-C`'s. On a series carrying
|
|
@@ -1013,9 +1031,11 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1013
1031
|
window: DurationInput;
|
|
1014
1032
|
alignment?: RollingAlignment;
|
|
1015
1033
|
output?: Output;
|
|
1034
|
+
missing?: 'skip' | 'bridge';
|
|
1016
1035
|
} | {
|
|
1017
1036
|
span: number;
|
|
1018
1037
|
output?: Output;
|
|
1038
|
+
missing?: 'skip' | 'bridge';
|
|
1019
1039
|
}): TimeSeries<Output extends string ? SmoothAppendSchema<S, Output> : SmoothSchema<S, Target>>;
|
|
1020
1040
|
/** Example: `series.slice(0, 10)`. Returns a positional half-open slice of the series. */
|
|
1021
1041
|
slice(beginIndex?: number, endIndex?: number): TimeSeries<S>;
|
|
@@ -1397,9 +1397,11 @@ export class TimeSeries {
|
|
|
1397
1397
|
* yields a histogram (distribution).
|
|
1398
1398
|
* - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
|
|
1399
1399
|
* `inclusive` defaults to `'[)'` (bin `i` = `[eᵢ, eᵢ₊₁)`, lower-inclusive);
|
|
1400
|
-
* pass `'(]'` for upper-inclusive bins (`(eᵢ, eᵢ₊₁]`) — Coggan power
|
|
1401
|
-
* zones, where a sample exactly on a zone's top edge belongs to the
|
|
1402
|
-
* zone
|
|
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.
|
|
1403
1405
|
*
|
|
1404
1406
|
* A row whose bin value is missing / non-finite (or, for `edges`, outside
|
|
1405
1407
|
* `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
|
|
@@ -1433,7 +1435,12 @@ export class TimeSeries {
|
|
|
1433
1435
|
* reducer non-finite policy still applies to the *source* columns.
|
|
1434
1436
|
*
|
|
1435
1437
|
* The window is centered and inclusive (`col[i] − radius ≤ col[j] ≤ col[i] +
|
|
1436
|
-
* 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
|
|
1437
1444
|
* `docs/notes/rolling-by-column.md`.
|
|
1438
1445
|
*/
|
|
1439
1446
|
rollingByColumn(col, spec, mapping) {
|
|
@@ -2211,6 +2218,17 @@ export class TimeSeries {
|
|
|
2211
2218
|
* When `output` is omitted, the smoothed values replace the target column. When `output` is
|
|
2212
2219
|
* supplied, the smoothed values are appended as a new optional numeric column.
|
|
2213
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
|
+
*
|
|
2214
2232
|
* **Multi-entity series:** the smoothing window pulls values from
|
|
2215
2233
|
* every entity into each smoothed point — `host-A`'s smoothed value
|
|
2216
2234
|
* is blended with `host-B`'s and `host-C`'s. On a series carrying
|
|
@@ -2297,8 +2315,14 @@ export class TimeSeries {
|
|
|
2297
2315
|
loessValues.push(value);
|
|
2298
2316
|
}
|
|
2299
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';
|
|
2300
2322
|
const resultRows = this.events.map((event, index) => {
|
|
2301
|
-
const smoothed =
|
|
2323
|
+
const smoothed = skipMissing && sourceValues[index] === undefined
|
|
2324
|
+
? undefined
|
|
2325
|
+
: loessAt(anchors[index], loessAnchors, loessValues, span);
|
|
2302
2326
|
const nextEvent = output === undefined
|
|
2303
2327
|
? event.set(column, smoothed)
|
|
2304
2328
|
: event.merge({ [output]: smoothed });
|
|
@@ -2321,6 +2345,7 @@ export class TimeSeries {
|
|
|
2321
2345
|
const window = options.window;
|
|
2322
2346
|
const windowMs = parseDuration(window);
|
|
2323
2347
|
const alignment = options.alignment ?? 'trailing';
|
|
2348
|
+
const skipMissing = options.missing === 'skip';
|
|
2324
2349
|
const resultValues = new Array(this.events.length);
|
|
2325
2350
|
let windowStart = 0;
|
|
2326
2351
|
let windowEnd = 0;
|
|
@@ -2387,7 +2412,12 @@ export class TimeSeries {
|
|
|
2387
2412
|
}
|
|
2388
2413
|
const smoothed = snapshot();
|
|
2389
2414
|
for (let index = groupStart; index < groupEnd; index++) {
|
|
2390
|
-
|
|
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;
|
|
2391
2421
|
}
|
|
2392
2422
|
groupStart = groupEnd;
|
|
2393
2423
|
}
|