pond-ts 0.33.0 → 0.34.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 +36 -1
- package/dist/batch/time-series.d.ts +40 -0
- package/dist/batch/time-series.js +104 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,8 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
|
|
|
8
8
|
them all. Pre-1.0: minor bumps may include new features and type-level changes;
|
|
9
9
|
patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.34.0...HEAD
|
|
12
|
+
[0.34.0]: https://github.com/pjm17971/pond-ts/compare/v0.33.0...v0.34.0
|
|
12
13
|
[0.33.0]: https://github.com/pjm17971/pond-ts/compare/v0.32.0...v0.33.0
|
|
13
14
|
[0.32.0]: https://github.com/pjm17971/pond-ts/compare/v0.31.2...v0.32.0
|
|
14
15
|
[0.31.2]: https://github.com/pjm17971/pond-ts/compare/v0.31.1...v0.31.2
|
|
@@ -28,6 +29,40 @@ patch bumps are strictly additive.
|
|
|
28
29
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
29
30
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
30
31
|
|
|
32
|
+
## [0.34.0] — 2026-07-01
|
|
33
|
+
|
|
34
|
+
A `pond-ts` core release: the columnar/typed-array ingress driven by the
|
|
35
|
+
Tidal wire-format spike. `@pond-ts/react`, `@pond-ts/charts`, and
|
|
36
|
+
`@pond-ts/fit` carry no code changes — republished in lock-step (their
|
|
37
|
+
`pond-ts` / `@pond-ts/react` peer ranges widen to `^0.34.0`).
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`TimeSeries.fromColumns`** — the columnar (struct-of-arrays) ingress,
|
|
42
|
+
the counterpart to `fromJSON`'s row-tuple shape. Accepts either a plain
|
|
43
|
+
`number[]` or a `Float64Array` per column — one polymorphic door, so a
|
|
44
|
+
wire format only changes the *decoder*, not the ingest. `Float64Array`
|
|
45
|
+
columns are adopted directly (zero-copy); `number[]` columns are copied.
|
|
46
|
+
A `null`/`undefined` cell or a non-finite value (`NaN`/`Infinity`) is a
|
|
47
|
+
gap, identically across both input shapes. Enforces the same
|
|
48
|
+
non-decreasing key-order invariant as `fromJSON`. v1 scope: a `time`-kind
|
|
49
|
+
key and `number` value columns. (#310)
|
|
50
|
+
|
|
51
|
+
Measured against a wire-format spike (`tidal-app/pond-columnar-ingest`):
|
|
52
|
+
`fromColumns` collapses the ingest step that every non-rows path
|
|
53
|
+
previously paid (transpose → `fromJSON`) — 100k-point protobuf ingest
|
|
54
|
+
27.7ms → 2.8ms; JSON-columnar 27.0ms → 5.2ms. In a browser, decoding
|
|
55
|
+
off-main in a Web Worker and transferring the resulting `Float64Array`s
|
|
56
|
+
back keeps `fromColumns`'s adopt path on the main thread to ~9ms,
|
|
57
|
+
dropping a 500k-point ingest's worst animation-frame stall from ~50ms to
|
|
58
|
+
~9ms.
|
|
59
|
+
|
|
60
|
+
- **Docs** — a "Columnar ingest" section on the
|
|
61
|
+
[Creating series](https://pjm17971.github.io/pond-ts/docs/start-here/creating#columnar-ingest)
|
|
62
|
+
page covering both input shapes, the adopt-vs-copy/aliasing distinction,
|
|
63
|
+
missing-value and ordering semantics, and why this matters for
|
|
64
|
+
interactive charts; a pointer from Getting Started.
|
|
65
|
+
|
|
31
66
|
## [0.33.0] — 2026-06-30
|
|
32
67
|
|
|
33
68
|
A `@pond-ts/charts` release: a label opt-out for annotations plus interaction
|
|
@@ -87,6 +87,46 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
87
87
|
*/
|
|
88
88
|
sort?: boolean;
|
|
89
89
|
}): TimeSeries<S>;
|
|
90
|
+
/**
|
|
91
|
+
* Example: `TimeSeries.fromColumns({ name, schema, columns })`.
|
|
92
|
+
*
|
|
93
|
+
* The **columnar** ingress — the struct-of-arrays counterpart to
|
|
94
|
+
* {@link TimeSeries.fromJSON} (which takes row tuples). Each entry in
|
|
95
|
+
* `columns` is one column's values, keyed by schema column name and aligned by
|
|
96
|
+
* index; every column must have the same length. Values may be a plain
|
|
97
|
+
* `number[]` (e.g. `JSON.parse` of a columnar wire payload) **or** a
|
|
98
|
+
* `Float64Array` (e.g. protobuf packed doubles / fixed-point after descale) —
|
|
99
|
+
* one polymorphic door, so the wire format only changes the *decoder*, not the
|
|
100
|
+
* ingest. A value cell is a gap (missing) iff it's `null`/`undefined` or
|
|
101
|
+
* non-finite (`NaN`/`Infinity`) — identical rule regardless of which array
|
|
102
|
+
* type supplied it. Note this is looser than `fromJSON`, which *rejects* a
|
|
103
|
+
* non-finite provided number outright rather than treating it as a gap.
|
|
104
|
+
*
|
|
105
|
+
* Skips the row-tuple materialization + per-row intake a columnar caller
|
|
106
|
+
* otherwise pays (transpose → `fromJSON`): each column packs directly, the key
|
|
107
|
+
* column is built from the first schema column, and the store is assembled via
|
|
108
|
+
* trusted construction (`ColumnarStore.fromTrustedStore` still validates kind +
|
|
109
|
+
* aligned length).
|
|
110
|
+
*
|
|
111
|
+
* **`Float64Array` inputs are adopted, not copied** (zero-copy — the point of
|
|
112
|
+
* the fast path): the resulting series' columns alias the caller's buffers.
|
|
113
|
+
* Mutating an adopted buffer after construction mutates the series. Pass a
|
|
114
|
+
* fresh buffer (e.g. straight off a decode) if this matters; `number[]`
|
|
115
|
+
* columns are always copied.
|
|
116
|
+
*
|
|
117
|
+
* **v1 scope:** a `time`-kind key + `number` value columns (the market-data /
|
|
118
|
+
* chart wire case). Other key kinds (`interval` / `timeRange`) and non-numeric
|
|
119
|
+
* value columns throw for now — extend as consumers need.
|
|
120
|
+
*
|
|
121
|
+
* @throws ValidationError on a missing column, a length mismatch, an
|
|
122
|
+
* unsupported kind, a non-finite timestamp key, or an out-of-order
|
|
123
|
+
* (decreasing) timestamp — keys must be non-decreasing, same as `fromJSON`.
|
|
124
|
+
*/
|
|
125
|
+
static fromColumns<S extends SeriesSchema>(input: {
|
|
126
|
+
name: string;
|
|
127
|
+
schema: S;
|
|
128
|
+
columns: Record<string, ReadonlyArray<number | null | undefined> | Float64Array>;
|
|
129
|
+
}): TimeSeries<S>;
|
|
90
130
|
/**
|
|
91
131
|
* Example: `TimeSeries.fromEvents(events, { schema, name })`.
|
|
92
132
|
* Builds a typed series from an array of `Event` instances. The events
|
|
@@ -18,7 +18,8 @@ import { TimeRange } from '../core/time-range.js';
|
|
|
18
18
|
import { compareEventKeys } from '../core/temporal.js';
|
|
19
19
|
import { PartitionedTimeSeries } from './partitioned-time-series.js';
|
|
20
20
|
import { Sequence } from '../sequence/sequence.js';
|
|
21
|
-
import { ColumnarStore, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, float64ColumnFromArray, stringColumnFromArray, withColumnAppended, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from '../columnar/index.js';
|
|
21
|
+
import { ColumnarStore, Float64Column, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, float64ColumnFromArray, stringColumnFromArray, validityFromPredicate, withColumnAppended, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from '../columnar/index.js';
|
|
22
|
+
import { ValidationError } from '../core/errors.js';
|
|
22
23
|
import { SeriesStore } from '../live/series-store.js';
|
|
23
24
|
import { parseDuration } from '../core/duration.js';
|
|
24
25
|
import { resolveReducer, bucketStateFor, rollingStateFor, } from '../reducers/index.js';
|
|
@@ -485,6 +486,108 @@ export class TimeSeries {
|
|
|
485
486
|
sort: input.sort ?? false,
|
|
486
487
|
});
|
|
487
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Example: `TimeSeries.fromColumns({ name, schema, columns })`.
|
|
491
|
+
*
|
|
492
|
+
* The **columnar** ingress — the struct-of-arrays counterpart to
|
|
493
|
+
* {@link TimeSeries.fromJSON} (which takes row tuples). Each entry in
|
|
494
|
+
* `columns` is one column's values, keyed by schema column name and aligned by
|
|
495
|
+
* index; every column must have the same length. Values may be a plain
|
|
496
|
+
* `number[]` (e.g. `JSON.parse` of a columnar wire payload) **or** a
|
|
497
|
+
* `Float64Array` (e.g. protobuf packed doubles / fixed-point after descale) —
|
|
498
|
+
* one polymorphic door, so the wire format only changes the *decoder*, not the
|
|
499
|
+
* ingest. A value cell is a gap (missing) iff it's `null`/`undefined` or
|
|
500
|
+
* non-finite (`NaN`/`Infinity`) — identical rule regardless of which array
|
|
501
|
+
* type supplied it. Note this is looser than `fromJSON`, which *rejects* a
|
|
502
|
+
* non-finite provided number outright rather than treating it as a gap.
|
|
503
|
+
*
|
|
504
|
+
* Skips the row-tuple materialization + per-row intake a columnar caller
|
|
505
|
+
* otherwise pays (transpose → `fromJSON`): each column packs directly, the key
|
|
506
|
+
* column is built from the first schema column, and the store is assembled via
|
|
507
|
+
* trusted construction (`ColumnarStore.fromTrustedStore` still validates kind +
|
|
508
|
+
* aligned length).
|
|
509
|
+
*
|
|
510
|
+
* **`Float64Array` inputs are adopted, not copied** (zero-copy — the point of
|
|
511
|
+
* the fast path): the resulting series' columns alias the caller's buffers.
|
|
512
|
+
* Mutating an adopted buffer after construction mutates the series. Pass a
|
|
513
|
+
* fresh buffer (e.g. straight off a decode) if this matters; `number[]`
|
|
514
|
+
* columns are always copied.
|
|
515
|
+
*
|
|
516
|
+
* **v1 scope:** a `time`-kind key + `number` value columns (the market-data /
|
|
517
|
+
* chart wire case). Other key kinds (`interval` / `timeRange`) and non-numeric
|
|
518
|
+
* value columns throw for now — extend as consumers need.
|
|
519
|
+
*
|
|
520
|
+
* @throws ValidationError on a missing column, a length mismatch, an
|
|
521
|
+
* unsupported kind, a non-finite timestamp key, or an out-of-order
|
|
522
|
+
* (decreasing) timestamp — keys must be non-decreasing, same as `fromJSON`.
|
|
523
|
+
*/
|
|
524
|
+
static fromColumns(input) {
|
|
525
|
+
const { name, schema, columns } = input;
|
|
526
|
+
// Key column (schema[0]). v1: point-in-time (`time`) keys only.
|
|
527
|
+
const keyDef = schema[0];
|
|
528
|
+
if (keyDef === undefined) {
|
|
529
|
+
throw new ValidationError('fromColumns: schema must have at least a key column');
|
|
530
|
+
}
|
|
531
|
+
if (keyDef.kind !== 'time') {
|
|
532
|
+
throw new ValidationError(`fromColumns: v1 supports a 'time' key; schema[0] '${keyDef.name}' is '${keyDef.kind}'`);
|
|
533
|
+
}
|
|
534
|
+
const keyRaw = columns[keyDef.name];
|
|
535
|
+
if (keyRaw === undefined) {
|
|
536
|
+
throw new ValidationError(`fromColumns: missing key column '${keyDef.name}'`);
|
|
537
|
+
}
|
|
538
|
+
// epoch-ms buffer; TimeKeyColumn asserts all finite.
|
|
539
|
+
const begin = keyRaw instanceof Float64Array
|
|
540
|
+
? keyRaw
|
|
541
|
+
: Float64Array.from(keyRaw, (v) => (v == null ? NaN : Number(v)));
|
|
542
|
+
const count = begin.length;
|
|
543
|
+
// Throws on any non-finite timestamp.
|
|
544
|
+
const keys = new TimeKeyColumn(begin, count);
|
|
545
|
+
// Enforce the non-decreasing-key invariant that `fromJSON`'s
|
|
546
|
+
// `validateAndNormalize` guarantees. Trusted construction skips row
|
|
547
|
+
// materialization + kind re-validation, but NOT this correctness contract:
|
|
548
|
+
// bisect-based operators (crop, `atTime`, range queries) rely on it, so an
|
|
549
|
+
// unsorted columnar input must fail loudly here rather than build a silently
|
|
550
|
+
// broken series. One O(N) scan over already-finite values — negligible next
|
|
551
|
+
// to decode.
|
|
552
|
+
for (let j = 1; j < count; j += 1) {
|
|
553
|
+
if (begin[j] < begin[j - 1]) {
|
|
554
|
+
throw new ValidationError(`fromColumns: key column '${keyDef.name}' is out of order at index ${j} ` +
|
|
555
|
+
`(${begin[j]} < ${begin[j - 1]}) — timestamps must be non-decreasing; pre-sort the columns`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Value columns — packed directly (missing-aware) from the arrays.
|
|
559
|
+
const columnMap = new Map();
|
|
560
|
+
for (let i = 1; i < schema.length; i += 1) {
|
|
561
|
+
const def = schema[i];
|
|
562
|
+
if (def.kind !== 'number') {
|
|
563
|
+
throw new ValidationError(`fromColumns: v1 supports 'number' value columns; column '${def.name}' is '${def.kind}'`);
|
|
564
|
+
}
|
|
565
|
+
const raw = columns[def.name];
|
|
566
|
+
if (raw === undefined) {
|
|
567
|
+
throw new ValidationError(`fromColumns: missing column '${def.name}'`);
|
|
568
|
+
}
|
|
569
|
+
if (raw.length !== count) {
|
|
570
|
+
throw new ValidationError(`fromColumns: column '${def.name}' length ${raw.length} does not match key length ${count}`);
|
|
571
|
+
}
|
|
572
|
+
// Normalize to a Float64Array either way — adopt if already typed (the
|
|
573
|
+
// fast path a protobuf / fixed-point decoder hits, zero-copy), else
|
|
574
|
+
// convert (`null`/`undefined` -> `NaN`) — then apply ONE validity rule
|
|
575
|
+
// to both: a cell is a gap iff it's non-finite. This must be identical
|
|
576
|
+
// regardless of input type: an earlier version used `float64ColumnFromArray`
|
|
577
|
+
// for the `number[]` branch, which treats a `NaN` *value* (as opposed to
|
|
578
|
+
// `null`) as defined-but-non-finite rather than missing, diverging from
|
|
579
|
+
// the `Float64Array` branch's `Number.isFinite` gap signal — the same
|
|
580
|
+
// wire value would silently mean different things depending on which
|
|
581
|
+
// array type decoded it.
|
|
582
|
+
const values = raw instanceof Float64Array
|
|
583
|
+
? raw
|
|
584
|
+
: Float64Array.from(raw, (v) => (v == null ? NaN : v));
|
|
585
|
+
const validity = validityFromPredicate(count, (j) => Number.isFinite(values[j]));
|
|
586
|
+
columnMap.set(def.name, new Float64Column(values, count, validity, validity === undefined));
|
|
587
|
+
}
|
|
588
|
+
const store = ColumnarStore.fromTrustedStore(schema, keys, columnMap);
|
|
589
|
+
return TimeSeries.#fromTrustedStore(name, schema, store);
|
|
590
|
+
}
|
|
488
591
|
/**
|
|
489
592
|
* Example: `TimeSeries.fromEvents(events, { schema, name })`.
|
|
490
593
|
* Builds a typed series from an array of `Event` instances. The events
|