pond-ts 0.29.0 → 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 +96 -6
- package/dist/batch/by-column.js +12 -8
- package/dist/batch/operators/by-value.d.ts +19 -0
- package/dist/batch/operators/by-value.js +91 -0
- package/dist/batch/operators/scan.d.ts +47 -0
- package/dist/batch/operators/scan.js +81 -0
- package/dist/batch/partitioned-time-series.d.ts +7 -1
- package/dist/batch/partitioned-time-series.js +3 -0
- 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 +114 -5
- package/dist/batch/time-series.js +107 -6
- package/dist/batch/value-series.d.ts +53 -0
- package/dist/batch/value-series.js +105 -0
- package/dist/columnar/index.d.ts +1 -1
- package/dist/columnar/index.js +1 -1
- package/dist/columnar/key-column.d.ts +36 -2
- package/dist/columnar/key-column.js +89 -0
- package/dist/columnar/types.d.ts +8 -2
- package/dist/columnar/view.js +7 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/live/series-store.js +19 -11
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/series.d.ts +19 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
|
|
7
|
+
`@pond-ts/fit` — release together under a single `v*` tag, so this file covers
|
|
8
|
+
them all. Pre-1.0: minor bumps may include new features and type-level changes;
|
|
9
|
+
patch bumps are strictly additive.
|
|
10
|
+
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.31.0...HEAD
|
|
12
|
+
[0.31.0]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...v0.31.0
|
|
13
|
+
[0.30.0]: https://github.com/pjm17971/pond-ts/compare/v0.29.0...v0.30.0
|
|
11
14
|
[0.29.0]: https://github.com/pjm17971/pond-ts/compare/v0.28.0...v0.29.0
|
|
12
15
|
[0.28.0]: https://github.com/pjm17971/pond-ts/compare/v0.27.0...v0.28.0
|
|
13
16
|
[0.27.0]: https://github.com/pjm17971/pond-ts/compare/v0.26.0...v0.27.0
|
|
@@ -21,7 +24,94 @@ type-level changes; patch bumps are strictly additive.
|
|
|
21
24
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
22
25
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
23
26
|
|
|
24
|
-
## [
|
|
27
|
+
## [0.31.0] — 2026-06-28
|
|
28
|
+
|
|
29
|
+
First published release of **`@pond-ts/charts`** and **`@pond-ts/fit`** (both were
|
|
30
|
+
previously `private`). All four packages — `pond-ts`, `@pond-ts/react`,
|
|
31
|
+
`@pond-ts/charts`, `@pond-ts/fit` — now release together, lock-step, under one
|
|
32
|
+
`v*` tag.
|
|
33
|
+
|
|
34
|
+
### Added — `pond-ts` (core)
|
|
35
|
+
|
|
36
|
+
- **`ValueSeries` + `TimeSeries.byValue(axis)` — the value axis as a closed
|
|
37
|
+
type.** `byValue` re-keys a series onto a monotonic non-time **value axis**
|
|
38
|
+
(distance, cumulative work, …), returning a `ValueSeries` — the value-keyed
|
|
39
|
+
counterpart of `TimeSeries`. It carries the ordering-based operators
|
|
40
|
+
(`axisValues`, `axisAt`, `column`, `nearestIndex`, `sliceByValue`); the
|
|
41
|
+
calendar/clock operators are deliberately absent — a value axis has no
|
|
42
|
+
wall-clock semantics, and the disjoint `ValueSeriesSchema` makes them
|
|
43
|
+
type-impossible. The axis must be **defined, finite, and non-decreasing at
|
|
44
|
+
every row** (it becomes the index); it is dropped from the value columns (it
|
|
45
|
+
is now the key) and the rest reshare zero-copy. Substrate: a new `'value'`
|
|
46
|
+
`KeyKind` + `ValueKeyColumn`. Projection is O(N + C); `nearestIndex` is
|
|
47
|
+
O(log N); `sliceByValue` is O(log N + C) zero-copy. (value-axis RFC Phase 1.)
|
|
48
|
+
- **`scan(source, step, init, options?)` — typed-accumulator running fold.** The
|
|
49
|
+
general form of `cumulative` (the classic `mapAccumL`): the accumulator `A`
|
|
50
|
+
(any value, seeded from `init`) is **decoupled** from the numeric `output` and
|
|
51
|
+
the output column. `step(acc, value, i)` returns `[nextAcc, output]`. With no
|
|
52
|
+
`options.output` the source column is **replaced** in place (as `cumulative`
|
|
53
|
+
does); with `options.output` a **new** column is appended and the source is
|
|
54
|
+
left intact. Missing-cell carry, stored-`NaN`, and multi-entity semantics are
|
|
55
|
+
inherited from `cumulative` (scope per entity with
|
|
56
|
+
`partitionBy(col).scan(...).collect()`). Column-native, O(N + C), no event
|
|
57
|
+
materialization. Enables `split = scan + byColumn` — materialize cross-bin
|
|
58
|
+
state (e.g. hysteresis elevation gain) into a column, then segment it with
|
|
59
|
+
`byColumn`'s pure, order-free reducers. (estela F-geo-2-splits; value-axis
|
|
60
|
+
RFC wave lead.)
|
|
61
|
+
|
|
62
|
+
### Added — `@pond-ts/charts` (initial release)
|
|
63
|
+
|
|
64
|
+
- **First public release.** A React charting layer over pond-ts — a canvas data
|
|
65
|
+
plane with SVG interactive overlays. `ChartContainer` / `ChartRow` / `Layers`
|
|
66
|
+
composition; `LineChart`, `AreaChart`, `BarChart`, `Scatter`, `BoxPlot`;
|
|
67
|
+
`TimeAxis` / `YAxis` / `XAxis` (time **and** value x-axes); the cursor system
|
|
68
|
+
(staffed flag, per-row cursor modes); shared gap-rendering modes; and the
|
|
69
|
+
estela theme. Peer-depends on `pond-ts`, `@pond-ts/react`, and React 18/19.
|
|
70
|
+
|
|
71
|
+
### Added — `@pond-ts/fit` (initial release)
|
|
72
|
+
|
|
73
|
+
- **First public release.** A fitness / activity domain library over pond-ts — the
|
|
74
|
+
`Activity` / `Section` façade, unit-safe quantities (`Distance` / `Speed` /
|
|
75
|
+
`Power` / … with `.format()`), geo / power / zones analytics, `Profile` +
|
|
76
|
+
`usingProfile()` → `ProfiledActivity` / `ProfiledSection`, and the `Track`
|
|
77
|
+
value object. Façade-first: one curated flat barrel, with the functional
|
|
78
|
+
operator surface kept internal. Peer-depends on `pond-ts`.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- **All `@pond-ts/*` peer / dependency ranges widened to `^0.31.0`** for the
|
|
83
|
+
lock-step release.
|
|
84
|
+
|
|
85
|
+
## [0.30.0] — 2026-06-17
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **`rollingByColumn(col, { radius, at }, mapping)` — evaluate at explicit
|
|
90
|
+
centers.** `at` takes a **non-decreasing** array of center values (e.g. a chart's
|
|
91
|
+
coarse display grid) and returns **one record per center**, instead of the
|
|
92
|
+
default one-per-row. A center with no rows within `±radius` yields each
|
|
93
|
+
reducer's empty value. Same O(n + centers) two-pointer. Closes the
|
|
94
|
+
evaluate-at-grid gap surfaced adopting `rollingByColumn` for a chart variance
|
|
95
|
+
band. (estela F-rolling-by-row.)
|
|
96
|
+
- **`smooth(col, 'movingAverage' | 'loess', { …, missing: 'skip' })` —
|
|
97
|
+
validity-respecting smoothing.** By default (`missing: 'bridge'`) a cell whose
|
|
98
|
+
own value is missing is still assigned a smoothed value from its present
|
|
99
|
+
neighbours — the line is drawn _across_ the hole. `missing: 'skip'` keeps a
|
|
100
|
+
missing cell **missing** in the output, so a sustained dropout (a coast, a
|
|
101
|
+
sensor gap) is preserved as a break rather than fabricated through. Present
|
|
102
|
+
cells smooth over only the present values in their window either way. `ema`
|
|
103
|
+
takes no `missing` option (it is causal and never fabricates across a gap). A
|
|
104
|
+
`maxGap` hard segment boundary is a deferred follow-on. (estela
|
|
105
|
+
F-smooth-interactive.)
|
|
106
|
+
|
|
107
|
+
### Changed
|
|
108
|
+
|
|
109
|
+
- **`byColumn(…, { inclusive: '(]' })` floor edge is now inclusive.** Under
|
|
110
|
+
`'(]'`, interior bins stay upper-inclusive (`(eᵢ, eᵢ₊₁]`) but the **floor `e₀`
|
|
111
|
+
is inclusive** (bin 0 is `[e₀, e₁]`), so a value at exactly the minimum edge —
|
|
112
|
+
e.g. a `0` W coast/stop sample at a zone floor of 0 — lands in bin 0 instead of
|
|
113
|
+
being dropped (the `include_lowest` convention). Previously the floor was
|
|
114
|
+
exclusive. (estela F-inclusive-floor.)
|
|
25
115
|
|
|
26
116
|
## [0.29.0] — 2026-06-17
|
|
27
117
|
|
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) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ColumnarStore, type ColumnSchema } from '../../columnar/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* **Column-native `byValue`** — the raw `TimeSeries → ValueSeries` projection
|
|
4
|
+
* (RFC `value-axis.md` §6). Re-keys the store onto the monotonic `axis` column
|
|
5
|
+
* (a no-op reindex: the rows already sit in axis order, since the axis is
|
|
6
|
+
* non-decreasing in storage order) and **drops `axis` from the value columns**
|
|
7
|
+
* — it is now the key, and `fromTrustedStore` rejects the duplicate name
|
|
8
|
+
* otherwise. The non-axis value columns + their buffers are shared by reference
|
|
9
|
+
* (zero-copy); only the key column is newly allocated.
|
|
10
|
+
*
|
|
11
|
+
* Returns the reshaped store + the value-keyed output schema. The schema cast
|
|
12
|
+
* is the trust boundary; `TimeSeries.byValue` wraps the store in a
|
|
13
|
+
* `ValueSeries` with the precise `ValueKeyedSchema<S, Axis>` type.
|
|
14
|
+
*/
|
|
15
|
+
export declare function byValueOp(store: ColumnarStore<ColumnSchema>, schema: ColumnSchema, axis: string): {
|
|
16
|
+
store: ColumnarStore<ColumnSchema>;
|
|
17
|
+
schema: ColumnSchema;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=by-value.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ColumnarStore, Float64Column, ValueKeyColumn, } from '../../columnar/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates that the `axis` column is a usable value axis — **every cell
|
|
4
|
+
* defined + finite + non-decreasing** — and returns the `Float64Array` to key
|
|
5
|
+
* on. Throws otherwise.
|
|
6
|
+
*
|
|
7
|
+
* This is the monotonicity contract for `byValue`: it lives on the *projection*,
|
|
8
|
+
* not on `ValueKeyColumn` or `byColumn` (Codex review #1). An order-free
|
|
9
|
+
* value-bin aggregation has no monotonic precondition, but promoting a column to
|
|
10
|
+
* the *index* of a series does — and a missing/non-finite cell can't be placed
|
|
11
|
+
* in the ordering, so (unlike a value column, where a gap is fine) the axis must
|
|
12
|
+
* be dense.
|
|
13
|
+
*
|
|
14
|
+
* **Zero-copy fast path (Lever 1).** A packed {@link Float64Column} (every batch
|
|
15
|
+
* value column is one) already holds a contiguous backing buffer. Once the scan
|
|
16
|
+
* has proven `[0, n)` is dense + finite, that buffer *is* the key data, so we
|
|
17
|
+
* hand back a `subarray(0, n)` view rather than allocating and copying a fresh
|
|
18
|
+
* array — the source axis column and the new key then share it read-only (the
|
|
19
|
+
* same zero-copy contract as a slice). `_values.length` can exceed the logical
|
|
20
|
+
* length (capacity-grown columns), hence the `subarray`. A chunked column has no
|
|
21
|
+
* single contiguous buffer, so it falls back to materializing one.
|
|
22
|
+
*
|
|
23
|
+
* Trade-off of the reuse: the returned `subarray` view retains the source
|
|
24
|
+
* column's whole `ArrayBuffer` (including any capacity slack on a grown
|
|
25
|
+
* column), where the old copy released it. Negligible for batch (`_values`
|
|
26
|
+
* is sized to the column) and for a single live projection; only worth
|
|
27
|
+
* revisiting if a path holds many such views at once.
|
|
28
|
+
*
|
|
29
|
+
* The validation read-loop is unavoidable here (it enforces dense + finite +
|
|
30
|
+
* sorted). A future `{ assumeSorted }` fast path could skip it for a
|
|
31
|
+
* caller-guaranteed axis (e.g. a `scan`-produced cumulative distance); that's a
|
|
32
|
+
* trusted-construction seam, deferred until a re-projection hot path earns it.
|
|
33
|
+
*/
|
|
34
|
+
function assertMonotonicAxis(store, axis) {
|
|
35
|
+
const col = store.columns.get(axis);
|
|
36
|
+
if (col === undefined) {
|
|
37
|
+
throw new RangeError(`byValue: unknown column '${axis}'`);
|
|
38
|
+
}
|
|
39
|
+
const n = store.length;
|
|
40
|
+
let prev = -Infinity;
|
|
41
|
+
for (let i = 0; i < n; i += 1) {
|
|
42
|
+
const v = col.read(i);
|
|
43
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
44
|
+
throw new RangeError(`byValue: axis '${axis}' must be defined and finite at every row to be the index; ` +
|
|
45
|
+
`row ${i} is ${v === undefined ? 'missing' : String(v)}`);
|
|
46
|
+
}
|
|
47
|
+
if (v < prev) {
|
|
48
|
+
throw new RangeError(`byValue: axis '${axis}' must be non-decreasing; row ${i} (${v}) < previous (${prev})`);
|
|
49
|
+
}
|
|
50
|
+
prev = v;
|
|
51
|
+
}
|
|
52
|
+
// Validation passed → `[0, n)` is dense + finite. Reuse the packed backing
|
|
53
|
+
// buffer zero-copy; materialize only for a (rare) chunked axis column.
|
|
54
|
+
if (col instanceof Float64Column) {
|
|
55
|
+
return col._values.subarray(0, n);
|
|
56
|
+
}
|
|
57
|
+
const out = new Float64Array(n);
|
|
58
|
+
for (let i = 0; i < n; i += 1) {
|
|
59
|
+
out[i] = col.read(i);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* **Column-native `byValue`** — the raw `TimeSeries → ValueSeries` projection
|
|
65
|
+
* (RFC `value-axis.md` §6). Re-keys the store onto the monotonic `axis` column
|
|
66
|
+
* (a no-op reindex: the rows already sit in axis order, since the axis is
|
|
67
|
+
* non-decreasing in storage order) and **drops `axis` from the value columns**
|
|
68
|
+
* — it is now the key, and `fromTrustedStore` rejects the duplicate name
|
|
69
|
+
* otherwise. The non-axis value columns + their buffers are shared by reference
|
|
70
|
+
* (zero-copy); only the key column is newly allocated.
|
|
71
|
+
*
|
|
72
|
+
* Returns the reshaped store + the value-keyed output schema. The schema cast
|
|
73
|
+
* is the trust boundary; `TimeSeries.byValue` wraps the store in a
|
|
74
|
+
* `ValueSeries` with the precise `ValueKeyedSchema<S, Axis>` type.
|
|
75
|
+
*/
|
|
76
|
+
export function byValueOp(store, schema, axis) {
|
|
77
|
+
const values = assertMonotonicAxis(store, axis);
|
|
78
|
+
// `fromValidatedSubarray`, not `new ValueKeyColumn` — `assertMonotonicAxis`
|
|
79
|
+
// already proved finiteness, so skip the constructor's redundant finite scan.
|
|
80
|
+
const keyCol = ValueKeyColumn.fromValidatedSubarray(values, store.length);
|
|
81
|
+
// Drop the axis column from the value columns (it becomes the key).
|
|
82
|
+
const newColumns = new Map(store.columns);
|
|
83
|
+
newColumns.delete(axis);
|
|
84
|
+
const newSchema = [
|
|
85
|
+
{ name: axis, kind: 'value' },
|
|
86
|
+
...schema.slice(1).filter((c) => c.name !== axis),
|
|
87
|
+
];
|
|
88
|
+
const newStore = ColumnarStore.fromTrustedStore(newSchema, keyCol, newColumns);
|
|
89
|
+
return { store: newStore, schema: newSchema };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=by-value.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ColumnarStore } from '../../columnar/index.js';
|
|
2
|
+
import type { SeriesSchema } from '../../schema/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The step function for {@link TimeSeries.scan} — the classic `mapAccumL`:
|
|
5
|
+
* given the carried accumulator, the current (defined) source value, and the
|
|
6
|
+
* row index, return the next accumulator and this row's numeric output. The
|
|
7
|
+
* accumulator type `A` is **decoupled** from the numeric output, which is what
|
|
8
|
+
* `cumulative` — where the accumulator *is* the output *is* a `number` — cannot
|
|
9
|
+
* express (e.g. hysteresis elevation gain carries `(ref, gain)` but emits only
|
|
10
|
+
* `gain`).
|
|
11
|
+
*/
|
|
12
|
+
export type ScanStep<A> = (acc: A, value: number, index: number) => readonly [next: A, output: number];
|
|
13
|
+
/**
|
|
14
|
+
* **Column-native `scan`** — a typed-accumulator running fold, the
|
|
15
|
+
* generalization of `cumulativeOp`. Threads `acc: A` (any value, seeded from
|
|
16
|
+
* `init`) across the source column's defined cells, emits one numeric `output`
|
|
17
|
+
* per row, and either **replaces** the source column (`output === undefined`,
|
|
18
|
+
* `cumulative`'s convention) or **appends** a new column (`output` named,
|
|
19
|
+
* `withColumn`'s convention; the source stays intact). Reads straight off
|
|
20
|
+
* `Column.read(i)` — no event materialization, one pass, **O(n)**.
|
|
21
|
+
*
|
|
22
|
+
* Semantics are inherited from `cumulativeOp` so `scan` is consistent, not a
|
|
23
|
+
* new dialect:
|
|
24
|
+
* - A **defined numeric cell** calls `step(acc, value, i)`; the accumulator and
|
|
25
|
+
* the last-emitted output both advance.
|
|
26
|
+
* - A **missing / undefined cell** does *not* call `step`: the accumulator is
|
|
27
|
+
* carried unchanged and the row re-emits the last output, so the output holds
|
|
28
|
+
* flat across a gap (exactly as `cumulative`'s accumulator holds). The output
|
|
29
|
+
* is `undefined` only until the first defined value produces one
|
|
30
|
+
* (`float64ColumnFromArray` derives validity from the `undefined`s).
|
|
31
|
+
* - A **stored `NaN`** is a defined number — `step` is called with it, and a
|
|
32
|
+
* computed non-finite output lands as a defined cell. This is the
|
|
33
|
+
* *trusted-compute* path (matching `cumulative`), not the validated
|
|
34
|
+
* `withColumn` intake; the step author owns output finiteness.
|
|
35
|
+
*
|
|
36
|
+
* Returns the reshaped store + the output schema. The result-schema cast is the
|
|
37
|
+
* single trust boundary; `TimeSeries.scan` wraps the store via
|
|
38
|
+
* `#fromTrustedStore`. A non-numeric source is unreachable through the typed
|
|
39
|
+
* surface (`scan<Source extends NumericColumnNameForSchema<S>>`); on the
|
|
40
|
+
* replace path it fails fast (`withColumnReplaced`'s kind guard), matching
|
|
41
|
+
* `cumulative`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function scanOp<S extends SeriesSchema, OutSchema extends SeriesSchema, A>(store: ColumnarStore<S>, schema: S, source: string, step: ScanStep<A>, init: A, output: string | undefined): {
|
|
44
|
+
store: ColumnarStore<OutSchema>;
|
|
45
|
+
schema: OutSchema;
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { float64ColumnFromArray, withColumnAppended, withColumnReplaced, } from '../../columnar/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* **Column-native `scan`** — a typed-accumulator running fold, the
|
|
4
|
+
* generalization of `cumulativeOp`. Threads `acc: A` (any value, seeded from
|
|
5
|
+
* `init`) across the source column's defined cells, emits one numeric `output`
|
|
6
|
+
* per row, and either **replaces** the source column (`output === undefined`,
|
|
7
|
+
* `cumulative`'s convention) or **appends** a new column (`output` named,
|
|
8
|
+
* `withColumn`'s convention; the source stays intact). Reads straight off
|
|
9
|
+
* `Column.read(i)` — no event materialization, one pass, **O(n)**.
|
|
10
|
+
*
|
|
11
|
+
* Semantics are inherited from `cumulativeOp` so `scan` is consistent, not a
|
|
12
|
+
* new dialect:
|
|
13
|
+
* - A **defined numeric cell** calls `step(acc, value, i)`; the accumulator and
|
|
14
|
+
* the last-emitted output both advance.
|
|
15
|
+
* - A **missing / undefined cell** does *not* call `step`: the accumulator is
|
|
16
|
+
* carried unchanged and the row re-emits the last output, so the output holds
|
|
17
|
+
* flat across a gap (exactly as `cumulative`'s accumulator holds). The output
|
|
18
|
+
* is `undefined` only until the first defined value produces one
|
|
19
|
+
* (`float64ColumnFromArray` derives validity from the `undefined`s).
|
|
20
|
+
* - A **stored `NaN`** is a defined number — `step` is called with it, and a
|
|
21
|
+
* computed non-finite output lands as a defined cell. This is the
|
|
22
|
+
* *trusted-compute* path (matching `cumulative`), not the validated
|
|
23
|
+
* `withColumn` intake; the step author owns output finiteness.
|
|
24
|
+
*
|
|
25
|
+
* Returns the reshaped store + the output schema. The result-schema cast is the
|
|
26
|
+
* single trust boundary; `TimeSeries.scan` wraps the store via
|
|
27
|
+
* `#fromTrustedStore`. A non-numeric source is unreachable through the typed
|
|
28
|
+
* surface (`scan<Source extends NumericColumnNameForSchema<S>>`); on the
|
|
29
|
+
* replace path it fails fast (`withColumnReplaced`'s kind guard), matching
|
|
30
|
+
* `cumulative`.
|
|
31
|
+
*/
|
|
32
|
+
export function scanOp(store, schema, source, step, init, output) {
|
|
33
|
+
const col = store.columns.get(source);
|
|
34
|
+
if (col === undefined) {
|
|
35
|
+
throw new RangeError(`scan: unknown column '${source}'`);
|
|
36
|
+
}
|
|
37
|
+
const n = store.length;
|
|
38
|
+
const out = new Array(n);
|
|
39
|
+
let acc = init;
|
|
40
|
+
let last;
|
|
41
|
+
for (let i = 0; i < n; i += 1) {
|
|
42
|
+
const raw = col.read(i);
|
|
43
|
+
if (typeof raw === 'number') {
|
|
44
|
+
const r = step(acc, raw, i);
|
|
45
|
+
acc = r[0];
|
|
46
|
+
last = r[1];
|
|
47
|
+
}
|
|
48
|
+
out[i] = last;
|
|
49
|
+
}
|
|
50
|
+
const column = float64ColumnFromArray(out);
|
|
51
|
+
const base = store;
|
|
52
|
+
// Output omitted ⇒ replace the source column in place (cumulative's
|
|
53
|
+
// convention; the source is widened to optional `number`).
|
|
54
|
+
if (output === undefined) {
|
|
55
|
+
const result = withColumnReplaced(base, source, column);
|
|
56
|
+
const outSchema = Object.freeze(schema.map((c, i) => i === 0 || c.name !== source
|
|
57
|
+
? c
|
|
58
|
+
: { ...c, kind: 'number', required: false }));
|
|
59
|
+
return {
|
|
60
|
+
store: result,
|
|
61
|
+
schema: outSchema,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Output named ⇒ append a new column, leaving the source intact. The name
|
|
65
|
+
// must not collide with the key or an existing value column (the schema
|
|
66
|
+
// includes the key at index 0). `withColumnAppended` re-checks, but the
|
|
67
|
+
// explicit guard gives a scan-specific message pointing at the replace path.
|
|
68
|
+
if (schema.some((c) => c.name === output)) {
|
|
69
|
+
throw new RangeError(`scan: output column '${output}' already exists; omit options.output to replace the source column`);
|
|
70
|
+
}
|
|
71
|
+
const result = withColumnAppended(base, output, column);
|
|
72
|
+
const outSchema = Object.freeze([
|
|
73
|
+
...schema,
|
|
74
|
+
{ name: output, kind: 'number' },
|
|
75
|
+
]);
|
|
76
|
+
return {
|
|
77
|
+
store: result,
|
|
78
|
+
schema: outSchema,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -4,7 +4,8 @@ import { Sequence } from '../sequence/sequence.js';
|
|
|
4
4
|
import type { DurationInput } from '../core/duration.js';
|
|
5
5
|
import type { TemporalLike } from '../core/temporal.js';
|
|
6
6
|
import type { BatchSampleStrategy } from '../sequence/sample.js';
|
|
7
|
-
import type { AggregateSchema, AlignSchema, BaselineSchema, DedupeKeep, DiffSchema, EventDataForSchema, FillMapping, FillStrategy, MaterializeSchema, NumericColumnNameForSchema, RollingAlignment, RollingSchema, SeriesSchema, SmoothAppendSchema, SmoothMethod, SmoothSchema, ValidatedAggregateMap } from '../schema/index.js';
|
|
7
|
+
import type { AggregateSchema, AlignSchema, AppendColumn, BaselineSchema, DedupeKeep, DiffSchema, EventDataForSchema, FillMapping, FillStrategy, MaterializeSchema, NumericColumnNameForSchema, RollingAlignment, RollingSchema, SeriesSchema, SmoothAppendSchema, SmoothMethod, SmoothSchema, ValidatedAggregateMap } from '../schema/index.js';
|
|
8
|
+
import type { ScanStep } from './operators/scan.js';
|
|
8
9
|
type SequenceLike = Sequence | BoundedSequence;
|
|
9
10
|
type AlignMethod = 'hold' | 'linear';
|
|
10
11
|
type AlignSample = 'begin' | 'center' | 'end';
|
|
@@ -303,6 +304,11 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
|
|
|
303
304
|
cumulative<const Targets extends NumericColumnNameForSchema<S>>(spec: {
|
|
304
305
|
[K in Targets]: 'sum' | 'max' | 'min' | 'count' | ((acc: number, value: number) => number);
|
|
305
306
|
}): PartitionedTimeSeries<DiffSchema<S, Targets>, K>;
|
|
307
|
+
/** Per-partition `scan`. See {@link TimeSeries.scan}. */
|
|
308
|
+
scan<const Source extends NumericColumnNameForSchema<S>, A>(source: Source, step: ScanStep<A>, init: A): PartitionedTimeSeries<DiffSchema<S, Source>, K>;
|
|
309
|
+
scan<const Source extends NumericColumnNameForSchema<S>, const Name extends string, A>(source: Source, step: ScanStep<A>, init: A, options: {
|
|
310
|
+
output: Name;
|
|
311
|
+
}): PartitionedTimeSeries<AppendColumn<S, Name, 'number'>, K>;
|
|
306
312
|
/** Per-partition `shift`. See {@link TimeSeries.shift}. */
|
|
307
313
|
shift<const Target extends NumericColumnNameForSchema<S>>(columns: Target | readonly Target[], n: number): PartitionedTimeSeries<DiffSchema<S, Target>, K>;
|
|
308
314
|
/** Per-partition `aggregate`. See {@link TimeSeries.aggregate}. */
|
|
@@ -419,6 +419,9 @@ export class PartitionedTimeSeries {
|
|
|
419
419
|
cumulative(spec) {
|
|
420
420
|
return this.rewrap(PartitionedTimeSeries.applyToSource(this.source, this.by, (g) => g.cumulative(spec)));
|
|
421
421
|
}
|
|
422
|
+
scan(source, step, init, options) {
|
|
423
|
+
return this.rewrap(PartitionedTimeSeries.applyToSource(this.source, this.by, (g) => g.scan(source, step, init, options)));
|
|
424
|
+
}
|
|
422
425
|
/** Per-partition `shift`. See {@link TimeSeries.shift}. */
|
|
423
426
|
shift(columns, n) {
|
|
424
427
|
return this.rewrap(PartitionedTimeSeries.applyToSource(this.source, this.by, (g) => g.shift(columns, n)));
|
|
@@ -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)
|