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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateSchema, AppendColumn, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap, ValidatedAggregateMap } from '../schema/index.js';
|
|
2
|
-
import type { RenameSchema, RollingAlignment, RollingSchema, ColumnValue, DedupeKeep, DiffSchema, FillMapping, FillStrategy, ScalarKind, ScalarValue, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesJsonInput, TimeSeriesInput, TimeRangeKeyedSchema, ValueColumnKindForName, ValueColumnNameForSchema, ValueColumnsForSchema } from '../schema/index.js';
|
|
2
|
+
import type { RenameSchema, RollingAlignment, RollingSchema, ColumnValue, DedupeKeep, DiffSchema, FillMapping, FillStrategy, ScalarKind, ScalarValue, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesJsonInput, TimeSeriesInput, TimeRangeKeyedSchema, ValueColumnKindForName, ValueColumnNameForSchema, ValueColumnsForSchema, ValueKeyedSchema } from '../schema/index.js';
|
|
3
|
+
import { type ScanStep } from './operators/scan.js';
|
|
4
|
+
import { ValueSeries } from './value-series.js';
|
|
3
5
|
import { type BinSpec } from './by-column.js';
|
|
4
6
|
import { type WindowSpec } from './rolling-by-column.js';
|
|
5
7
|
import { BoundedSequence } from '../sequence/bounded-sequence.js';
|
|
@@ -521,6 +523,29 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
521
523
|
*/
|
|
522
524
|
reduce(column: ValueColumnsForSchema<S>[number]['name'], reducer: AggregateReducer): ColumnValue | undefined;
|
|
523
525
|
reduce<const Mapping extends ValidatedAggregateMap<S, Mapping>>(mapping: Mapping): ReduceResult<S, Mapping>;
|
|
526
|
+
/**
|
|
527
|
+
* Example: `track.byValue('cumDist')`.
|
|
528
|
+
*
|
|
529
|
+
* **The raw `TimeSeries → ValueSeries` projection** (RFC `value-axis.md` §6):
|
|
530
|
+
* re-key the series onto a monotonic numeric **value axis** (distance,
|
|
531
|
+
* cumulative work, …), returning a {@link ValueSeries} that carries the
|
|
532
|
+
* ordering-based operators (axis read, nearest-by-value, slice-by-value) over
|
|
533
|
+
* that axis instead of time.
|
|
534
|
+
*
|
|
535
|
+
* `axis` must be **defined, finite, and non-decreasing at every row** — it
|
|
536
|
+
* becomes the index, so (unlike a value column) it cannot have gaps; an
|
|
537
|
+
* `assertMonotonicAxis` check throws otherwise. This monotonicity contract
|
|
538
|
+
* lives on the *projection*, not on {@link TimeSeries.byColumn} (whose
|
|
539
|
+
* order-free binning has no such precondition). The re-key is a no-op reindex
|
|
540
|
+
* (the rows already sit in axis order) and the axis column is **dropped from
|
|
541
|
+
* the value columns** (it is now the key); the other columns are shared
|
|
542
|
+
* zero-copy.
|
|
543
|
+
*
|
|
544
|
+
* `byValue` is the projection; `byColumn` is value-axis *aggregation* — pair
|
|
545
|
+
* them via {@link TimeSeries.scan} for stateful splits
|
|
546
|
+
* (`split = scan + byColumn`).
|
|
547
|
+
*/
|
|
548
|
+
byValue<const Axis extends NumericColumnNameForSchema<S>>(axis: Axis): Axis extends Axis ? ValueSeries<ValueKeyedSchema<S, Axis>> : never;
|
|
524
549
|
/**
|
|
525
550
|
* Example:
|
|
526
551
|
* `series.byColumn('cumDist', { width: 1000 }, { gain: { from: 'ele', using: 'sum' } })`.
|
|
@@ -541,9 +566,11 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
541
566
|
* yields a histogram (distribution).
|
|
542
567
|
* - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
|
|
543
568
|
* `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
|
|
569
|
+
* pass `'(]'` for upper-inclusive interior bins (`(eᵢ, eᵢ₊₁]`) — Coggan power
|
|
570
|
+
* / HR zones, where a sample exactly on a zone's top edge belongs to the
|
|
571
|
+
* lower zone. The floor `e₀` stays **inclusive** (bin 0 is `[e₀, e₁]`), so a
|
|
572
|
+
* minimum-edge value (e.g. a `0` W coast sample) lands in bin 0 rather than
|
|
573
|
+
* being dropped. Always emits all `n` bins.
|
|
547
574
|
*
|
|
548
575
|
* A row whose bin value is missing / non-finite (or, for `edges`, outside
|
|
549
576
|
* `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
|
|
@@ -577,7 +604,12 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
577
604
|
* reducer non-finite policy still applies to the *source* columns.
|
|
578
605
|
*
|
|
579
606
|
* 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.
|
|
607
|
+
* radius`); a single O(n) two-pointer sweep maintains the window.
|
|
608
|
+
*
|
|
609
|
+
* Pass `{ radius, at }` — a **non-decreasing** array of explicit center values
|
|
610
|
+
* (e.g. a chart's coarse display grid) — to evaluate at those centers instead
|
|
611
|
+
* of at every row, returning **one record per center** (a center with no rows
|
|
612
|
+
* in range yields each reducer's empty value). See
|
|
581
613
|
* `docs/notes/rolling-by-column.md`.
|
|
582
614
|
*/
|
|
583
615
|
rollingByColumn<const Mapping extends ValidatedAggregateMap<S, Mapping>>(col: NumericColumnNameForSchema<S>, spec: WindowSpec, mapping: Mapping): Array<ReduceResult<S, Mapping>>;
|
|
@@ -785,6 +817,56 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
785
817
|
cumulative<const Targets extends NumericColumnNameForSchema<S>>(spec: {
|
|
786
818
|
[K in Targets]: 'sum' | 'max' | 'min' | 'count' | ((acc: number, value: number) => number);
|
|
787
819
|
}): TimeSeries<DiffSchema<S, Targets>>;
|
|
820
|
+
/**
|
|
821
|
+
* Example:
|
|
822
|
+
* ```ts
|
|
823
|
+
* // running sum — the cumulative special case, replacing in place:
|
|
824
|
+
* series.scan('work', (acc, v) => [acc + v, acc + v], 0);
|
|
825
|
+
*
|
|
826
|
+
* // typed accumulator into a NEW column (hysteresis elevation gain):
|
|
827
|
+
* track.scan<'cumGain', { ref: number | null; gain: number }>(
|
|
828
|
+
* 'ele',
|
|
829
|
+
* (acc, ele) => {
|
|
830
|
+
* if (acc.ref === null) return [{ ref: ele, gain: 0 }, 0];
|
|
831
|
+
* const d = ele - acc.ref;
|
|
832
|
+
* if (d >= 3) return [{ ref: ele, gain: acc.gain + d }, acc.gain + d];
|
|
833
|
+
* if (d <= -3) return [{ ref: ele, gain: acc.gain }, acc.gain];
|
|
834
|
+
* return [acc, acc.gain]; // within deadband — carry
|
|
835
|
+
* },
|
|
836
|
+
* { ref: null, gain: 0 },
|
|
837
|
+
* { output: 'cumGain' },
|
|
838
|
+
* );
|
|
839
|
+
* ```
|
|
840
|
+
*
|
|
841
|
+
* **Typed-accumulator running fold** (the classic `mapAccumL`) over a numeric
|
|
842
|
+
* column. `step(acc, value, i)` returns `[nextAcc, output]`; the accumulator
|
|
843
|
+
* `A` (inferred from `init`) is **decoupled** from the numeric `output` — the
|
|
844
|
+
* generalization {@link TimeSeries.cumulative} can't express, since there the
|
|
845
|
+
* accumulator *is* the output *is* a `number`. `cumulative` is the scalar
|
|
846
|
+
* special case: `series.cumulative({ x: 'sum' })` is
|
|
847
|
+
* `series.scan('x', (a, v) => [a + v, a + v], 0)`.
|
|
848
|
+
*
|
|
849
|
+
* With no `options.output` the source column is **replaced** in place (widened
|
|
850
|
+
* to optional `number`, as `cumulative` does). With `options.output` a **new**
|
|
851
|
+
* column of that name is appended and the source is left intact (as
|
|
852
|
+
* {@link TimeSeries.withColumn} does); the name must not already exist.
|
|
853
|
+
*
|
|
854
|
+
* **Missing cells carry:** a missing / undefined source cell does not call
|
|
855
|
+
* `step` — the accumulator is held and the row re-emits the last output (so it
|
|
856
|
+
* holds flat across a gap), `undefined` only until the first defined value. A
|
|
857
|
+
* stored `NaN` is a defined number and is passed to `step`; the step author
|
|
858
|
+
* owns output finiteness (this is the trusted-compute path, not the validated
|
|
859
|
+
* `withColumn` intake).
|
|
860
|
+
*
|
|
861
|
+
* **Multi-entity series:** the accumulator threads across all rows in storage
|
|
862
|
+
* order, so it interleaves across entities — `host-A`'s next row folds on top
|
|
863
|
+
* of `host-B`'s. Use `series.partitionBy(col).scan(...).collect()` to scope
|
|
864
|
+
* per entity. See {@link TimeSeries.partitionBy}.
|
|
865
|
+
*/
|
|
866
|
+
scan<const Source extends NumericColumnNameForSchema<S>, A>(source: Source, step: ScanStep<A>, init: A): TimeSeries<DiffSchema<S, Source>>;
|
|
867
|
+
scan<const Source extends NumericColumnNameForSchema<S>, const Name extends string, A>(source: Source, step: ScanStep<A>, init: A, options: {
|
|
868
|
+
output: Name;
|
|
869
|
+
}): TimeSeries<AppendColumn<S, Name, 'number'>>;
|
|
788
870
|
/**
|
|
789
871
|
* Example: `series.shift("value", 1)`.
|
|
790
872
|
* Lags column values by N events (positive N) or leads them (negative N).
|
|
@@ -998,6 +1080,17 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
998
1080
|
* When `output` is omitted, the smoothed values replace the target column. When `output` is
|
|
999
1081
|
* supplied, the smoothed values are appended as a new optional numeric column.
|
|
1000
1082
|
*
|
|
1083
|
+
* **Missing cells** (`movingAverage` / `loess`): by default (`missing:
|
|
1084
|
+
* 'bridge'`) a cell whose own value is missing is still assigned a smoothed
|
|
1085
|
+
* value computed from its present neighbours — i.e. the line is drawn *across*
|
|
1086
|
+
* the hole. Pass `missing: 'skip'` to keep a missing cell **missing** in the
|
|
1087
|
+
* output, so a sustained dropout (a coast, a sensor gap) is preserved as a
|
|
1088
|
+
* break rather than fabricated through. Present cells already smooth over only
|
|
1089
|
+
* the present values in their window either way. `ema` is causal and never
|
|
1090
|
+
* fabricates across a gap, so it takes no `missing` option. (estela
|
|
1091
|
+
* F-smooth-interactive; a `maxGap` hard segment boundary is a deferred
|
|
1092
|
+
* follow-on.)
|
|
1093
|
+
*
|
|
1001
1094
|
* **Multi-entity series:** the smoothing window pulls values from
|
|
1002
1095
|
* every entity into each smoothed point — `host-A`'s smoothed value
|
|
1003
1096
|
* is blended with `host-B`'s and `host-C`'s. On a series carrying
|
|
@@ -1013,9 +1106,11 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1013
1106
|
window: DurationInput;
|
|
1014
1107
|
alignment?: RollingAlignment;
|
|
1015
1108
|
output?: Output;
|
|
1109
|
+
missing?: 'skip' | 'bridge';
|
|
1016
1110
|
} | {
|
|
1017
1111
|
span: number;
|
|
1018
1112
|
output?: Output;
|
|
1113
|
+
missing?: 'skip' | 'bridge';
|
|
1019
1114
|
}): TimeSeries<Output extends string ? SmoothAppendSchema<S, Output> : SmoothSchema<S, Target>>;
|
|
1020
1115
|
/** Example: `series.slice(0, 10)`. Returns a positional half-open slice of the series. */
|
|
1021
1116
|
slice(beginIndex?: number, endIndex?: number): TimeSeries<S>;
|
|
@@ -1069,6 +1164,20 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
1069
1164
|
atOrBefore(key: KeyLike): EventForSchema<S> | undefined;
|
|
1070
1165
|
/** Example: `series.atOrAfter(new Time(Date.now()))`. Returns the event with the exact key or the nearest later event, if any. */
|
|
1071
1166
|
atOrAfter(key: KeyLike): EventForSchema<S> | undefined;
|
|
1167
|
+
/**
|
|
1168
|
+
* Example: `series.nearest(new Time(Date.now()))`. Returns the event whose key
|
|
1169
|
+
* is **closest** to `key` by `begin()` distance, or `undefined` only when the
|
|
1170
|
+
* series is empty.
|
|
1171
|
+
*
|
|
1172
|
+
* Where `atOrBefore` / `atOrAfter` bound to one side, this rounds to the nearer
|
|
1173
|
+
* neighbour (ties go to the earlier event). A key outside the series resolves
|
|
1174
|
+
* to the first or last event — the nearest that exists — so callers that want
|
|
1175
|
+
* "no match past the data" should range-check against {@link timeRange}.
|
|
1176
|
+
*
|
|
1177
|
+
* O(log N) via `bisect`; the columnar key buffer is probed by `begin()`, with
|
|
1178
|
+
* no Event allocation beyond the single result.
|
|
1179
|
+
*/
|
|
1180
|
+
nearest(key: KeyLike): EventForSchema<S> | undefined;
|
|
1072
1181
|
/** Example: `series.timeRange()`. Returns the overall temporal extent of the series, if the series is not empty. */
|
|
1073
1182
|
timeRange(): TimeRange | undefined;
|
|
1074
1183
|
/** Example: `series.overlaps(range)`. Returns `true` when the overall series extent overlaps the supplied temporal value. */
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { isAggregateOutputSpec, normalizeAggregateColumns, tryAggregateColumnarTimeKeyed, } from './aggregate-columns.js';
|
|
2
2
|
import { cumulativeOp, } from './operators/cumulative.js';
|
|
3
|
+
import { scanOp } from './operators/scan.js';
|
|
4
|
+
import { byValueOp } from './operators/by-value.js';
|
|
5
|
+
import { ValueSeries } from './value-series.js';
|
|
3
6
|
import { diffRateOp } from './operators/diff-rate.js';
|
|
4
7
|
import { fillOp } from './operators/fill.js';
|
|
5
8
|
import { mapOp } from './operators/map.js';
|
|
@@ -1377,6 +1380,39 @@ export class TimeSeries {
|
|
|
1377
1380
|
}
|
|
1378
1381
|
return result;
|
|
1379
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Example: `track.byValue('cumDist')`.
|
|
1385
|
+
*
|
|
1386
|
+
* **The raw `TimeSeries → ValueSeries` projection** (RFC `value-axis.md` §6):
|
|
1387
|
+
* re-key the series onto a monotonic numeric **value axis** (distance,
|
|
1388
|
+
* cumulative work, …), returning a {@link ValueSeries} that carries the
|
|
1389
|
+
* ordering-based operators (axis read, nearest-by-value, slice-by-value) over
|
|
1390
|
+
* that axis instead of time.
|
|
1391
|
+
*
|
|
1392
|
+
* `axis` must be **defined, finite, and non-decreasing at every row** — it
|
|
1393
|
+
* becomes the index, so (unlike a value column) it cannot have gaps; an
|
|
1394
|
+
* `assertMonotonicAxis` check throws otherwise. This monotonicity contract
|
|
1395
|
+
* lives on the *projection*, not on {@link TimeSeries.byColumn} (whose
|
|
1396
|
+
* order-free binning has no such precondition). The re-key is a no-op reindex
|
|
1397
|
+
* (the rows already sit in axis order) and the axis column is **dropped from
|
|
1398
|
+
* the value columns** (it is now the key); the other columns are shared
|
|
1399
|
+
* zero-copy.
|
|
1400
|
+
*
|
|
1401
|
+
* `byValue` is the projection; `byColumn` is value-axis *aggregation* — pair
|
|
1402
|
+
* them via {@link TimeSeries.scan} for stateful splits
|
|
1403
|
+
* (`split = scan + byColumn`).
|
|
1404
|
+
*/
|
|
1405
|
+
byValue(axis) {
|
|
1406
|
+
// The return type is **distributive** over `Axis` (`Axis extends Axis ?`):
|
|
1407
|
+
// for a literal axis it is just `ValueSeries<ValueKeyedSchema<S, Axis>>`,
|
|
1408
|
+
// but for a union axis (e.g. a generic wrapper's `'cumDist' | 'hr'`) it
|
|
1409
|
+
// becomes the discriminated union `ValueSeries<…cumDist> | ValueSeries<…hr>`
|
|
1410
|
+
// — each branch drops only its own axis, so narrowing on `axisName` recovers
|
|
1411
|
+
// the right `column()` names. Without distribution, `ValueKeyedSchema` would
|
|
1412
|
+
// drop *every* union member from the value columns.
|
|
1413
|
+
const { store, schema } = byValueOp(this.#store.store, this.schema, axis);
|
|
1414
|
+
return ValueSeries.fromTrustedStore(this.name, schema, store);
|
|
1415
|
+
}
|
|
1380
1416
|
/**
|
|
1381
1417
|
* Example:
|
|
1382
1418
|
* `series.byColumn('cumDist', { width: 1000 }, { gain: { from: 'ele', using: 'sum' } })`.
|
|
@@ -1397,9 +1433,11 @@ export class TimeSeries {
|
|
|
1397
1433
|
* yields a histogram (distribution).
|
|
1398
1434
|
* - `{ edges, inclusive? }` — explicit ascending edges `[e₀ … eₙ]` → `n` bins.
|
|
1399
1435
|
* `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
|
|
1436
|
+
* pass `'(]'` for upper-inclusive interior bins (`(eᵢ, eᵢ₊₁]`) — Coggan power
|
|
1437
|
+
* / HR zones, where a sample exactly on a zone's top edge belongs to the
|
|
1438
|
+
* lower zone. The floor `e₀` stays **inclusive** (bin 0 is `[e₀, e₁]`), so a
|
|
1439
|
+
* minimum-edge value (e.g. a `0` W coast sample) lands in bin 0 rather than
|
|
1440
|
+
* being dropped. Always emits all `n` bins.
|
|
1403
1441
|
*
|
|
1404
1442
|
* A row whose bin value is missing / non-finite (or, for `edges`, outside
|
|
1405
1443
|
* `[e₀, eₙ)`) contributes to no bin. The reducer non-finite policy still
|
|
@@ -1433,7 +1471,12 @@ export class TimeSeries {
|
|
|
1433
1471
|
* reducer non-finite policy still applies to the *source* columns.
|
|
1434
1472
|
*
|
|
1435
1473
|
* 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.
|
|
1474
|
+
* radius`); a single O(n) two-pointer sweep maintains the window.
|
|
1475
|
+
*
|
|
1476
|
+
* Pass `{ radius, at }` — a **non-decreasing** array of explicit center values
|
|
1477
|
+
* (e.g. a chart's coarse display grid) — to evaluate at those centers instead
|
|
1478
|
+
* of at every row, returning **one record per center** (a center with no rows
|
|
1479
|
+
* in range yields each reducer's empty value). See
|
|
1437
1480
|
* `docs/notes/rolling-by-column.md`.
|
|
1438
1481
|
*/
|
|
1439
1482
|
rollingByColumn(col, spec, mapping) {
|
|
@@ -1673,6 +1716,13 @@ export class TimeSeries {
|
|
|
1673
1716
|
const { store, schema } = cumulativeOp(this.#store.store, this.schema, spec);
|
|
1674
1717
|
return TimeSeries.#fromTrustedStore(this.name, schema, store);
|
|
1675
1718
|
}
|
|
1719
|
+
scan(source, step, init, options) {
|
|
1720
|
+
// Column-native: the typed-accumulator fold runs straight off the store's
|
|
1721
|
+
// source column in the extracted `scanOp` — no `this.events`
|
|
1722
|
+
// materialization. The method is a thin delegate.
|
|
1723
|
+
const { store, schema } = scanOp(this.#store.store, this.schema, source, step, init, options?.output);
|
|
1724
|
+
return TimeSeries.#fromTrustedStore(this.name, schema, store);
|
|
1725
|
+
}
|
|
1676
1726
|
/**
|
|
1677
1727
|
* Example: `series.shift("value", 1)`.
|
|
1678
1728
|
* Lags column values by N events (positive N) or leads them (negative N).
|
|
@@ -2211,6 +2261,17 @@ export class TimeSeries {
|
|
|
2211
2261
|
* When `output` is omitted, the smoothed values replace the target column. When `output` is
|
|
2212
2262
|
* supplied, the smoothed values are appended as a new optional numeric column.
|
|
2213
2263
|
*
|
|
2264
|
+
* **Missing cells** (`movingAverage` / `loess`): by default (`missing:
|
|
2265
|
+
* 'bridge'`) a cell whose own value is missing is still assigned a smoothed
|
|
2266
|
+
* value computed from its present neighbours — i.e. the line is drawn *across*
|
|
2267
|
+
* the hole. Pass `missing: 'skip'` to keep a missing cell **missing** in the
|
|
2268
|
+
* output, so a sustained dropout (a coast, a sensor gap) is preserved as a
|
|
2269
|
+
* break rather than fabricated through. Present cells already smooth over only
|
|
2270
|
+
* the present values in their window either way. `ema` is causal and never
|
|
2271
|
+
* fabricates across a gap, so it takes no `missing` option. (estela
|
|
2272
|
+
* F-smooth-interactive; a `maxGap` hard segment boundary is a deferred
|
|
2273
|
+
* follow-on.)
|
|
2274
|
+
*
|
|
2214
2275
|
* **Multi-entity series:** the smoothing window pulls values from
|
|
2215
2276
|
* every entity into each smoothed point — `host-A`'s smoothed value
|
|
2216
2277
|
* is blended with `host-B`'s and `host-C`'s. On a series carrying
|
|
@@ -2297,8 +2358,14 @@ export class TimeSeries {
|
|
|
2297
2358
|
loessValues.push(value);
|
|
2298
2359
|
}
|
|
2299
2360
|
}
|
|
2361
|
+
// `missing: 'skip'` — a cell whose own value is missing stays missing
|
|
2362
|
+
// (don't fit a fabricated value across the hole from its present
|
|
2363
|
+
// neighbours). Default `'bridge'` is the prior behaviour (fit everywhere).
|
|
2364
|
+
const skipMissing = options.missing === 'skip';
|
|
2300
2365
|
const resultRows = this.events.map((event, index) => {
|
|
2301
|
-
const smoothed =
|
|
2366
|
+
const smoothed = skipMissing && sourceValues[index] === undefined
|
|
2367
|
+
? undefined
|
|
2368
|
+
: loessAt(anchors[index], loessAnchors, loessValues, span);
|
|
2302
2369
|
const nextEvent = output === undefined
|
|
2303
2370
|
? event.set(column, smoothed)
|
|
2304
2371
|
: event.merge({ [output]: smoothed });
|
|
@@ -2321,6 +2388,7 @@ export class TimeSeries {
|
|
|
2321
2388
|
const window = options.window;
|
|
2322
2389
|
const windowMs = parseDuration(window);
|
|
2323
2390
|
const alignment = options.alignment ?? 'trailing';
|
|
2391
|
+
const skipMissing = options.missing === 'skip';
|
|
2324
2392
|
const resultValues = new Array(this.events.length);
|
|
2325
2393
|
let windowStart = 0;
|
|
2326
2394
|
let windowEnd = 0;
|
|
@@ -2387,7 +2455,12 @@ export class TimeSeries {
|
|
|
2387
2455
|
}
|
|
2388
2456
|
const smoothed = snapshot();
|
|
2389
2457
|
for (let index = groupStart; index < groupEnd; index++) {
|
|
2390
|
-
|
|
2458
|
+
// `missing: 'skip'` — a cell whose own value is missing stays missing
|
|
2459
|
+
// (don't fabricate it from the window average). Default `'bridge'` fills.
|
|
2460
|
+
resultValues[index] =
|
|
2461
|
+
skipMissing && sourceValues[index] === undefined
|
|
2462
|
+
? undefined
|
|
2463
|
+
: smoothed;
|
|
2391
2464
|
}
|
|
2392
2465
|
groupStart = groupEnd;
|
|
2393
2466
|
}
|
|
@@ -2573,6 +2646,34 @@ export class TimeSeries {
|
|
|
2573
2646
|
atOrAfter(key) {
|
|
2574
2647
|
return this.at(this.bisect(key));
|
|
2575
2648
|
}
|
|
2649
|
+
/**
|
|
2650
|
+
* Example: `series.nearest(new Time(Date.now()))`. Returns the event whose key
|
|
2651
|
+
* is **closest** to `key` by `begin()` distance, or `undefined` only when the
|
|
2652
|
+
* series is empty.
|
|
2653
|
+
*
|
|
2654
|
+
* Where `atOrBefore` / `atOrAfter` bound to one side, this rounds to the nearer
|
|
2655
|
+
* neighbour (ties go to the earlier event). A key outside the series resolves
|
|
2656
|
+
* to the first or last event — the nearest that exists — so callers that want
|
|
2657
|
+
* "no match past the data" should range-check against {@link timeRange}.
|
|
2658
|
+
*
|
|
2659
|
+
* O(log N) via `bisect`; the columnar key buffer is probed by `begin()`, with
|
|
2660
|
+
* no Event allocation beyond the single result.
|
|
2661
|
+
*/
|
|
2662
|
+
nearest(key) {
|
|
2663
|
+
const n = this.#store.length;
|
|
2664
|
+
if (n === 0)
|
|
2665
|
+
return undefined;
|
|
2666
|
+
const normalizedKey = toKey(key);
|
|
2667
|
+
const index = this.bisect(normalizedKey); // first index with keyAt(i) >= key
|
|
2668
|
+
if (index <= 0)
|
|
2669
|
+
return this.at(0);
|
|
2670
|
+
if (index >= n)
|
|
2671
|
+
return this.at(n - 1);
|
|
2672
|
+
const target = normalizedKey.begin();
|
|
2673
|
+
const before = this.#store.keyAt(index - 1).begin();
|
|
2674
|
+
const after = this.#store.keyAt(index).begin();
|
|
2675
|
+
return this.at(target - before <= after - target ? index - 1 : index);
|
|
2676
|
+
}
|
|
2576
2677
|
/** Example: `series.timeRange()`. Returns the overall temporal extent of the series, if the series is not empty. */
|
|
2577
2678
|
timeRange() {
|
|
2578
2679
|
// Columnar key-axis read. The old implementation reduced over
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Column as ColumnarColumn } from '../columnar/index.js';
|
|
2
|
+
import type { ValueSeriesColumnName, ValueSeriesSchema } from '../schema/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* A **value-keyed series** — the closed value-axis counterpart of
|
|
5
|
+
* `TimeSeries`. Its key is a monotonic non-time axis (distance, cumulative
|
|
6
|
+
* work, …) rather than time, produced by `TimeSeries.byValue(axis)`.
|
|
7
|
+
*
|
|
8
|
+
* `ValueSeries` carries the **ordering-based** operators (read the axis, read
|
|
9
|
+
* value columns, nearest-by-value, slice-by-value) — the part of the series
|
|
10
|
+
* algebra that was never really about time (RFC `value-axis.md` §5). The
|
|
11
|
+
* calendar/clock operators (`Sequence.every`, tz formatting) are deliberately
|
|
12
|
+
* absent: a value axis has no wall-clock semantics, and the disjoint
|
|
13
|
+
* `ValueSeriesSchema` makes them type-impossible here.
|
|
14
|
+
*
|
|
15
|
+
* Minimal by design (RFC §7: adopt the type early, grow the algebra as a second
|
|
16
|
+
* value-axis consumer earns it). Wraps the columnar store directly — a value
|
|
17
|
+
* row is an `(axis, …values)` tuple, not a `Time`-keyed `Event`, so it does not
|
|
18
|
+
* go through the time-only `SeriesStore` / EventKey layer.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ValueSeries<VS extends ValueSeriesSchema> {
|
|
21
|
+
#private;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly schema: VS;
|
|
24
|
+
private constructor();
|
|
25
|
+
/** Number of rows. */
|
|
26
|
+
get length(): number;
|
|
27
|
+
/** The axis (key) column's name — e.g. `'cumDist'`. */
|
|
28
|
+
get axisName(): VS[0]['name'];
|
|
29
|
+
/**
|
|
30
|
+
* The axis values (the x of every row), in axis order. **Zero-copy** — the
|
|
31
|
+
* returned `Float64Array` is the live key buffer; treat it as read-only.
|
|
32
|
+
*/
|
|
33
|
+
axisValues(): Float64Array;
|
|
34
|
+
/** The axis value at row `i`. Throws if out of range. */
|
|
35
|
+
axisAt(i: number): number;
|
|
36
|
+
/** A value column by name, for direct columnar reads (`.read(i)`, `.values()`). */
|
|
37
|
+
column(name: ValueSeriesColumnName<VS>): ColumnarColumn | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Index of the row whose axis value is **closest** to `value` — the
|
|
40
|
+
* value-axis cursor primitive. The axis is non-decreasing, so this is a
|
|
41
|
+
* binary search. Returns `-1` for an empty series; clamps to the first / last
|
|
42
|
+
* row when `value` is outside the axis extent.
|
|
43
|
+
*/
|
|
44
|
+
nearestIndex(value: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* The contiguous sub-series whose axis value lies in `[lo, hi)` — the
|
|
47
|
+
* value-axis cull (pan / zoom on a value x). Binary-searches the bounds and
|
|
48
|
+
* zero-copy slices the store. `lo >= hi` (or a range outside the extent)
|
|
49
|
+
* yields an empty series.
|
|
50
|
+
*/
|
|
51
|
+
sliceByValue(lo: number, hi: number): ValueSeries<VS>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=value-series.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { withRowRange, } from '../columnar/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* A **value-keyed series** — the closed value-axis counterpart of
|
|
4
|
+
* `TimeSeries`. Its key is a monotonic non-time axis (distance, cumulative
|
|
5
|
+
* work, …) rather than time, produced by `TimeSeries.byValue(axis)`.
|
|
6
|
+
*
|
|
7
|
+
* `ValueSeries` carries the **ordering-based** operators (read the axis, read
|
|
8
|
+
* value columns, nearest-by-value, slice-by-value) — the part of the series
|
|
9
|
+
* algebra that was never really about time (RFC `value-axis.md` §5). The
|
|
10
|
+
* calendar/clock operators (`Sequence.every`, tz formatting) are deliberately
|
|
11
|
+
* absent: a value axis has no wall-clock semantics, and the disjoint
|
|
12
|
+
* `ValueSeriesSchema` makes them type-impossible here.
|
|
13
|
+
*
|
|
14
|
+
* Minimal by design (RFC §7: adopt the type early, grow the algebra as a second
|
|
15
|
+
* value-axis consumer earns it). Wraps the columnar store directly — a value
|
|
16
|
+
* row is an `(axis, …values)` tuple, not a `Time`-keyed `Event`, so it does not
|
|
17
|
+
* go through the time-only `SeriesStore` / EventKey layer.
|
|
18
|
+
*/
|
|
19
|
+
export class ValueSeries {
|
|
20
|
+
name;
|
|
21
|
+
schema;
|
|
22
|
+
#store;
|
|
23
|
+
/**
|
|
24
|
+
* @internal Trusted construction — `store` must be value-keyed and structurally
|
|
25
|
+
* match `schema` (the invariant `TimeSeries.byValue` / `byValueOp` establish).
|
|
26
|
+
* Not for general use; construct a `ValueSeries` via `TimeSeries.byValue`.
|
|
27
|
+
*/
|
|
28
|
+
static fromTrustedStore(name, schema, store) {
|
|
29
|
+
return new ValueSeries(name, schema, store);
|
|
30
|
+
}
|
|
31
|
+
constructor(name, schema, store) {
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.schema = Object.freeze(schema.slice());
|
|
34
|
+
this.#store = store;
|
|
35
|
+
}
|
|
36
|
+
/** Number of rows. */
|
|
37
|
+
get length() {
|
|
38
|
+
return this.#store.length;
|
|
39
|
+
}
|
|
40
|
+
/** The axis (key) column's name — e.g. `'cumDist'`. */
|
|
41
|
+
get axisName() {
|
|
42
|
+
return this.schema[0].name;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The axis values (the x of every row), in axis order. **Zero-copy** — the
|
|
46
|
+
* returned `Float64Array` is the live key buffer; treat it as read-only.
|
|
47
|
+
*/
|
|
48
|
+
axisValues() {
|
|
49
|
+
return this.#store.keys.begin;
|
|
50
|
+
}
|
|
51
|
+
/** The axis value at row `i`. Throws if out of range. */
|
|
52
|
+
axisAt(i) {
|
|
53
|
+
return this.#store.keys.beginAt(i);
|
|
54
|
+
}
|
|
55
|
+
/** A value column by name, for direct columnar reads (`.read(i)`, `.values()`). */
|
|
56
|
+
column(name) {
|
|
57
|
+
return this.#store.columns.get(name);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Index of the row whose axis value is **closest** to `value` — the
|
|
61
|
+
* value-axis cursor primitive. The axis is non-decreasing, so this is a
|
|
62
|
+
* binary search. Returns `-1` for an empty series; clamps to the first / last
|
|
63
|
+
* row when `value` is outside the axis extent.
|
|
64
|
+
*/
|
|
65
|
+
nearestIndex(value) {
|
|
66
|
+
const n = this.length;
|
|
67
|
+
if (n === 0)
|
|
68
|
+
return -1;
|
|
69
|
+
const ax = this.axisValues();
|
|
70
|
+
const lo = lowerBound(ax, n, value);
|
|
71
|
+
if (lo === 0)
|
|
72
|
+
return 0;
|
|
73
|
+
if (lo === n)
|
|
74
|
+
return n - 1;
|
|
75
|
+
return value - ax[lo - 1] <= ax[lo] - value ? lo - 1 : lo;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The contiguous sub-series whose axis value lies in `[lo, hi)` — the
|
|
79
|
+
* value-axis cull (pan / zoom on a value x). Binary-searches the bounds and
|
|
80
|
+
* zero-copy slices the store. `lo >= hi` (or a range outside the extent)
|
|
81
|
+
* yields an empty series.
|
|
82
|
+
*/
|
|
83
|
+
sliceByValue(lo, hi) {
|
|
84
|
+
const ax = this.axisValues();
|
|
85
|
+
const n = this.length;
|
|
86
|
+
const loIdx = lowerBound(ax, n, lo);
|
|
87
|
+
const hiIdx = lowerBound(ax, n, hi);
|
|
88
|
+
const sliced = withRowRange(this.#store, loIdx, hiIdx);
|
|
89
|
+
return ValueSeries.fromTrustedStore(this.name, this.schema, sliced);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** First index `i` in `ax[0..n)` with `ax[i] >= target` (lower bound). */
|
|
93
|
+
function lowerBound(ax, n, target) {
|
|
94
|
+
let lo = 0;
|
|
95
|
+
let hi = n;
|
|
96
|
+
while (lo < hi) {
|
|
97
|
+
const mid = (lo + hi) >>> 1;
|
|
98
|
+
if (ax[mid] < target)
|
|
99
|
+
lo = mid + 1;
|
|
100
|
+
else
|
|
101
|
+
hi = mid;
|
|
102
|
+
}
|
|
103
|
+
return lo;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=value-series.js.map
|
package/dist/columnar/index.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export { type ColumnarRingBufferOptions, ColumnarRingBuffer, } from './ring-buff
|
|
|
33
33
|
export { type OnUndefinedPartition, type ScatterByPartitionOptions, scatterByPartition, } from './scatter.js';
|
|
34
34
|
export { DICT_ENCODE_MIN_LENGTH, DICT_ENCODE_RATIO, StringColumn, buildDictionaryIndex, estimateDictionaryBytes, remapColumnToDictionary, remapIndicesToDictionary, stringColumnDictEncoded, stringColumnFallback, stringColumnFromArray, } from './string-column.js';
|
|
35
35
|
export { ArrayColumn, EMPTY_ARRAY_SENTINEL, arrayColumnFromArray, } from './array-column.js';
|
|
36
|
-
export { type IntervalLabelKind, type KeyColumn, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, timeKeyColumnFromArray, timeRangeKeyColumnFromPairs, } from './key-column.js';
|
|
36
|
+
export { type IntervalLabelKind, type KeyColumn, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, ValueKeyColumn, timeKeyColumnFromArray, timeRangeKeyColumnFromPairs, valueKeyColumnFromArray, } from './key-column.js';
|
|
37
37
|
export { type FromTrustedStoreOptions, ColumnarStore } from './store.js';
|
|
38
38
|
export { type ColumnBuilder, ArrayColumnBuilder, BooleanColumnBuilder, Float64ColumnBuilder, StringColumnBuilder, columnBuilderForKind, } from './builder.js';
|
|
39
39
|
export { type AnyColumnKind, type ArrayValue, type ColumnDef, type ColumnSchema, type KeyKind, type ScalarValue, } from './types.js';
|
package/dist/columnar/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export { ColumnarRingBuffer, } from './ring-buffer.js';
|
|
|
33
33
|
export { scatterByPartition, } from './scatter.js';
|
|
34
34
|
export { DICT_ENCODE_MIN_LENGTH, DICT_ENCODE_RATIO, StringColumn, buildDictionaryIndex, estimateDictionaryBytes, remapColumnToDictionary, remapIndicesToDictionary, stringColumnDictEncoded, stringColumnFallback, stringColumnFromArray, } from './string-column.js';
|
|
35
35
|
export { ArrayColumn, EMPTY_ARRAY_SENTINEL, arrayColumnFromArray, } from './array-column.js';
|
|
36
|
-
export { IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, timeKeyColumnFromArray, timeRangeKeyColumnFromPairs, } from './key-column.js';
|
|
36
|
+
export { IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, ValueKeyColumn, timeKeyColumnFromArray, timeRangeKeyColumnFromPairs, valueKeyColumnFromArray, } from './key-column.js';
|
|
37
37
|
export { ColumnarStore } from './store.js';
|
|
38
38
|
export { ArrayColumnBuilder, BooleanColumnBuilder, Float64ColumnBuilder, StringColumnBuilder, columnBuilderForKind, } from './builder.js';
|
|
39
39
|
export { materialize, withColumnAppended, withColumnReplaced, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from './view.js';
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
import { Float64Column } from './column.js';
|
|
34
34
|
import { StringColumn } from './string-column.js';
|
|
35
35
|
/** The framework's key-column discriminated union. */
|
|
36
|
-
export type KeyColumn = TimeKeyColumn | TimeRangeKeyColumn | IntervalKeyColumn;
|
|
36
|
+
export type KeyColumn = TimeKeyColumn | TimeRangeKeyColumn | IntervalKeyColumn | ValueKeyColumn;
|
|
37
37
|
/**
|
|
38
38
|
* Shared interface implemented by every key-column class. Pure
|
|
39
39
|
* indexed buffer access; the framework knows nothing about
|
|
40
40
|
* `EventKey` / `Time` / `TimeRange` / `Interval`.
|
|
41
41
|
*/
|
|
42
|
-
interface KeyColumnBase<K extends 'time' | 'timeRange' | 'interval'> {
|
|
42
|
+
interface KeyColumnBase<K extends 'time' | 'timeRange' | 'interval' | 'value'> {
|
|
43
43
|
readonly kind: K;
|
|
44
44
|
/** Row count. */
|
|
45
45
|
readonly length: number;
|
|
@@ -97,6 +97,34 @@ export declare class TimeKeyColumn implements KeyColumnBase<'time'> {
|
|
|
97
97
|
*/
|
|
98
98
|
sliceByIndices(indices: Int32Array): TimeKeyColumn;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Point key on a **value axis** (distance, cumulative work, …) rather than
|
|
102
|
+
* time — the substrate of a `ValueSeries`. Structurally identical to
|
|
103
|
+
* {@link TimeKeyColumn} (single `Float64Array`, `end === begin`); only the
|
|
104
|
+
* `kind` tag differs, which is what gates the calendar/clock operators
|
|
105
|
+
* (`Sequence.every`, tz tick formatting) off a value-keyed series.
|
|
106
|
+
*
|
|
107
|
+
* The buffer carries finite axis values, not epoch milliseconds. **Ordering
|
|
108
|
+
* (non-decreasing) is NOT enforced here** — the monotonicity contract lives on
|
|
109
|
+
* the `byValue` projection (`assertMonotonicAxis`), so the column stays a dumb
|
|
110
|
+
* indexed buffer (matching how the time columns don't self-validate sort
|
|
111
|
+
* order). Finiteness *is* enforced: a `NaN` / `Infinity` axis value would break
|
|
112
|
+
* bisection and range logic.
|
|
113
|
+
*/
|
|
114
|
+
export declare class ValueKeyColumn implements KeyColumnBase<'value'> {
|
|
115
|
+
readonly kind: "value";
|
|
116
|
+
readonly length: number;
|
|
117
|
+
readonly begin: Float64Array;
|
|
118
|
+
/** For a point value key, `end === begin` (same buffer) — a zero-width axis position. */
|
|
119
|
+
readonly end: Float64Array;
|
|
120
|
+
constructor(begin: Float64Array, length: number);
|
|
121
|
+
beginAt(i: number): number;
|
|
122
|
+
endAt(i: number): number;
|
|
123
|
+
/** Zero-copy index-range view. Mirrors {@link TimeKeyColumn.sliceByRange}. */
|
|
124
|
+
sliceByRange(start: number, end: number): ValueKeyColumn;
|
|
125
|
+
/** Gathers rows by index. See {@link TimeKeyColumn.sliceByIndices}. */
|
|
126
|
+
sliceByIndices(indices: Int32Array): ValueKeyColumn;
|
|
127
|
+
}
|
|
100
128
|
export declare class TimeRangeKeyColumn implements KeyColumnBase<'timeRange'> {
|
|
101
129
|
readonly kind: "timeRange";
|
|
102
130
|
readonly length: number;
|
|
@@ -181,6 +209,12 @@ export declare class IntervalKeyColumn implements KeyColumnBase<'interval'> {
|
|
|
181
209
|
* value columns.
|
|
182
210
|
*/
|
|
183
211
|
export declare function timeKeyColumnFromArray(timestamps: ReadonlyArray<number>): TimeKeyColumn;
|
|
212
|
+
/**
|
|
213
|
+
* Builds a {@link ValueKeyColumn} from an array of axis values. The values
|
|
214
|
+
* must be finite; ordering (non-decreasing) is the `byValue` projection's
|
|
215
|
+
* contract, not this factory's.
|
|
216
|
+
*/
|
|
217
|
+
export declare function valueKeyColumnFromArray(values: ReadonlyArray<number>): ValueKeyColumn;
|
|
184
218
|
/**
|
|
185
219
|
* Builds a `TimeRangeKeyColumn` from `[begin, end]` pairs. Each pair
|
|
186
220
|
* must satisfy `begin <= end`.
|