@pond-ts/charts 0.72.0 → 0.74.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/API.md CHANGED
@@ -118,7 +118,8 @@ Value-axis wire types
118
118
  - **Temporal range**: `timeRange()`, `overlaps()`, `contains()`,
119
119
  `intersection()`, `overlapping(range)`, `containedBy(range)`, `trim(range)`,
120
120
  `after()`, `before()`, `within()`, `tail(duration)`
121
- - **Key-type conversion**: `asTime({ at })`, `asTimeRange()`, `asInterval()`
121
+ - **Key-type conversion**: `asTime({ at })`, `asTimeRange()`, `asInterval()`;
122
+ `offsetTime(by)` moves every key by a constant (kind kept)
122
123
  - **Filter/slice**: `filter()`, `sample(strategy)`, `slice(begin, end)`
123
124
  - **Column reshape**: `select()`, `rename()`, `map()`, `mapColumns()`,
124
125
  `withColumn()`, `collapse()`
@@ -161,8 +162,8 @@ Deliberately small — the ordering-based slice of the algebra, no calendar ops
161
162
 
162
163
  | Type group | Names | Source |
163
164
  | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
164
- | Schema contract | `SeriesSchema`, `RowForSchema`, `EventForSchema`, `EventDataForSchema`, `EventKeyForSchema`, `TimeSeriesInput`, `TimeSeriesJsonInput` | `packages/core/src/schema/index.ts` |
165
- | Aggregation specs | `AggregateReducer`, `AggregateMap`, `AggregateOutputMap`, `AggregateSchema`, `BinReducerName`, `BinOutput` | `packages/core/src/schema/index.ts`, `packages/core/src/column.ts` |
165
+ | Schema contract | `SeriesSchema`, `RowForSchema`, `EventForSchema`, `EventDataForSchema`, `EventKeyForSchema`, `TimeSeriesInput`, `TimeSeriesJsonInput`, `BatchOrderingMode` | `packages/core/src/schema/index.ts` |
166
+ | Aggregation specs | `AggregateReducer`, `AggregateMap`, `AggregateSchema`, `BinReducerName`, `BinOutput` | `packages/core/src/schema/index.ts`, `packages/core/src/column.ts` |
166
167
  | Operation schemas | `RollingSchema`, `RollingAlignment`, `AlignSchema`, `DiffSchema`, `SmoothSchema`, `SmoothMethod`, `FillStrategy`, `FillMapping` | `packages/core/src/schema/index.ts` |
167
168
  | Column/data kinds | `Column`, `KeyColumn`, `ColumnKind`, `ScalarKind`, `ScalarValue`, `ColumnValue`, `ArrayValue`, `ValidityBitmap` | `packages/core/src/columnar/` |
168
169
  | JSON wire format | `JsonRowFormat`, `JsonRowForSchema`, `JsonObjectRowForSchema`, `JsonValueForKind`, `JsonTimestampInput`, `JsonTimeRangeInput`, `JsonIntervalInput` | `packages/core/src/schema/index.ts` |
@@ -723,7 +724,7 @@ the default for pipelines known at authoring time — see the package README.
723
724
  | Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
724
725
  | Sources | `source<T>()` → `SourceNode` (`set()`), `fromLive(liveSource)` → `LiveSourceNode` (`dispose()`); `GraphSource` (bind contract — looser than core's `LiveSource`, accepts `LiveAggregation`), `SnapshotSource`, `NoInputs` | `packages/process/src/source.ts` |
725
726
  | Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
726
- | Range output buffers | `prepareRange(length, keep, prior)` → `RangeOutput` (`values`, `bits`, `set`, `clear`) carrying `[0, keep)` forward as blocks — values **and** validity; `sealRange(out, length)` → `Float64Column`; `validityByteCount`. Reached from an op as `ctx.out[n]` | `packages/process/src/column.ts` |
727
+ | Range output buffers | `prepareRange(length, keep, prior)` → `RangeOutput` (`values`, `set`, `clear`; a gap is `NaN` in `values`) carrying `[0, keep)` forward as one block copy; `sealRange(out, length)` → `Float64Column`. Reached from an op as `ctx.out[n]` | `packages/process/src/column.ts` |
727
728
  | Ranged recompute | `graph.setSourceFrom(series, changedFrom)` — declares which row first changed; `graph.recomputes` → `{ ranged, full }`. An op opts in with `OpDef.runRange(ctx)`, receiving `{ from, to, previous, previousView, out }` (type `RangeContext`) — write into `out` and return nothing for the block path alongside the usual context. Requires `lookback`. Falls back to a full `run` whenever anything is missing | `packages/process/src/plan/graph.ts` |
728
729
  | Node budget | `bind(series, { registry, budgetBytes })` — engine-wide cap on retained node values, LRU, enforced after each `run`; `graph.retainedBytes` / `graph.evictions` / `graph.enforceBudget()`. Unbounded when omitted. Skips a node whose consumer still holds its outlet | `packages/process/src/plan/graph.ts` |
729
730
  | Plan history | `requiredHistory(registry, plan)` → `{ known, rows?, undeclared, byOp }` — the minimum safe tail in rows, folded from per-op `OpDef.lookback`. Sums along nesting, maxes across siblings. `known: false` names ops with no declared lookback rather than defaulting to zero (type `HistoryResult`) | `packages/process/src/plan/history.ts` |
package/CHANGELOG.md CHANGED
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  under a single `v*` tag, so this file covers them all. Pre-1.0: minor bumps may
9
9
  include new features and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pond-ts/pond/compare/v0.72.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.74.0...HEAD
12
+ [0.74.0]: https://github.com/pond-ts/pond/compare/v0.73.0...v0.74.0
13
+ [0.73.0]: https://github.com/pond-ts/pond/compare/v0.72.0...v0.73.0
12
14
  [0.72.0]: https://github.com/pond-ts/pond/compare/v0.71.0...v0.72.0
13
15
  [0.71.0]: https://github.com/pond-ts/pond/compare/v0.70.0...v0.71.0
14
16
  [0.70.0]: https://github.com/pond-ts/pond/compare/v0.69.0...v0.70.0
@@ -75,6 +77,321 @@ include new features and type-level changes; patch bumps are strictly additive.
75
77
 
76
78
  ## [Unreleased]
77
79
 
80
+ ## [0.74.0] — 2026-10-14
81
+
82
+ ### Added
83
+
84
+ - **`@pond-ts/fit`: open water swims.** `ActivityStreams.openWater` marks an
85
+ open water swim. A swim watch only gets a GPS fix while the arm is out of the
86
+ water, so the fixes are sparse and many land well off the path; joined up
87
+ they measured about twice the real distance (a real 3,068 m river swim
88
+ measured 6,944 m). With the flag, distance (and so speed, splits and pace)
89
+ comes from the watch's own `distanceMeters` when it recorded one, and the
90
+ line is cleaned for the map: fixes that would mean moving faster than 2 m/s
91
+ are dropped and the rest smoothed over time (Gaussian, 20 s), one position
92
+ per sample so the map, its hover dot and the bounds agree. Without a usable
93
+ device distance (most samples carry it, ending above zero), distance is
94
+ measured along the cleaned line, which reads somewhat short. Moving time is
95
+ unchanged (still the speed threshold), so a swim's own timer is the better
96
+ source for it.
97
+
98
+ ## [0.73.0] — 2026-10-12
99
+
100
+ ### Added
101
+
102
+ - **core**: **`ordering: 'strict' | 'reorder' | 'clamp'` on every loader**
103
+ (`new TimeSeries`, `TimeSeries.fromJSON` / `fromColumns` / `fromArrow`,
104
+ `ValueSeries.fromJSON` / `fromColumns` / `fromArrow`), with the new
105
+ `BatchOrderingMode` type exported. Same option name and vocabulary as
106
+ `LiveSeries`' `ordering` (`'strict'` / `'reorder'`), so one concept has one
107
+ name across batch and live. `'strict'` is the default (unchanged);
108
+ `'reorder'` is what `sort: true` did. **`'clamp'`** is new: rows stay in place and a key
109
+ earlier than its predecessor's takes that key (the running max), so a noisy
110
+ feed's lone backwards timestamp lands on its neighbour instead of failing the
111
+ ingest or being moved. Values are untouched, ties pass through, non-finite
112
+ keys are still rejected, value-column `Float64Array`s stay zero-copy, and a
113
+ caller's key buffer is never mutated. `time` keys and `ValueSeries` axes
114
+ only — a `timeRange` / `interval` key throws under `'clamp'`. [PND-INGEST]
115
+
116
+ - `pond-ts`: **`series.offsetTime(by)` moves every key by a constant** —
117
+ milliseconds or a duration string, negative for earlier
118
+ (`yesterday.offsetTime('1d')` overlays yesterday on today; `'1d'` is a fixed
119
+ 24 hours, not a calendar day). The key kind is kept; a `timeRange` /
120
+ `interval` key moves both edges and keeps its labels.
121
+ Value columns are shared, not copied, so the cost is one pass over the key:
122
+ 0.26 ms on 97k rows × 118 columns, where rebuilding the series through its
123
+ rows took ~990 ms (`packages/core/scripts/perf-offset-time.mjs`). `0` returns
124
+ the series itself; a non-finite or unparseable offset throws.
125
+
126
+ - **charts**: **`xOffset` on `<LineChart>`, `<AreaChart>` and `<BandChart>`
127
+ draws each row a fixed time after its key**, without touching the series. A
128
+ 1-minute bar is keyed at its open but its `close` is the price at its end, so
129
+ `<LineChart column="close" xOffset="1m">` puts the line where the closes
130
+ happened — the session's last close on the close, not a bar early — while a
131
+ `<Candlestick>` from the same series still draws at the key. Previously this
132
+ meant rebuilding the whole series with shifted keys. Everything the layer
133
+ reports is in drawn time: the tracker's nearest row and dot, its auto-fit
134
+ x-extent, `sessionBreaks`, a sweep's span. Time series only; a non-zero
135
+ offset on a `ValueSeries` throws.
136
+
137
+ - **charts**: **`<AreaChart sessionBreaks>`** ends the fill and its outline at
138
+ each trading-axis session close and re-starts them at the next open, as
139
+ `<LineChart>` and `<BandChart>` already did — so the line and area styles of
140
+ one intraday series break alike. `gaps="none"` still interpolates a dropout
141
+ inside a session but never across a break, and no inferred gap connector
142
+ spans one.
143
+
144
+ - **charts:** `TradingTimeScale.flatFormat(count, drawn?)` — `drawn` is the
145
+ subset of `ticks(count)` actually rendered; flat promotions walk only those,
146
+ so a dropped tick's period turn carries to the next drawn one. Omitted, it
147
+ labels every tick as before.
148
+
149
+ - **docs**: **The versioning policy is now published** — a "Versioning"
150
+ section in the root README and each package README, and a
151
+ [Versioning](https://pond-ts.org/docs/reference/versioning) page on the
152
+ docs site. Pre-1.0 a minor may break (always marked **breaking** here with a
153
+ migration note), patches never break, all packages release in lock-step,
154
+ and from 1.0 the project follows semver. The page also defines the public
155
+ API: each package's entry-point exports as mapped in `API.md`, excluding
156
+ anything marked experimental or internal.
157
+
158
+ ### Changed
159
+
160
+ - **core** (**breaking**): **A `NaN` in a number column now always means
161
+ missing.** Typed-array intake (`fromColumns`, `withColumn` with a
162
+ `Float64Array`) already turned `NaN` into a gap and row intake already
163
+ rejected it, but an operator's own arithmetic could still write a
164
+ _defined_ `NaN` — `diff` / `rate` / `pctChange` of `Infinity - Infinity`,
165
+ a `cumulative` sum that reaches
166
+ `Infinity` and then meets `-Infinity`, or a `cumulative` custom fold,
167
+ `scan` step or numeric `collapse` reducer that returns `NaN`. Those
168
+ cells are now stored as **missing**: `at(i)` / `get` / `read` return
169
+ `undefined`, `scan()` skips them, and `nullCount()` / `hasMissing()` count
170
+ them. Same in live `diff` / `rate` / `pctChange` / `cumulative` views, in a
171
+ live `map` whose function writes `NaN` into a number field, and in
172
+ `@pond-ts/process` `RangeOutput.set` / `sealRange`. A `cumulative` that hits
173
+ `NaN` keeps its accumulator, so built-in folds read missing from that row on.
174
+ `±Infinity` now follows the same rule — see the next entry. The storage
175
+ switch this unlocked landed in the same release — see "number columns store
176
+ a missing cell as `NaN`" below. **Migration:** code that tested
177
+ `Number.isNaN(v)` on an operator's output should test `v === undefined`.
178
+
179
+ - **core** (**breaking**): **Any non-finite number — `NaN`, `Infinity` or
180
+ `-Infinity` — in a number column means missing.** `Infinity` used to get five
181
+ different answers; it now gets one. A non-finite result is stored as a
182
+ **missing** cell (`undefined` on read, counted by `nullCount()` /
183
+ `hasMissing()`, skipped by `scan()`), so `count() + nullCount() === length`
184
+ always holds. What changed:
185
+ - **No longer throws:** `mapColumns` (a mapper returning `NaN` /
186
+ `±Infinity` — was a `RangeError`), and `rolling` / `aggregate` / `smooth`
187
+ / `baseline` / `byColumn` / `pivotByGroup` / `arrayAggregate` whose result
188
+ overflows (was a `ValidationError`). Those cells are now missing.
189
+ - **No longer a defined value:** `diff` / `rate` / `pctChange` /
190
+ `cumulative` / `scan` / `collapse` results that overflow to `±Infinity`
191
+ (e.g. `diff` of `MAX - (-MAX)`, a `cumulative` sum past `Number.MAX_VALUE`
192
+ — missing from that row on, as its accumulator stays `Infinity`).
193
+ - **Reducers never answer non-finite:** a reducer result that overflows
194
+ (`sum`, `avg`, `stdev`, or a custom reducer returning `Infinity`) is
195
+ `undefined` — in `reduce()` too, which used to return `Infinity`. A
196
+ rolling `sum` / `avg` / `stdev` (batch, live, and `smooth('movingAverage')`)
197
+ reads missing only while the current window overflows, and reads finite
198
+ again once the large values leave it. A mean is still sum-then-divide, so
199
+ `avg([MAX_VALUE, MAX_VALUE])` reads missing although the mean is finite.
200
+ (The column methods `col.sum()` / `col.mean()` are unchanged.)
201
+ - **Doors:** `withColumn` with a `Float64Array` reads `±Infinity` as a gap,
202
+ as it already read `NaN` (was a `ValidationError`). Row intake
203
+ (`new TimeSeries`, `fromJSON`, `LiveSeries.push`) and `withColumn` with a
204
+ plain array still reject `NaN` and `±Infinity` alike; `fromColumns` /
205
+ `fromArrow` already read both as a gap.
206
+ - **Live:** `diff` / `rate` / `pctChange` / `cumulative` / `map` views and
207
+ every live accumulator (`aggregate`, `rolling`, `reduce`) emit `undefined`
208
+ for a non-finite result, so a library view's `toTimeSeries()` no longer
209
+ throws on an overflow. A user `map` keeps the source schema, so a `map` fn
210
+ that writes a non-finite value reads missing too — and `toTimeSeries()`
211
+ then works if that column is optional, but throws the required-column
212
+ `ValidationError` if it is required. `@pond-ts/process` `RangeOutput.set` / `sealRange` / `packColumn`
213
+ store `±Infinity` as missing too.
214
+
215
+ **Migration:** code that caught a `ValidationError` / `RangeError` from these
216
+ operators to detect overflow, or tested `v === Infinity` on their output,
217
+ should test `v === undefined`. Code relying on
218
+ `withColumn(name, float64Array)` to reject `Infinity` must check it before
219
+ the call.
220
+
221
+ - **core** (**breaking**, mostly internal): **Number columns store a missing
222
+ cell as `NaN` in their own `Float64Array`, and keep no separate validity
223
+ bitmap** ([PND-NANREP]). Now that any non-finite number in a number column
224
+ means missing (entries above), the `NaN` is the only marker the column needs.
225
+ Answers do not change, with one exception: a gappy `sum` / `avg` over a
226
+ sub-range (an `aggregate` bucket of 32+ cells) can differ from before in
227
+ the last digit, because the blocked sum now groups its eight lanes from the
228
+ range start rather than from a multiple of 8 — the same kind of rounding
229
+ difference blocked summation already allows. Speed improves — on 1M rows with 4% gaps, `fromColumns`
230
+ is 70% faster, `reduce` `sum` / `avg` / `min` 30–38%, `diff` / `cumulative` /
231
+ `shift` 14–18%, and `nullCount()` on a fresh column 35%. Dense columns are
232
+ mostly unchanged or faster (`fromColumns` 32%); dense `cumulative` measured
233
+ ~11% slower. String, boolean and array columns keep their bitmaps. What you
234
+ can see:
235
+ - **`toFloat64Array()` returns `NaN` at a missing cell** (it used to be
236
+ whatever the buffer held, often `0`). A draw loop now breaks the line at a
237
+ gap with no extra check, and `withColumn(name, col.toFloat64Array())` now
238
+ round-trips gaps.
239
+ - **`fromArrow` copies a `Float64` column that has nulls** (one bulk copy,
240
+ writing `NaN` at each null) instead of sharing Arrow's buffers. Gap-free
241
+ columns are still shared with no copy. `toArrow` builds a number column's
242
+ `nullBitmap` from its `NaN` cells (once, then cached).
243
+ - **`Float64Column` constructor** (trusted, internal) is now
244
+ `(values, length, allFinite = false)` — the `validity` argument is gone,
245
+ and the old four-argument call shape throws a `TypeError`. `allFinite` now means "no
246
+ missing cells". `col.validity` still exists for kind-generic code but is
247
+ built from the `NaN` cells on first read. A column built this way with a
248
+ `NaN` partition value or interval label now reads it as missing (it used
249
+ to be a `NaN` partition / a "not a finite number" error).
250
+ - **`@pond-ts/process`:** `RangeOutput`, `ColumnView` and `ColumnBuffers`
251
+ lose their `bits` field — a gap is `NaN` in `values`. `ColumnBuffers`
252
+ gains `allFinite`. `validityByteCount` is removed (nothing to size). `columnBytes` no longer counts a bitmap for number
253
+ columns.
254
+
255
+ **Migration:** code that read `validity.bits` of a number column, or passed
256
+ `bits` across a worker boundary, should test `Number.isNaN(values[i])`
257
+ instead. Code that relied on `fromArrow` sharing a null-bearing column's
258
+ buffer with the Arrow table should not.
259
+
260
+ - **core**: Out-of-order ingest errors now suggest `{ ordering: 'reorder' }`
261
+ (and `'clamp'` where it applies) instead of `{ sort: true }`. Code matching
262
+ on the old message text needs updating.
263
+
264
+ - **core** (**breaking**): **`select(...keys)` throws on a key that isn't a
265
+ column, and on an array argument.** It used to drop unknown keys silently, so
266
+ a typo — or `select(['a', 'b'])` instead of `select('a', 'b')` — returned an
267
+ empty-payload series with no error. Now an unknown key throws a `RangeError`
268
+ naming it and listing the valid columns, and an array argument throws a
269
+ `TypeError` telling you to spread it (`select(...names)`). Applies to
270
+ `TimeSeries`, `LiveSeries`, `LiveView`, `LiveAggregation` and
271
+ `LiveRollingAggregation` (the live ones throw when the view is built, not on
272
+ the first event). `Event.select` rejects an array argument but still
273
+ tolerates absent keys, since an event payload may omit optional fields.
274
+ Duplicate keys are still accepted and select the column once. **Migration:**
275
+ fix the misspelled column, or spread the array.
276
+
277
+ - **core / react**: **The live column reads are no longer experimental.**
278
+ `LiveView.column()`, `LiveView.keyColumn()`, `LiveView.partitionBy().toMap()`,
279
+ the `LiveColumnGroup` class and `@pond-ts/react`'s `useLiveVersion` shipped in
280
+ v0.19.0 labelled "experimental — may change in 0.19.x". They are now part of
281
+ the stable surface that 1.0 will commit to, with no change to their shape.
282
+
283
+ - **charts** (**breaking**): **An `<AreaChart>`'s `baseline` no longer changes
284
+ the y-axis range.** Since v0.71.0 the default zero baseline (and any numeric baseline)
285
+ was pulled into the auto-fit domain, so an area over data running $100–$200
286
+ drew on a $0–$200 axis, and switching `baseline` rescaled the chart — and
287
+ every other layer sharing its axis. The axis now fits the data, exactly as it
288
+ does for a `<LineChart>` of the same column; `baseline` only moves where the
289
+ fill stops. A baseline outside the axis range rests the fill on the nearest
290
+ edge, so on data that never crosses zero the default now draws the same as
291
+ `baseline="floor"`. **Migration:** where the fill should read from zero — a
292
+ stack, a quantity like throughput — put zero on the axis with
293
+ `<YAxis min={0}>` (`max={0}` for an all-negative series). Areas whose data
294
+ already reaches their baseline, or whose axis is pinned, are unchanged.
295
+
296
+ - `pond-ts` (**breaking**, types only): **`TimeSeries.toJSON()`'s return
297
+ type narrows on `rowFormat`**, matching `LiveSeries.toJSON()`. The default
298
+ / `'array'` form returns `TimeSeriesJsonOutputArray<S>` and
299
+ `{ rowFormat: 'object' }` returns `TimeSeriesJsonOutputObject<S>`, so
300
+ `result.rows` is typed against your schema with no cast; a `rowFormat`
301
+ typed as `JsonRowFormat` gets the union. It used to return the loose
302
+ `TimeSeriesJsonInput<SeriesSchema>`. **Migration:** drop any
303
+ `as TimeSeriesJsonInput<…>` / `as TimeSeriesJsonOutput…<…>` casts (they
304
+ still compile). Two patterns now fail to compile: annotating an
305
+ **object**-form result as `TimeSeriesJsonInput<SeriesSchema>` — including
306
+ a `JsonRowFormat`-typed (union) result, since it can be the object form —
307
+ (use `TimeSeriesJsonInput<typeof schema>`, or no annotation), and reading a
308
+ column by name off a default (tuple) row, which was always `undefined` at
309
+ runtime. `LiveSeries.toJSON()` also gains the `JsonRowFormat` overload, which
310
+ previously had no matching signature. [PND-TSJSONT]
311
+
312
+ - `pond-ts`: **`TimeSeries.toJSON()` tuple rows are ~2.5–3.5× faster at 100k
313
+ rows** — one array per row, filled in place, instead of a mapped array spread
314
+ into a second one. Tuple rows were slower than object rows; they now match or
315
+ beat them. Output unchanged.
316
+
317
+ - **core** (**breaking**): **Every `on()` overload on `LiveAggregation` and
318
+ `LiveRollingAggregation` now returns an unsubscribe function**, matching
319
+ `LiveSeries.on` / `LiveView.on`. `on('bucket')` and `on('update')` used to
320
+ return `this`, so those listeners could never be removed. **Migration:** a
321
+ chained `live.aggregate(…).on('bucket', fn)` now yields the unsubscribe
322
+ function, not the operator (same for `live.rolling(…).on('update', fn)`) —
323
+ assign the operator first, then subscribe.
324
+
325
+ - **core** (**breaking, type-level**): **`LiveAggregation` listeners
326
+ (`'event'`, `'bucket'`) and `LiveRollingAggregation`'s `'event'` listener
327
+ receive the schema-narrowed `EventForSchema<Out>`** — the same type `at()`
328
+ and the `closed()` series hand out — instead of a widened
329
+ `Event<Interval, Record<string, …>>` / `any`. `e.get('col')` is now typed
330
+ per output column, and naming a column the aggregation doesn't produce is a
331
+ compile error. As a result a `LiveAggregation<S, Out>` is now assignable to
332
+ `LiveSource<Out>` ([PND-LIVESRC]). The same narrowing applies to the
333
+ `'event'` listener of the keyed-form fused `live.rolling({...})`
334
+ (`LiveFusedRolling`), `live.reduce(…)` (`LiveReduce`), and the
335
+ clock-triggered partitioned `rolling(…)` variants
336
+ (`LivePartitionedSyncRolling` / `LivePartitionedFusedRolling`), which
337
+ previously typed it `(event: any)`. Both `LiveAggregation` /
338
+ `LiveRollingAggregation` `on()` methods now throw a
339
+ `TypeError` on an unknown event type instead of silently registering the
340
+ listener.
341
+
342
+ ### Deprecated
343
+
344
+ - **core**: **`sort?: boolean`** on the loaders above — use
345
+ `ordering: 'reorder'`. `sort: true` still works for this release and maps to
346
+ `'reorder'`; it will be removed in the next minor. Passing `sort` together with
347
+ a conflicting `ordering` (e.g. `sort: true, ordering: 'clamp'`)
348
+ throws a `ValidationError`.
349
+
350
+ ### Removed
351
+
352
+ - **core** (**breaking**): **`LiveAggregation.on('close', fn)` is removed —
353
+ use `on('event', fn)`**, which it was an alias for. Untyped callers that
354
+ still pass `'close'` get a `TypeError` naming the replacement.
355
+
356
+ - `pond-ts` (**breaking**, types only): **the back-compat type alias
357
+ `AggregateOutputMap` is gone — use `AggregateMap`.** It has been an exact
358
+ alias of `AggregateMap` since v0.23.0, when the shorthand and
359
+ `{ from, using }` spec forms were unified into one map type. The internal
360
+ schema aliases `RollingOutputMapSchema` (identical to `RollingSchema`) and
361
+ `AggregateOutputMapResultSchema` (identical to `AggregateSchema`) are removed
362
+ with it; neither was reachable from a public entry point. **Migration:**
363
+ replace `AggregateOutputMap<S>` with `AggregateMap<S>` — no runtime change.
364
+ The fused-rolling duplicate-output error message now says "use a
365
+ `{ from, using }` spec entry" instead of naming the removed type.
366
+
367
+ ### Fixed
368
+
369
+ - **charts: time-axis tick labels no longer overprint each other.** The tick
370
+ ladder places every session open unconditionally, so two anchors could land
371
+ closer than a label is wide — a session calendar derived from the bars turns
372
+ one stray pre-market print into a one-minute session whose `Sep 29` open sat
373
+ a pixel left of the real `15:30` open, and a seam could crowd the last clock
374
+ tick before it (`21:00` against the next day's `Sep 23`). A time axis now
375
+ measures its labels and drops the one that would collide — a date / month /
376
+ year turn outlasts a session-open clock label, which outlasts a plain one,
377
+ which outlasts a tick in closed-market time (an elapsed axis no longer labels
378
+ a seam with the shut night); of two colliding turns (or opens) the later is
379
+ drawn — then re-labels the
380
+ ticks it kept, so the dates they show stay correct (a stray New Year's print
381
+ can't take `2027` with it; a Sunday-evening futures open can't caption
382
+ Monday), and puts back any dropped tick that fits once a neighbour's label
383
+ narrowed. A blank label (a custom `format` returning `''`) never blocks one.
384
+ Under `align="auto"` the edge labels anchor by the full tick list, so a drop
385
+ never re-anchors another label. Applies to every `align` mode and to custom
386
+ `format`s; explicit `ticks` and `transform` ticks are untouched.
387
+
388
+ - **charts**: a **decimated** `<LineChart sessionBreaks>` with
389
+ `gaps="none"` interpolated straight across each session break, and
390
+ `dashed` / `step` / `fade` drew a gap connector over it — dense intraday data
391
+ decimates, so this was the common case. Decimation marks the break as a gap
392
+ at the break instant, and the draw now cuts there instead of reading it as a
393
+ dropout.
394
+
78
395
  ## [0.72.0] — 2026-10-08
79
396
 
80
397
  ### Changed
@@ -245,8 +562,8 @@ include new features and type-level changes; patch bumps are strictly additive.
245
562
 
246
563
  - `@pond-ts/financial`: **`StudyOutput.id` documents the `@pond-ts/process`
247
564
  bridge**, and a new `test/catalog-process.test.ts` pins it. The field is a
248
- *financial column suffix*; process's `OutputDef.id` is a *process outlet
249
- id*; they share a name and are different namespaces. Process names its own
565
+ _financial column suffix_; process's `OutputDef.id` is a _process outlet
566
+ id_; they share a name and are different namespaces. Process names its own
250
567
  columns (`specId + OutputDef.id`) and matches an op's return to its outputs
251
568
  positionally, so a study's own column names never reach it — which means a
252
569
  registry bridging the two chooses its own suffixes, and for the **twelve**
@@ -259,7 +576,7 @@ include new features and type-level changes; patch bumps are strictly additive.
259
576
  worked around it by hand. No API change: the descriptors, the studies and
260
577
  the guard are all unchanged, and the round-trip test is what stops the two
261
578
  packages drifting — `catalog.test.ts` validates a descriptor against its
262
- *study*, so it is structurally blind to a cross-package disagreement.
579
+ _study_, so it is structurally blind to a cross-package disagreement.
263
580
  - `@pond-ts/process`: **`Skipped.spec` echoes the request verbatim**, and its
264
581
  `params` / `inputs` are typed `unknown` accordingly. The plan pass normalized
265
582
  `params: null` to `{}`, so recomputing an id from the echo produced the
@@ -279,7 +596,6 @@ include new features and type-level changes; patch bumps are strictly additive.
279
596
 
280
597
  (All three reported by Tidal against 0.62.0, after adopting it.)
281
598
 
282
-
283
599
  ## [0.69.0] — 2026-09-13
284
600
 
285
601
  ### Added
package/README.md CHANGED
@@ -69,6 +69,17 @@ Guides, the component reference, and live examples live at
69
69
  **<https://pond-ts.org/docs/charts/>**. Source and issues:
70
70
  [github.com/pond-ts/pond](https://github.com/pond-ts/pond).
71
71
 
72
+ ## Versioning
73
+
74
+ Released in lock-step with `pond-ts` and the other `@pond-ts/*` packages
75
+ under one version — keep them in step. Pre-1.0, a minor release may contain
76
+ breaking changes (each marked **breaking** in the
77
+ [CHANGELOG](https://github.com/pond-ts/pond/blob/main/CHANGELOG.md) with a
78
+ migration note); patch releases never break. From 1.0 the project follows
79
+ semver. See
80
+ [Versioning](https://pond-ts.org/docs/reference/versioning) for what
81
+ counts as the public API.
82
+
72
83
  ## License
73
84
 
74
85
  MIT
@@ -4,6 +4,7 @@ import type { NumericColumn, ValueNumericColumn } from './column-names.js';
4
4
  import type { DecimateOption } from './decimate.js';
5
5
  import { type Curve } from './curve.js';
6
6
  import { type GapMode } from './gaps.js';
7
+ import { type XOffset } from './x-offset.js';
7
8
  export interface AreaChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
8
9
  /**
9
10
  * The series' semantic identifier — what the data _is_ / how it should read
@@ -33,23 +34,23 @@ export interface AreaChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
33
34
  /**
34
35
  * The value the fill rests on — the flat edge opposite the value line.
35
36
  *
36
- * - **Omitted ⇒ `0`**: an area measures size from zero, so values above zero
37
- * fill up and values below it fill down, each side's shade fading toward
38
- * the zero line. Zero is pulled into the auto-fit domain, so the axis always
39
- * shows where the fill starts.
37
+ * - **Omitted ⇒ `0`**: values above zero fill up and values below it fill
38
+ * down, each side's shade fading toward the zero line.
40
39
  * - **Another number ⇒ that reference level**, with the same behaviour (e.g.
41
40
  * `baseline={100}` for an index that fills above and below 100). For the
42
41
  * esnet two-colour traffic look, compose two `<AreaChart>`s (an "in" column
43
42
  * and an "out" column, distinct `as` roles).
44
43
  * - **`'floor'` ⇒ the axis's lower bound** (the bottom of the plot): the fill
45
- * rests on whatever the axis starts at, and nothing is added to the domain.
46
- * For a series that never nears zero where zero would flatten the shape — a
47
- * price, an elevation profile.
44
+ * rests on whatever the axis starts at, even when zero is on screen.
48
45
  *
49
- * On a **log** axis zero has no position, so a baseline at or below zero
50
- * falls back to the axis floor and is not pulled into the domain. On an axis
51
- * pinned so that it excludes the baseline, the fill rests on the nearest
52
- * edge (the baseline is clamped into the domain, as a bar's is).
46
+ * **The baseline never changes the axis range.** The axis fits the data, as
47
+ * it does for a `<LineChart>`, so switching `baseline` only moves where the
48
+ * fill stops, never the scale. A baseline outside the axis range rests the
49
+ * fill on the nearest edge (it is clamped into the domain, as a bar's is), so
50
+ * on data that never crosses zero the default looks the same as `'floor'`.
51
+ * To show the fill from zero, put zero on the axis: `<YAxis min={0}>`. On a
52
+ * **log** axis zero has no position, so a baseline at or below zero falls
53
+ * back to the axis floor.
53
54
  */
54
55
  baseline?: number | 'floor';
55
56
  /**
@@ -71,6 +72,21 @@ export interface AreaChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
71
72
  * `'dashed'` / `'step'` connector faintness is the theme's `gap.connectorOpacity`.)
72
73
  */
73
74
  gaps?: GapMode;
75
+ /**
76
+ * End the fill (and its outline) at each **trading-axis discontinuity** — a
77
+ * session / day / lunch close→open — when the container renders on a
78
+ * trading-time axis (a `discontinuities` / `calendar` provider). **Omitted ⇒
79
+ * `false`**: the area runs from the last pre-close point straight to the next
80
+ * open, filling the collapsed gap. `true` ends the shade at the close and
81
+ * re-starts it at the open, the same break `<LineChart sessionBreaks>` draws,
82
+ * so the line and area styles of one intraday series read the same.
83
+ *
84
+ * A **scale** break, orthogonal to {@link gaps} (a **data** break, a NaN run):
85
+ * `gaps="none"` interpolates across a dropout but never across a session
86
+ * break, and no inferred gap connector spans one. A no-op on a continuous
87
+ * axis (no provider) or a provider without `boundaries`.
88
+ */
89
+ sessionBreaks?: boolean;
74
90
  /**
75
91
  * **M4 viewport decimation** (charts decimator wave). **Omitted ⇒ `true`**:
76
92
  * once the visible data is denser than ~2 samples per device pixel, the fill +
@@ -140,6 +156,13 @@ export interface AreaChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
140
156
  * identity** (`as` ?? `column`). The swatch is the resolved area style.
141
157
  */
142
158
  legend?: boolean | string;
159
+ /**
160
+ * Draw each row this long **after its key** — milliseconds or a duration
161
+ * string. **Omitted ⇒ `0`**. Time series only. Same knob as
162
+ * {@link LineChartCommon.xOffset}, so an area and the line it pairs with
163
+ * move together.
164
+ */
165
+ xOffset?: XOffset;
143
166
  /**
144
167
  * @internal Declaration position among the `<Layers>` children, injected by
145
168
  * `Layers` so z-order follows JSX order. Do not set.
@@ -211,6 +234,6 @@ export declare function resolveAreaBaseline(baseline: number | undefined, yScale
211
234
  * </Layers>
212
235
  * ```
213
236
  */
214
- export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps, decimate, thresholds, bandColors, legend, index, }: AreaChartProps<S, VS>): null;
237
+ export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series: source, column, readout, as: semantic, axis, id, baseline, curve, gaps, sessionBreaks, decimate, thresholds, bandColors, legend, xOffset, index, }: AreaChartProps<S, VS>): null;
215
238
  export {};
216
239
  //# sourceMappingURL=AreaChart.d.ts.map
package/dist/AreaChart.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { useContext, useEffect, useMemo } from 'react';
2
2
  import { ValueSeries } from 'pond-ts';
3
3
  import { assertNumericColumn, fromTimeSeries, fromValueSeries, } from './data.js';
4
- import { areaExtent, areaHitIndex, areaStateStyle, drawArea } from './area.js';
5
- import { drawPartitioned, plotExtentOf, strokeSpanEdges, } from './line.js';
4
+ import { areaHitIndex, areaStateStyle, drawArea } from './area.js';
5
+ import { drawPartitioned, plotExtentOf, strokeSpanEdges, yExtent, } from './line.js';
6
6
  import { sweepSpan } from './sweep.js';
7
7
  import { resolveCurve } from './curve.js';
8
8
  import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
9
9
  import { ContainerContext, LayersContext, } from './context.js';
10
10
  import { legendLabelFor, useLegendItems, } from './swatch.js';
11
11
  import { useSlotKey } from './use-slot-key.js';
12
+ import { useXOffset } from './x-offset.js';
12
13
  import { useBandLadder } from './use-band-ladder.js';
13
14
  /** Read a d3 linear scale's domain lower bound (the axis floor) from the plain
14
15
  * `(value) => pixel` function the row hands to `draw`. The runtime object is a
@@ -37,13 +38,17 @@ export function resolveAreaBaseline(baseline, yScale) {
37
38
  return floor;
38
39
  if (!d || d.length === 0)
39
40
  return baseline;
40
- // Clamped into the domain, as a bar's baseline is: under an explicit
41
- // `<YAxis min={40}>` the default 0 is off the plot, and a fill resting
41
+ // Clamped into the domain, as a bar's baseline is: the baseline never
42
+ // widens the axis, so on data that never crosses zero (or under an explicit
43
+ // `<YAxis min={40}>`) the default 0 is off the plot, and a fill resting
42
44
  // there would anchor its fade (and a gap's `'fade'` connector) off-plot.
43
45
  const lo = Math.min(d[0], d[d.length - 1]);
44
46
  const hi = Math.max(d[0], d[d.length - 1]);
45
47
  return Math.min(hi, Math.max(lo, baseline));
46
48
  }
49
+ /** Stable empty boundary list — so `sessionBreaks={false}` keeps a referentially
50
+ * constant array and the layer entry isn't rebuilt every render. */
51
+ const NO_BREAKS = [];
47
52
  /**
48
53
  * An area draw layer: fills between a value `column` and a `baseline`, with a
49
54
  * graded (gradient) shade — opaque at the line, transparent at the baseline —
@@ -64,7 +69,7 @@ export function resolveAreaBaseline(baseline, yScale) {
64
69
  * </Layers>
65
70
  * ```
66
71
  */
67
- export function AreaChart({ series, column, readout, as: semantic, axis, id, baseline, curve, gaps = DEFAULT_GAP_MODE, decimate = true, thresholds, bandColors, legend, index = 0, }) {
72
+ export function AreaChart({ series: source, column, readout, as: semantic, axis, id, baseline, curve, gaps = DEFAULT_GAP_MODE, sessionBreaks = false, decimate = true, thresholds, bandColors, legend, xOffset, index = 0, }) {
68
73
  const container = useContext(ContainerContext);
69
74
  if (container === null) {
70
75
  throw new Error('<AreaChart> must be rendered inside a <ChartContainer>');
@@ -73,11 +78,24 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
73
78
  if (layers === null) {
74
79
  throw new Error('<AreaChart> must be rendered inside a <Layers>');
75
80
  }
81
+ // The series as drawn: moved by `xOffset` (the source itself when there is
82
+ // none), so every read below shares one axis.
83
+ const series = useXOffset(source, xOffset, 'AreaChart');
76
84
  // `undefined` below means "the axis floor" — the internal helpers' spelling.
77
85
  const baseValue = baseline === 'floor' ? undefined : (baseline ?? 0);
78
86
  const cs = useMemo(() => series instanceof ValueSeries
79
87
  ? fromValueSeries(series, column)
80
88
  : fromTimeSeries(series, column), [series, column]);
89
+ // Trading-axis session breaks: the collapse instants inside this series' span.
90
+ // Data instants, not pixels — view-independent, so pan/zoom reuse them. The
91
+ // same lookup `<LineChart>` / `<BandChart>` do, so the three break alike.
92
+ const sessionBreakInstants = useMemo(() => {
93
+ const provider = container.discontinuities;
94
+ if (!sessionBreaks || provider?.boundaries === undefined || cs.length < 2) {
95
+ return NO_BREAKS;
96
+ }
97
+ return provider.boundaries(cs.x[0], cs.x[cs.length - 1]);
98
+ }, [sessionBreaks, container.discontinuities, cs]);
81
99
  // Readout column values for a value-axis series (time path reads it off the
82
100
  // event) — the tracker reports it alongside the plotted fill so an off-chart
83
101
  // readout can show a source value. See AreaChartProps.readout.
@@ -150,13 +168,9 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
150
168
  const entry = useMemo(() => ({
151
169
  layer: {
152
170
  as: semantic,
153
- // A baseline with no position on a log axis (zero, the default) is
154
- // left out of the fit: the draw rests it on the floor anyway, and an
155
- // extent of `[0, max]` gives the log fit no positive low end, so it
156
- // would collapse the domain onto the max and clip the series.
157
- yExtent: (scale) => areaExtent(cs, scale === 'log' && baseValue !== undefined && baseValue <= 0
158
- ? undefined
159
- : baseValue),
171
+ // The values alone: the baseline never widens the axis (see
172
+ // `baseline`), so an area fits the same domain a line would.
173
+ yExtent: () => yExtent(cs),
160
174
  // The container infers the shared x scale's kind from its layers — a
161
175
  // ValueSeries plots on a value axis, a TimeSeries on time.
162
176
  xKind: series instanceof ValueSeries ? 'value' : 'time',
@@ -289,7 +303,7 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
289
303
  // `NaN` and poison every coordinate in the fill path. Fall back
290
304
  // to the axis floor, which is exactly what `'floor'`
291
305
  // already resolves to.
292
- resolveAreaBaseline(baseValue, yScale), curveFactory, gaps, gapConnectorOpacity, decimate, bandLadder);
306
+ resolveAreaBaseline(baseValue, yScale), curveFactory, gaps, gapConnectorOpacity, decimate, bandLadder, sessionBreakInstants);
293
307
  }
294
308
  },
295
309
  },
@@ -307,6 +321,7 @@ export function AreaChart({ series, column, readout, as: semantic, axis, id, bas
307
321
  curveFactory,
308
322
  gaps,
309
323
  gapConnectorOpacity,
324
+ sessionBreakInstants,
310
325
  decimate,
311
326
  bandLadder,
312
327
  axis,
@@ -3,6 +3,7 @@ import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
3
3
  import type { NumericColumn, ValueNumericColumn } from './column-names.js';
4
4
  import { type Curve } from './curve.js';
5
5
  import type { DecimateOption } from './decimate.js';
6
+ import { type XOffset } from './x-offset.js';
6
7
  export interface BandChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
7
8
  /**
8
9
  * The band's semantic identifier — what the spread _is_ (e.g. `outer` for a
@@ -44,6 +45,13 @@ export interface BandChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
44
45
  * `boundaries`.
45
46
  */
46
47
  sessionBreaks?: boolean;
48
+ /**
49
+ * Draw each row this long **after its key** — milliseconds or a duration
50
+ * string. **Omitted ⇒ `0`**. Time series only. Same knob as
51
+ * {@link LineChartCommon.xOffset}, so a band and the line it pairs with
52
+ * move together.
53
+ */
54
+ xOffset?: XOffset;
47
55
  /**
48
56
  * **M4 viewport decimation** (charts decimator wave). **Omitted ⇒ `true`**:
49
57
  * once the visible envelope is denser than ~2 samples per device pixel, it is
@@ -111,6 +119,6 @@ export type BandChartProps<S extends SeriesSchema = SeriesSchema, VS extends Val
111
119
  * </Layers>
112
120
  * ```
113
121
  */
114
- export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, upper, as: semantic, axis, curve, sessionBreaks, decimate, legend, index, }: BandChartProps<S, VS>): null;
122
+ export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series: source, lower, upper, as: semantic, axis, curve, sessionBreaks, decimate, legend, xOffset, index, }: BandChartProps<S, VS>): null;
115
123
  export {};
116
124
  //# sourceMappingURL=BandChart.d.ts.map