@pond-ts/react 0.51.0 → 0.53.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 +161 -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.51.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.53.0...HEAD
12
+ [0.53.0]: https://github.com/pond-ts/pond/compare/v0.52.0...v0.53.0
13
+ [0.52.0]: https://github.com/pond-ts/pond/compare/v0.51.0...v0.52.0
12
14
  [0.51.0]: https://github.com/pond-ts/pond/compare/v0.50.0...v0.51.0
13
15
  [0.50.0]: https://github.com/pond-ts/pond/compare/v0.49.0...v0.50.0
14
16
  [0.49.0]: https://github.com/pond-ts/pond/compare/v0.48.1...v0.49.0
@@ -51,6 +53,164 @@ and type-level changes; patch bumps are strictly additive.
51
53
 
52
54
  ## [Unreleased]
53
55
 
56
+ ## [0.53.0] — 2026-07-25
57
+
58
+ ### Changed
59
+
60
+ - **fit (breaking):** **Power bins and zones now use pond's canonical bin
61
+ edges**, so they feed `@pond-ts/charts` with no mapping step:
62
+
63
+ ```tsx
64
+ <BarChart bins={power.distribution} column="seconds" />
65
+ <BarChart bins={power.zones} column="seconds" orientation="horizontal" ordinal />
66
+ ```
67
+
68
+ Each type previously spoke its own dialect for the same concept —
69
+ `PowerBin.wattsFrom` (with **no upper edge at all**), `ZoneTime.lo`/`hi`, and
70
+ `PowerZone.minWatts`/`maxWatts` — while core's `byColumn` and charts' `BinRecord`
71
+ both use `{ start, end, …aggregates }`. Every caller had to hand-map before
72
+ drawing, even though the internals already computed the canonical shape and
73
+ discarded it.
74
+
75
+ **Migration** (pre-1.0, so the old names are gone rather than deprecated):
76
+
77
+ | Was | Now |
78
+ | --------------------- | -------------------------------------- |
79
+ | `PowerBin.wattsFrom` | `PowerBin.start` (+ new `end`) |
80
+ | `ZoneTime.lo` / `.hi` | `ZoneTime.start` / `.end`, `openEnded` |
81
+ | `PowerZone.minWatts` | `PowerZone.start` |
82
+ | `PowerZone.maxWatts` | `PowerZone.end`, `openEnded` |
83
+
84
+ Only **`PowerZone.maxWatts`** — a zone's upper edge — is affected. The
85
+ identically-named `PowerSummary.maxWatts` and the per-lap / per-section peak
86
+ power are a different concept and are unchanged.
87
+
88
+ `end` is now **always finite and always `> start`** — the guarantee core
89
+ enforces (`byColumn` throws on a zero-width bin) and charts need (an infinite
90
+ edge blows up an axis domain). The open-ended top band, which previously
91
+ carried only `Infinity`, gets a **drawable stand-in** edge: wide enough to
92
+ cover the highest value observed, and at least as wide as the band below it.
93
+ Treat it as a drawing bound rather than data, and test for the band with the
94
+ new **`openEnded`** flag rather than comparing an edge against `Infinity`
95
+ (`openEnded` is now also strictly positional — only the final band can carry
96
+ it). Rounding zone edges to whole watts no longer collapses bands at very low
97
+ FTPs.
98
+
99
+ ### Added
100
+
101
+ - **charts:** **Duration (elapsed) x axis** — `<ChartContainer origin>` labels
102
+ the shared x axis as offsets from a zero point instead of absolute values, so
103
+ a workout / lab run / load test reads `00:00 00:05 00:10` rather than
104
+ `10:35 10:40 10:45`:
105
+
106
+ ```tsx
107
+ <ChartContainer width={620} origin="data">
108
+
109
+ <XAxis label="Elapsed" />
110
+ </ChartContainer>
111
+ ```
112
+
113
+ `'data'` zeroes at the start of the data (and stays there as you pan); a
114
+ **number** sets an explicit zero point — a gun, a trigger, a lap — with ticks
115
+ before it reading negative (`-00:05`). Ticks are placed at round durations
116
+ **measured from the origin** (a ride starting at 10:33:17 ticks 10:33:17,
117
+ 10:38:17, …), off a clock ladder (…15s, 30s, 1m, 2m, 5m, …, 12h, then whole
118
+ days) rather than the 1-2-5 ladder — the part a formatter alone can't do.
119
+ Labels pick their shape from the step and the axis's magnitude
120
+ (`00:00.500` · `00:15` · `01:01:30` · `1d 12:00` · `5d`), gridlines follow the
121
+ same ticks, and the cursor / marker pills read one grain finer (`00:05:12`).
122
+
123
+ It's a **labelling** mode, not a data transform: `range`, `<Marker at>`,
124
+ `onRegionSelect`, `trackerPosition` all stay in absolute axis units. The same
125
+ prop works on a **value** x axis (distance travelled, not distance recorded).
126
+ An explicit format still wins — on a time axis a d3 _time_ specifier can only
127
+ describe an instant, so it labels the wall clock, which is the lever for
128
+ stacking a wall-clock strip under a duration strip on one shared tick set; on
129
+ a value axis a number specifier formats the offset. Ignored on a category
130
+ axis; on a trading calendar the durations are wall-clock, so ticks spanning a
131
+ collapsed session gap sit unevenly.
132
+
133
+ - **fit:** `computePower` takes an options object — **`{ binWatts }`** sets the
134
+ width of the `distribution` buckets (default `1`, unchanged). 1 W bins draw as
135
+ hairlines, so pass the width you intend to render rather than re-bucketing the
136
+ output yourself. It throws `RangeError` on a non-positive or non-finite
137
+ `binWatts`. New exported type `ComputePowerOptions`, also accepted by the
138
+ activity façade: `Activity.power(ftp, options)` and
139
+ `ProfiledActivity.power(options)`.
140
+
141
+ ## [0.52.0] — 2026-07-23
142
+
143
+ ### Changed
144
+
145
+ - **core / financial:** **Market-scale studies are now typed-array fast**
146
+ (the "SMA/EMA at 1M bars costs hundreds of ms" report). Three cuts along
147
+ the same path, all behaviour-preserving (identical values, warm-ups,
148
+ missing-cell semantics, and rejection errors; every fast path falls back
149
+ to the original sweep when it doesn't apply):
150
+ - **`smooth('ema')` columnar fast path** — on a packed numeric source
151
+ column the EMA recurrence runs straight off the typed buffer into a
152
+ typed result column via trusted construction (key + untouched columns
153
+ pass through zero-copy), replacing the per-row Event/tuple rebuild +
154
+ full-series intake re-pack. 1M rows: **530 ms → 4.4 ms (~120×)**.
155
+ - **`rolling({ count })` numeric fast path** — an all-built-in numeric
156
+ mapping over packed sources feeds the shared incremental reducer states
157
+ directly from the typed buffers and writes snapshots into typed columns
158
+ (no per-row snapshot arrays, no boxed accumulators, no post-pass
159
+ assert/re-pack). 1M rows, `avg`: **135 ms → 32 ms (~4×)**.
160
+ - **financial kernel reads columns, not events** — `rollingColumns` /
161
+ `columnValues` now read study inputs/outputs off the public column API
162
+ instead of materializing `series.events` (an Event + data object per
163
+ row, ~400 ms of pure overhead at 1M rows).
164
+ - End-to-end at 1M bars: `ema()` **603 ms → 2.5 ms (~240×)**, `sma()`
165
+ **569 ms → 56 ms (~10×)**, `bollinger()` **748 ms → 162 ms (~4.6×)**.
166
+ Durable benchmarks: `packages/core/scripts/perf-smooth-ema.mjs`,
167
+ `packages/financial/scripts/perf-studies.mjs`.
168
+
169
+ ### Added
170
+
171
+ - **core:** **`TimeSeries.fromArrow(table, options?)` — ingest a decoded Apache
172
+ Arrow `Table`.** pond stays zero-dependency: bring your own Arrow
173
+ (`tableFromIPC(...)`) and hand the `Table` in; the input is duck-typed against
174
+ a small structural surface (`ArrowTableLike` / `ArrowVectorLike` / …, all
175
+ exported). Ingest is the zero-copy path — every `Float64` column's backing
176
+ `Float64Array` is adopted as-is (`Float32`/int columns convert; int64 value
177
+ columns recombine BigInt-free), and the schema is derived from the Arrow
178
+ fields. The time key is converted **BigInt-free**: Arrow's idiomatic int64
179
+ timestamps are recombined from their two int32 halves rather than
180
+ `Number(bigint)` per row — measured **~11× faster** on the time column (0.6ms
181
+ vs 6.8ms at 500k rows; `scripts/perf-from-arrow.mjs`). Options: `time` (key
182
+ column, default the `'time'` field), `timeUnit` (default read from the Arrow
183
+ Arrow type family — a `Timestamp`'s raw-unit int64 is scaled by its
184
+ `TimeUnit`; `Date32`/`Date64` arrive already normalized to epoch-ms and pass
185
+ through; overridable), `columns` (subset, in order), `name`, `sort`. Numeric
186
+ **and string** columns are supported — string columns (Arrow
187
+ `Utf8`) become dict-encoded `StringColumn`s; any other Arrow type
188
+ (list/struct) throws, naming it. A null time key throws; numeric nulls map to
189
+ `NaN` and string nulls to missing.
190
+ - **core:** **`TimeSeries.fromColumns` / `ValueSeries.fromColumns` now accept
191
+ `string` value columns** (previously numeric-only), packed to dict-encoded
192
+ `StringColumn`s (`null`/`undefined` → missing) — the shared columnar-ingress
193
+ engine now dispatches on the schema kind. Other value kinds (`boolean`,
194
+ arrays) still throw.
195
+ - **charts:** **`<ScatterChart decimate>` — dense scatter plots now decimate**
196
+ (PND-MARKDEC scatter half — the last un-decimated mark type). **Default
197
+ `true`.** When the marks are **uniform** (fixed size + colour, no data-driven
198
+ `radius`/`color`), **opaque**, and denser than the pixel grid, overlapping
199
+ marks collapse to one representative per **mark-radius cell** via a 2D
200
+ pixel-**occupancy** sweep. Scatter has no fill, so a line/bar's per-column
201
+ `[min, max]` envelope would erase interior points — the occupancy grid keeps
202
+ one mark per occupied cell instead, which is **visually lossless** for uniform
203
+ opaque marks at that density (same-cell marks overlap). Interaction (hover /
204
+ click / tracker) still reads **every source point**; the per-point selection
205
+ ring + labels are suppressed only on the decimated (dense) path. A
206
+ **translucent** fill (density-encoded — overlap _should_ build up) or a
207
+ data-driven size/colour keeps the full draw. `decimate={false}` draws every
208
+ mark; `{ threshold }` tunes the trigger. The occupancy sweep uses the affine
209
+ fast path for the per-point pixel mapping. Measured (SciChart-suite
210
+ point-update, real browser): **100k 18 → 73 fps (4×)**, and the ladder now
211
+ runs to **10M** points (previously dead by 1M). `drawScatter` now returns
212
+ `LayerDrawStats` (visible via `onDrawStats`).
213
+
54
214
  ## [0.51.0] — 2026-07-22
55
215
 
56
216
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.51.0",
3
+ "version": "0.53.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.51.0",
36
+ "pond-ts": "^0.53.0",
37
37
  "react": "^18.0.0 || ^19.0.0"
38
38
  },
39
39
  "devDependencies": {