pond-ts 0.39.0 → 0.40.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,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.39.0...HEAD
11
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.40.0...HEAD
12
+ [0.40.0]: https://github.com/pjm17971/pond-ts/compare/v0.39.0...v0.40.0
12
13
  [0.39.0]: https://github.com/pjm17971/pond-ts/compare/v0.38.0...v0.39.0
13
14
  [0.38.0]: https://github.com/pjm17971/pond-ts/compare/v0.37.0...v0.38.0
14
15
  [0.37.0]: https://github.com/pjm17971/pond-ts/compare/v0.36.0...v0.37.0
@@ -37,21 +38,34 @@ patch bumps are strictly additive.
37
38
 
38
39
  ## [Unreleased]
39
40
 
41
+ ## [0.40.0] — 2026-07-05
42
+
43
+ A **core + charts** release from the estela `DataChart`-port friction wave.
44
+ `@pond-ts/react` and `@pond-ts/fit` carry no code changes — republished in
45
+ lock-step (peer ranges widen to `^0.40.0`).
46
+
47
+ ### Added
48
+
49
+ - `pond-ts`: **`TimeSeries.fromColumns({ sort })`** — an opt-in `sort?: boolean`
50
+ (default `false`) that stable-sorts a columnar payload by key before
51
+ construction, the columnar counterpart of `fromJSON`'s `sort`. The default path
52
+ is unchanged: a decreasing key still throws (a backwards key on the trusted fast
53
+ door is a corruption signal, not silently accepted), and the `Float64Array`
54
+ zero-copy adoption is preserved when `sort` is unset. (#344)
55
+ - `@pond-ts/charts`: **controlled bar hover** — `<ChartContainer hovered
56
+ onHover>`, the transient-hover analog of the existing `selected` / `onSelect`
57
+ pair, keyed by the same `SelectInfo`. Pin a lit `<BarChart>` bar from a legend
58
+ or list row (`hovered`), or mirror a bar-originated hover out-of-band
59
+ (`onHover`); omit both for today's uncontrolled behavior. (#343)
60
+
40
61
  ### Fixed
41
62
 
42
- - **Charts — click-to-select an annotation now works while `panZoom` is on.** A
43
- _selectable but non-editable_ `<Region>` / `<Marker>` (one with no `onChange`)
44
- lets its press bubble to the plot so a drag can pan _through_ it. The plot
45
- captured the pointer on press to start the pan, and the browser then retargeted
46
- the resulting `click` onto the plot (Pointer Events spec: a captured pointer's
47
- compatibility mouse events fire on the capture target) silently dropping the
48
- mark's `onSelectAnnotation`. The plot now **defers** its pan pointer-capture
49
- until the pointer actually moves past the drag slop, so a click (no drag) leaves
50
- the pointer on the mark and its select fires, while a press-drag still pans
51
- through and the tracker still hides once the pan commits. Resolves the
52
- browser-dependent finding deferred from #308; adds
53
- `e2e/annotations-panzoom.spec.ts`, the first real-pointer-event behavior e2e for
54
- the annotation layer. (#309)
63
+ - `@pond-ts/charts`: **axis and layer registration are value-equality-guarded**
64
+ a fresh-but-value-equal `ticks` / `format` / `byValue()`-projected `series`
65
+ reference no longer re-registers the axis/layer, fixing a "Maximum update depth
66
+ exceeded" loop on frequently re-rendering (scrub-driven) charts. The layer and
67
+ axis docs gain a memoize note for `format` / `series` (an inline `format`
68
+ closure still must be hoisted a closure can't be value-compared). (#342)
55
69
 
56
70
  ## [0.39.0] — 2026-07-03
57
71
 
@@ -89,6 +103,19 @@ carry no code changes — republished in lock-step (peer ranges widen to `^0.39.
89
103
 
90
104
  - `@pond-ts/charts`: the crosshair x-axis pill and marker pills read the axis's
91
105
  own formatter (a value-axis / off-boundary time no longer shows a raw number).
106
+ - **Charts — click-to-select an annotation now works while `panZoom` is on.** A
107
+ _selectable but non-editable_ `<Region>` / `<Marker>` (one with no `onChange`)
108
+ lets its press bubble to the plot so a drag can pan _through_ it. The plot
109
+ captured the pointer on press to start the pan, and the browser then retargeted
110
+ the resulting `click` onto the plot (Pointer Events spec: a captured pointer's
111
+ compatibility mouse events fire on the capture target) — silently dropping the
112
+ mark's `onSelectAnnotation`. The plot now **defers** its pan pointer-capture
113
+ until the pointer actually moves past the drag slop, so a click (no drag) leaves
114
+ the pointer on the mark and its select fires, while a press-drag still pans
115
+ through and the tracker still hides once the pan commits. Resolves the
116
+ browser-dependent finding deferred from #308; adds
117
+ `e2e/annotations-panzoom.spec.ts`, the first real-pointer-event behavior e2e for
118
+ the annotation layer. (#309)
92
119
 
93
120
  ## [0.38.0] — 2026-07-03
94
121
 
@@ -112,7 +112,19 @@ export declare class TimeSeries<S extends SeriesSchema> {
112
112
  * the fast path): the resulting series' columns alias the caller's buffers.
113
113
  * Mutating an adopted buffer after construction mutates the series. Pass a
114
114
  * fresh buffer (e.g. straight off a decode) if this matters; `number[]`
115
- * columns are always copied.
115
+ * columns are always copied. (**`sort` disables the adoption** — a reorder
116
+ * needs its own buffers, so every column is copied when sorting.)
117
+ *
118
+ * **Ordering.** Keys must be **non-decreasing** — bisect-based operators (crop,
119
+ * `atTime`, range queries) rely on it — so an out-of-order columnar input
120
+ * throws by default (a backwards key on the *trusted* fast door is a corruption
121
+ * signal, not something to silently accept). Pass **`sort: true`** to sort the
122
+ * rows by key before construction instead — the columnar analog of
123
+ * {@link TimeSeries.fromJSON}'s `sort`, paying `O(n log n)` + a copy only when
124
+ * asked. The sort is **stable**, so rows sharing a key keep their input order.
125
+ * (To flatten a genuine backwards blip to a plateau — a different, lossy choice
126
+ * — clamp the key column yourself before ingest; the library won't mutate data
127
+ * for you.)
116
128
  *
117
129
  * **v1 scope:** a `time`-kind key + `number` value columns (the market-data /
118
130
  * chart wire case). Other key kinds (`interval` / `timeRange`) and non-numeric
@@ -120,12 +132,20 @@ export declare class TimeSeries<S extends SeriesSchema> {
120
132
  *
121
133
  * @throws ValidationError on a missing column, a length mismatch, an
122
134
  * unsupported kind, a non-finite timestamp key, or an out-of-order
123
- * (decreasing) timestamp — keys must be non-decreasing, same as `fromJSON`.
135
+ * (decreasing) timestamp when `sort` is not set — keys must be non-decreasing,
136
+ * same as `fromJSON`.
124
137
  */
125
138
  static fromColumns<S extends SeriesSchema>(input: {
126
139
  name: string;
127
140
  schema: S;
128
141
  columns: Record<string, ReadonlyArray<number | null | undefined> | Float64Array>;
142
+ /**
143
+ * Sort the rows by key before construction (off by default), for a columnar
144
+ * payload whose rows aren't guaranteed ordered — the counterpart of
145
+ * `fromJSON`'s `sort`. Stable; disables the `Float64Array` zero-copy adoption
146
+ * (columns are reordered into fresh buffers).
147
+ */
148
+ sort?: boolean;
129
149
  }): TimeSeries<S>;
130
150
  /**
131
151
  * Example: `TimeSeries.fromEvents(events, { schema, name })`.
@@ -511,7 +511,19 @@ export class TimeSeries {
511
511
  * the fast path): the resulting series' columns alias the caller's buffers.
512
512
  * Mutating an adopted buffer after construction mutates the series. Pass a
513
513
  * fresh buffer (e.g. straight off a decode) if this matters; `number[]`
514
- * columns are always copied.
514
+ * columns are always copied. (**`sort` disables the adoption** — a reorder
515
+ * needs its own buffers, so every column is copied when sorting.)
516
+ *
517
+ * **Ordering.** Keys must be **non-decreasing** — bisect-based operators (crop,
518
+ * `atTime`, range queries) rely on it — so an out-of-order columnar input
519
+ * throws by default (a backwards key on the *trusted* fast door is a corruption
520
+ * signal, not something to silently accept). Pass **`sort: true`** to sort the
521
+ * rows by key before construction instead — the columnar analog of
522
+ * {@link TimeSeries.fromJSON}'s `sort`, paying `O(n log n)` + a copy only when
523
+ * asked. The sort is **stable**, so rows sharing a key keep their input order.
524
+ * (To flatten a genuine backwards blip to a plateau — a different, lossy choice
525
+ * — clamp the key column yourself before ingest; the library won't mutate data
526
+ * for you.)
515
527
  *
516
528
  * **v1 scope:** a `time`-kind key + `number` value columns (the market-data /
517
529
  * chart wire case). Other key kinds (`interval` / `timeRange`) and non-numeric
@@ -519,10 +531,11 @@ export class TimeSeries {
519
531
  *
520
532
  * @throws ValidationError on a missing column, a length mismatch, an
521
533
  * unsupported kind, a non-finite timestamp key, or an out-of-order
522
- * (decreasing) timestamp — keys must be non-decreasing, same as `fromJSON`.
534
+ * (decreasing) timestamp when `sort` is not set — keys must be non-decreasing,
535
+ * same as `fromJSON`.
523
536
  */
524
537
  static fromColumns(input) {
525
- const { name, schema, columns } = input;
538
+ const { name, schema, columns, sort = false } = input;
526
539
  // Key column (schema[0]). v1: point-in-time (`time`) keys only.
527
540
  const keyDef = schema[0];
528
541
  if (keyDef === undefined) {
@@ -540,18 +553,38 @@ export class TimeSeries {
540
553
  // generic iterable-protocol path even for a plain array, ~15-20x slower
541
554
  // than a preallocated-buffer copy at 100k-element scale — measured, not
542
555
  // theoretical (see the pond-columnar-ingest spike's ingest regression).
543
- let begin;
556
+ let rawBegin;
544
557
  if (keyRaw instanceof Float64Array) {
545
- begin = keyRaw;
558
+ rawBegin = keyRaw;
546
559
  }
547
560
  else {
548
- begin = new Float64Array(keyRaw.length);
561
+ rawBegin = new Float64Array(keyRaw.length);
549
562
  for (let j = 0; j < keyRaw.length; j += 1) {
550
563
  const v = keyRaw[j];
551
- begin[j] = v == null ? NaN : Number(v);
564
+ rawBegin[j] = v == null ? NaN : Number(v);
552
565
  }
553
566
  }
554
- const count = begin.length;
567
+ const count = rawBegin.length;
568
+ // `sort: true` — reorder every column by ascending key before construction.
569
+ // Compute the row permutation once (a stable sort of the index array; V8's
570
+ // Array.sort is stable, so equal keys keep input order, matching fromJSON's
571
+ // stable intake), then remap the key + each value column through it below.
572
+ // `order` stays null on the (default) trusted fast path, so no allocation /
573
+ // copy is paid unless asked. A non-finite key is left for `TimeKeyColumn` to
574
+ // reject — sorting can't make it valid.
575
+ let begin;
576
+ let order = null;
577
+ if (sort) {
578
+ const idx = Array.from({ length: count }, (_, i) => i);
579
+ idx.sort((a, b) => rawBegin[a] - rawBegin[b]);
580
+ order = Uint32Array.from(idx);
581
+ begin = new Float64Array(count);
582
+ for (let j = 0; j < count; j += 1)
583
+ begin[j] = rawBegin[order[j]];
584
+ }
585
+ else {
586
+ begin = rawBegin;
587
+ }
555
588
  // Throws on any non-finite timestamp.
556
589
  const keys = new TimeKeyColumn(begin, count);
557
590
  // Enforce the non-decreasing-key invariant that `fromJSON`'s
@@ -560,11 +593,13 @@ export class TimeSeries {
560
593
  // bisect-based operators (crop, `atTime`, range queries) rely on it, so an
561
594
  // unsorted columnar input must fail loudly here rather than build a silently
562
595
  // broken series. One O(N) scan over already-finite values — negligible next
563
- // to decode.
596
+ // to decode. (When `sort` is set the keys are now non-decreasing, so this is
597
+ // a cheap post-condition check rather than a rejection.)
564
598
  for (let j = 1; j < count; j += 1) {
565
599
  if (begin[j] < begin[j - 1]) {
566
600
  throw new ValidationError(`fromColumns: key column '${keyDef.name}' is out of order at index ${j} ` +
567
- `(${begin[j]} < ${begin[j - 1]}) — timestamps must be non-decreasing; pre-sort the columns`);
601
+ `(${begin[j]} < ${begin[j - 1]}) — timestamps must be non-decreasing; ` +
602
+ `pass { sort: true } or pre-sort the columns`);
568
603
  }
569
604
  }
570
605
  // Value columns — packed directly (missing-aware) from the arrays.
@@ -594,7 +629,23 @@ export class TimeSeries {
594
629
  // Manual loop, not `Float64Array.from(arr, mapFn)` — see the key-column
595
630
  // comment above; the cost applies identically here.
596
631
  let values;
597
- if (raw instanceof Float64Array) {
632
+ if (order !== null) {
633
+ // Sorting: reorder into a fresh buffer through the key permutation (no
634
+ // zero-copy adoption — the rows are being moved). Same missing rule
635
+ // (`null`/`undefined` → NaN) applied while remapping.
636
+ values = new Float64Array(count);
637
+ if (raw instanceof Float64Array) {
638
+ for (let j = 0; j < count; j += 1)
639
+ values[j] = raw[order[j]];
640
+ }
641
+ else {
642
+ for (let j = 0; j < count; j += 1) {
643
+ const v = raw[order[j]];
644
+ values[j] = v == null ? NaN : v;
645
+ }
646
+ }
647
+ }
648
+ else if (raw instanceof Float64Array) {
598
649
  values = raw;
599
650
  }
600
651
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pond-ts",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "TypeScript-first time series primitives",
5
5
  "license": "MIT",
6
6
  "repository": {