pond-ts 0.32.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 CHANGED
@@ -8,7 +8,9 @@ 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.32.0...HEAD
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
13
+ [0.33.0]: https://github.com/pjm17971/pond-ts/compare/v0.32.0...v0.33.0
12
14
  [0.32.0]: https://github.com/pjm17971/pond-ts/compare/v0.31.2...v0.32.0
13
15
  [0.31.2]: https://github.com/pjm17971/pond-ts/compare/v0.31.1...v0.31.2
14
16
  [0.31.1]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...v0.31.1
@@ -27,6 +29,75 @@ patch bumps are strictly additive.
27
29
  [0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
28
30
  [0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
29
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
+
66
+ ## [0.33.0] — 2026-06-30
67
+
68
+ A `@pond-ts/charts` release: a label opt-out for annotations plus interaction
69
+ fixes from an adversarial review of the #306 annotation system. `pond-ts`,
70
+ `@pond-ts/react`, and `@pond-ts/fit` carry no code changes — republished in
71
+ lock-step (their `pond-ts` / `@pond-ts/react` peer ranges widen to `^0.33.0`).
72
+
73
+ ### Added
74
+
75
+ - **Charts — annotation label opt-out.** `<Region>`, `<Marker>`, and
76
+ `<Baseline>` accept `label={false}` (or `label=""`) to render **no label
77
+ chip** — for an inert background mark (e.g. a `selectable={false}` highlight
78
+ band) where the auto-label would only show a raw axis value. Omitting `label`
79
+ still auto-labels; a string still renders it. `label` widens to
80
+ `string | false`. New `Highlight` story + Linux e2e baselines for the
81
+ `Annotations` stories. (#308)
82
+
83
+ ### Changed
84
+
85
+ - **Charts — `<Region>` / `<Marker>` / `<Baseline>` with `label=""` now render
86
+ no chip** (previously an empty, zero-width chip). The label-less path; pass a
87
+ non-empty string for a visible label. (#308)
88
+
89
+ ### Fixed
90
+
91
+ - **Charts — annotation edge-resize no longer inverts.** Dragging a `<Region>`
92
+ edge past the opposite one previously reported `{ from > to }`; it now pivots
93
+ around the fixed opposite edge, so the reported span stays ordered and a drag
94
+ either way re-opens the region instead of dead-ending at zero width. (#308)
95
+ - **Charts — annotation drag releases on `pointercancel`.** A system gesture
96
+ takeover (which fires `pointercancel`, not `pointerup`) no longer leaves a mark
97
+ stuck mid-drag. (#308)
98
+ - **Charts — single-annotation edit exits on an empty-plot click** even when the
99
+ mark is `editing` but not `selected`. (#308)
100
+
30
101
  ## [0.32.0] — 2026-06-29
31
102
 
32
103
  A `@pond-ts/charts` release: value-axis support across the fill/bar layers, plus
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pond-ts",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "TypeScript-first time series primitives",
5
5
  "license": "MIT",
6
6
  "repository": {