@pond-ts/react 0.50.0 → 0.52.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.
Files changed (2) hide show
  1. package/CHANGELOG.md +187 -1
  2. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  tag, so this file covers them all. Pre-1.0: minor bumps may include new features
9
9
  and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pond-ts/pond/compare/v0.50.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.52.0...HEAD
12
+ [0.52.0]: https://github.com/pond-ts/pond/compare/v0.51.0...v0.52.0
13
+ [0.51.0]: https://github.com/pond-ts/pond/compare/v0.50.0...v0.51.0
12
14
  [0.50.0]: https://github.com/pond-ts/pond/compare/v0.49.0...v0.50.0
13
15
  [0.49.0]: https://github.com/pond-ts/pond/compare/v0.48.1...v0.49.0
14
16
  [0.48.1]: https://github.com/pond-ts/pond/compare/v0.48.0...v0.48.1
@@ -50,6 +52,190 @@ and type-level changes; patch bumps are strictly additive.
50
52
 
51
53
  ## [Unreleased]
52
54
 
55
+ ## [0.52.0] — 2026-07-23
56
+
57
+ ### Changed
58
+
59
+ - **core / financial:** **Market-scale studies are now typed-array fast**
60
+ (the "SMA/EMA at 1M bars costs hundreds of ms" report). Three cuts along
61
+ the same path, all behaviour-preserving (identical values, warm-ups,
62
+ missing-cell semantics, and rejection errors; every fast path falls back
63
+ to the original sweep when it doesn't apply):
64
+ - **`smooth('ema')` columnar fast path** — on a packed numeric source
65
+ column the EMA recurrence runs straight off the typed buffer into a
66
+ typed result column via trusted construction (key + untouched columns
67
+ pass through zero-copy), replacing the per-row Event/tuple rebuild +
68
+ full-series intake re-pack. 1M rows: **530 ms → 4.4 ms (~120×)**.
69
+ - **`rolling({ count })` numeric fast path** — an all-built-in numeric
70
+ mapping over packed sources feeds the shared incremental reducer states
71
+ directly from the typed buffers and writes snapshots into typed columns
72
+ (no per-row snapshot arrays, no boxed accumulators, no post-pass
73
+ assert/re-pack). 1M rows, `avg`: **135 ms → 32 ms (~4×)**.
74
+ - **financial kernel reads columns, not events** — `rollingColumns` /
75
+ `columnValues` now read study inputs/outputs off the public column API
76
+ instead of materializing `series.events` (an Event + data object per
77
+ row, ~400 ms of pure overhead at 1M rows).
78
+ - End-to-end at 1M bars: `ema()` **603 ms → 2.5 ms (~240×)**, `sma()`
79
+ **569 ms → 56 ms (~10×)**, `bollinger()` **748 ms → 162 ms (~4.6×)**.
80
+ Durable benchmarks: `packages/core/scripts/perf-smooth-ema.mjs`,
81
+ `packages/financial/scripts/perf-studies.mjs`.
82
+
83
+ ### Added
84
+
85
+ - **core:** **`TimeSeries.fromArrow(table, options?)` — ingest a decoded Apache
86
+ Arrow `Table`.** pond stays zero-dependency: bring your own Arrow
87
+ (`tableFromIPC(...)`) and hand the `Table` in; the input is duck-typed against
88
+ a small structural surface (`ArrowTableLike` / `ArrowVectorLike` / …, all
89
+ exported). Ingest is the zero-copy path — every `Float64` column's backing
90
+ `Float64Array` is adopted as-is (`Float32`/int columns convert; int64 value
91
+ columns recombine BigInt-free), and the schema is derived from the Arrow
92
+ fields. The time key is converted **BigInt-free**: Arrow's idiomatic int64
93
+ timestamps are recombined from their two int32 halves rather than
94
+ `Number(bigint)` per row — measured **~11× faster** on the time column (0.6ms
95
+ vs 6.8ms at 500k rows; `scripts/perf-from-arrow.mjs`). Options: `time` (key
96
+ column, default the `'time'` field), `timeUnit` (default read from the Arrow
97
+ Arrow type family — a `Timestamp`'s raw-unit int64 is scaled by its
98
+ `TimeUnit`; `Date32`/`Date64` arrive already normalized to epoch-ms and pass
99
+ through; overridable), `columns` (subset, in order), `name`, `sort`. Numeric
100
+ **and string** columns are supported — string columns (Arrow
101
+ `Utf8`) become dict-encoded `StringColumn`s; any other Arrow type
102
+ (list/struct) throws, naming it. A null time key throws; numeric nulls map to
103
+ `NaN` and string nulls to missing.
104
+ - **core:** **`TimeSeries.fromColumns` / `ValueSeries.fromColumns` now accept
105
+ `string` value columns** (previously numeric-only), packed to dict-encoded
106
+ `StringColumn`s (`null`/`undefined` → missing) — the shared columnar-ingress
107
+ engine now dispatches on the schema kind. Other value kinds (`boolean`,
108
+ arrays) still throw.
109
+ - **charts:** **`<ScatterChart decimate>` — dense scatter plots now decimate**
110
+ (PND-MARKDEC scatter half — the last un-decimated mark type). **Default
111
+ `true`.** When the marks are **uniform** (fixed size + colour, no data-driven
112
+ `radius`/`color`), **opaque**, and denser than the pixel grid, overlapping
113
+ marks collapse to one representative per **mark-radius cell** via a 2D
114
+ pixel-**occupancy** sweep. Scatter has no fill, so a line/bar's per-column
115
+ `[min, max]` envelope would erase interior points — the occupancy grid keeps
116
+ one mark per occupied cell instead, which is **visually lossless** for uniform
117
+ opaque marks at that density (same-cell marks overlap). Interaction (hover /
118
+ click / tracker) still reads **every source point**; the per-point selection
119
+ ring + labels are suppressed only on the decimated (dense) path. A
120
+ **translucent** fill (density-encoded — overlap _should_ build up) or a
121
+ data-driven size/colour keeps the full draw. `decimate={false}` draws every
122
+ mark; `{ threshold }` tunes the trigger. The occupancy sweep uses the affine
123
+ fast path for the per-point pixel mapping. Measured (SciChart-suite
124
+ point-update, real browser): **100k 18 → 73 fps (4×)**, and the ladder now
125
+ runs to **10M** points (previously dead by 1M). `drawScatter` now returns
126
+ `LayerDrawStats` (visible via `onDrawStats`).
127
+
128
+ ## [0.51.0] — 2026-07-22
129
+
130
+ ### Changed
131
+
132
+ - **charts:** **`trackerPosition` is now a _followed_ position, not a hard pin —
133
+ enabling cross-chart cursor sync.** A live local hover wins over
134
+ `trackerPosition`, so the chart under the pointer shows its own cursor while
135
+ any chart without a local pointer follows the controlled time (mapped through
136
+ its own `xScale`, so it's correct across different zooms). This makes
137
+ **multi-chart dashboard cursor sync** fall out of the plain props: give every
138
+ `<ChartContainer>` the same `trackerPosition={sharedTime}` and set `sharedTime`
139
+ from each one's `onTrackerChanged` (clear it to `null` on the group's
140
+ `onPointerLeave`) — no "which chart is active" bookkeeping. **Behaviour
141
+ change:** previously a numeric `trackerPosition` overrode local hover, and
142
+ `trackerPosition={null}` force-hid the cursor; now `null` and `undefined` are
143
+ equivalent ("no controlled position") and a hovered chart always tracks its
144
+ pointer. To force a chart to never show a cursor, use `cursor="none"`. See the
145
+ "Synced cursors across charts" story. No type change (`number | null`).
146
+ - **charts:** **Line / area draw is ~3× faster on stroke-bound frames**
147
+ (PND-AFFINE / PND-GRADX; 2026-07 external-bench profile). When the curve is
148
+ linear and both scales are affine (every y axis is `scaleLinear`; x is
149
+ `scaleLinear` / `scaleTime` / the gap-free default time axis), `drawLine` and
150
+ `drawArea` now map points with an inline `k·v + b` over the typed arrays
151
+ instead of a per-point d3-scale closure + d3-shape generator — a **visually
152
+ identical** draw (guarded by the decimation pixel-identity and per-layer
153
+ visual-regression e2e). A real-gap trading-time axis, or a non-linear curve,
154
+ transparently keeps the exact d3 path. Measured on a JS-only micro-bench
155
+ (`scripts/perf-affine.mjs`): line 1M 60.4 → 19.0 ms (3.2×), area 1M 132 →
156
+ 38 ms (3.5×). Separately, the area fill gradient's full-series value extent is
157
+ now memoized per column buffer, so a y-zoom / pan repaint no longer re-walks
158
+ the whole series to find the gradient span. No API change.
159
+ - **charts:** **A y-zoom / y-autorange repaint no longer re-decimates line /
160
+ area layers** (PND-DECKEY; same 2026-07 profile, finding 3). The M4
161
+ decimation output is a pure function of the source data, x-domain, device
162
+ width, threshold, and session breaks — it never reads the y-scale — so it is
163
+ identical across every y-only frame. `drawLine` / `drawArea` now memoize the
164
+ cull+decimate result per source series (one entry, keyed on the x-scale
165
+ object + width + threshold + breaks), so a y-zoom / live y-autorange frame
166
+ reuses the prior polyline instead of re-binning O(N) points; a pan / x-zoom
167
+ mints a fresh x-scale and correctly recomputes. Measured
168
+ (`scripts/perf-deckey.mjs`): the ~5 ms/frame decimation walk at 1M points is
169
+ eliminated on every y-only frame. No API change.
170
+
171
+ ### Added
172
+
173
+ - **charts:** **`<BarChart decimate>` — dense column charts now decimate**
174
+ (PND-MARKDEC; 2026-07 profile, finding 4 — "column dead by 5M"). **Default
175
+ `true`**: once the visible **single-series** bars are denser than ~2 per device
176
+ pixel (each slot < ~1px), they're drawn as one per-column **envelope** rect —
177
+ the exact painted union `[min(value, baseline), max(value, baseline)]` of each
178
+ pixel column — instead of every bar, so a 100k–5M-bar column chart stays
179
+ interactive. **Visually lossless** at that density (a perf knob, not a style);
180
+ interaction still reads the source bars (`barAt`), and the per-bar
181
+ selection/hover highlight is suppressed only when decimated (a <1px bar's ring
182
+ isn't visible anyway). `decimate={false}` draws every bar; `{ threshold }`
183
+ tunes the samples-per-pixel factor. No-op for a stacked / multi-group
184
+ histogram (the low-count categorical path). `drawBars` now returns
185
+ `LayerDrawStats` (visible via `onDrawStats`). Measured
186
+ (`scripts/perf-markdec.mjs`, JS-only): the bar draw at 5M points drops
187
+ 485 → 26 ms (18.9×), 100k drops 9.7 → 0.9 ms (10.7×) — with the larger
188
+ rasterization win on top, browser-side.
189
+ - **charts:** **`panZoom` is now a three-way mode + a `bounds` extent.**
190
+ `<ChartContainer panZoom>` takes `'none'` / `'pan'` / `'panZoom'` (drag-only
191
+ vs. drag+wheel), with the old boolean kept as shorthand (`true` ⇒ `'panZoom'`,
192
+ `false` ⇒ `'none'`) — so existing charts are unchanged. A new `bounds`
193
+ (`[min, max]`) prop fences pan/zoom to an **outer** extent (panning into an
194
+ edge stops there keeping its span; zoom-out is capped at the whole span), the
195
+ companion
196
+ to the existing `minDuration` zoom-in floor — together they pin the reachable
197
+ window between an inner and outer bound. On a trading-time axis `bounds`
198
+ clamps in wall-clock ms. Purely additive; no type narrowing.
199
+ - **charts:** **Draw-cost + decimation observability** — `<ChartContainer
200
+ onDrawStats>` (PND-DECOBS; dashboard A/B friction, 2026-07-21). Fires a
201
+ `DrawStatsFrame` once per row-canvas repaint (keyed by an opaque `rowKey` for
202
+ multi-row attribution), one `LayerDrawInfo` per layer carrying its `as`,
203
+ measured `drawMs`, and — for a decimating layer (line / area / band / candle /
204
+ box) — `sourceCount` / `drawnCount` / `decimated`.
205
+ Compare `drawnCount` to `sourceCount` to see whether M4 engaged; read `drawMs`
206
+ for per-layer render cost. **Zero-overhead when unused** — the render loop
207
+ skips per-layer timing entirely unless a consumer subscribes. New exports:
208
+ `DrawStatsFrame`, `LayerDrawInfo`.
209
+
210
+ ### Fixed
211
+
212
+ - **charts:** hovering no longer repaints the row data canvas on every cursor
213
+ mousemove. The container frame minted a fresh `timeRange` array identity per
214
+ rebuild (and the frame rebuilds per cursor move), which the Layers draw
215
+ callback — depending on `container.timeRange` — read as a domain change:
216
+ each hover frame re-fired the canvas draw effect, including per-layer M4
217
+ re-decimation (measured 105 repaints per 122 mousemove events; with
218
+ `decimate` off, hover fell to ~10 fps). The tuple is now identity-stable on
219
+ its endpoints, restoring the SVG-overlay cursor contract (0 repaints, full
220
+ frame rate while hovering). Found running uPlot's bench protocol against
221
+ pond-charts; guarded by a new hover-sweep perf invariant in
222
+ `e2e/perf-invariants.spec.ts`.
223
+ - **charts:** hovering no longer re-renders cursor-independent components
224
+ (both `YAxis`, `Bar`/`Box`). The cursor position was a `ContainerFrame`
225
+ field, so every mousemove re-identified the whole (~50-field) frame and
226
+ re-rendered **all** its context consumers — even ones that never read the
227
+ cursor. The per-move cursor state (`cursorX`/`cursorY`/`cursorRowKey`) now
228
+ lives in a dedicated `CursorContext`; the frame stays identity-stable across
229
+ a hover, so only the genuine cursor consumers (the `Layers` overlay,
230
+ `XAxis` crosshair pill, `Legend` values) re-render. Measured: 4 → 2 React
231
+ commits per mousemove, ~25% less hover script time on the uPlot-bench
232
+ workload (the win scales with axis/row count). No API change — the split
233
+ types are internal (`PND-HOVCTX`, follow-up to the repaint fix above).
234
+ - **charts:** corrected the `@pond-ts/charts` package-header doc comment, which
235
+ described a "chunked Path2D cache" render stage that was explored and
236
+ **deferred**, never built (it doesn't help the pan case, which re-decimates
237
+ every frame). The stale comment had misled a consumer's perf investigation.
238
+
53
239
  ## [0.50.0] — 2026-07-21
54
240
 
55
241
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.50.0",
3
+ "version": "0.52.0",
4
4
  "description": "React hooks for pond-ts live time series",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "test:runtime": "vitest run"
34
34
  },
35
35
  "peerDependencies": {
36
- "pond-ts": "^0.50.0",
36
+ "pond-ts": "^0.52.0",
37
37
  "react": "^18.0.0 || ^19.0.0"
38
38
  },
39
39
  "devDependencies": {