@pond-ts/react 0.58.0 → 0.60.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 (3) hide show
  1. package/API.md +580 -0
  2. package/CHANGELOG.md +339 -1
  3. package/package.json +5 -4
package/API.md ADDED
@@ -0,0 +1,580 @@
1
+ # API.md — public API map for agents
2
+
3
+ A fast-navigation map of every public export across pond's six packages,
4
+ written **for coding agents**. Use it to find the right primitive and the file
5
+ it lives in without crawling `src/`. It is a map, not a reference: one line per
6
+ export, grouped by purpose, with the source path. Verify exact signatures in
7
+ the listed source file (or the generated typedoc) before writing code against
8
+ them.
9
+
10
+ **This file ships inside every `pond-ts` / `@pond-ts/*` npm package**, so an
11
+ agent working in a consuming repo has the whole export surface locally — no
12
+ network, no crawling `node_modules/*/dist/*.d.ts`. Every package carries the
13
+ same monorepo-wide copy on purpose: the packages compose, so knowing what is
14
+ next door is the point.
15
+
16
+ - **Authority**: each package's `src/index.ts` is the export surface. If this
17
+ file and `index.ts` disagree, `index.ts` wins.
18
+ - **Source paths** (`packages/core/src/…`) are repo-relative. From a consuming
19
+ repo, read them on GitHub:
20
+ <https://github.com/pond-ts/pond/blob/main/>`<path>`.
21
+ - **Human-facing docs**: <https://pond-ts.org> — narrative guides, per-feature
22
+ reference, and generated typedoc per package. This file is the agent-facing
23
+ complement, not a replacement.
24
+ - **Contributing to pond itself**: when a PR adds, removes, or renames a public
25
+ export, update the matching row here in that PR — CI enforces it (the
26
+ `API map` workflow).
27
+
28
+ | Package | npm name | Entry points | Docs hub |
29
+ | -------------------- | -------------------- | ------------------------------------------------------ | ------------------------- |
30
+ | `packages/core` | `pond-ts` | `.` and `./types` (zero-runtime schema contract) | `website/docs/pond-ts/` |
31
+ | `packages/react` | `@pond-ts/react` | `.` | `website/docs/react/` |
32
+ | `packages/charts` | `@pond-ts/charts` | `.` | `website/docs/charts/` |
33
+ | `packages/financial` | `@pond-ts/financial` | `.` and `./fluent` (prototype augmentation) | `website/docs/financial/` |
34
+ | `packages/fit` | `@pond-ts/fit` | `.` | `website/docs/fit/` |
35
+ | `packages/process` | `@pond-ts/process` | `.` and `./pool` (Node worker pool) — **experimental** | `website/docs/process/` |
36
+
37
+ ---
38
+
39
+ ## pond-ts (core) — batch
40
+
41
+ ### Series classes & construction
42
+
43
+ | Export | Purpose | Source |
44
+ | ----------------------- | ------------------------------------------------------ | ---------------------------------------------------- |
45
+ | `TimeSeries` | Immutable time-indexed collection, columnar storage | `packages/core/src/batch/time-series.ts` |
46
+ | `ValueSeries` | Series keyed by a monotonic non-time value axis | `packages/core/src/batch/value-series.ts` |
47
+ | `PartitionedTimeSeries` | Scoped view for per-partition stateful transforms | `packages/core/src/batch/partitioned-time-series.ts` |
48
+ | `Sequence` | Infinite grid of time buckets (daily, hourly, every N) | `packages/core/src/sequence/sequence.ts` |
49
+ | `BoundedSequence` | Finite ordered list of explicit interval buckets | `packages/core/src/sequence/bounded-sequence.ts` |
50
+
51
+ Static constructors on `TimeSeries`: `fromJSON()` (row tuples/objects),
52
+ `fromColumns()` (struct-of-arrays; `number` + `string` value columns),
53
+ `fromArrow()` (bring-your-own Apache Arrow `Table`; zero-copy Float64 adopt +
54
+ BigInt-free int64 time. Readable Arrow types are an **allowlist checked
55
+ against each field's declared type** — `Int`, `Float32`/`Float64`, `Date`,
56
+ `Time`, `Timestamp`, `Utf8`/`Utf8View`, `Null`, and a `Dictionary` of any of
57
+ those — and anything else, notably `Decimal` and `Float16`, is refused by name
58
+ rather than misread; see
59
+ `packages/core/src/batch/operators/arrow-types.ts`), `fromEvents()`,
60
+ `fromPoints()` (wide rows with `ts`), `concat()`, `joinMany()`. On
61
+ `ValueSeries`, the same three shapes keyed on the axis instead of time:
62
+ `fromJSON()`, `fromColumns()`, `fromArrow()` (`{ axis }` is required — no
63
+ `'time'` field convention to fall back on, and no unit scaling). Arrow-ingest
64
+ types (`ArrowTableLike`, `ArrowVectorLike`, `ArrowDataLike`, `ArrowFieldLike`,
65
+ `ArrowSchemaLike`, `ArrowTimeUnit`, `FromArrowOptions`,
66
+ `FromArrowValueOptions`) live in
67
+ `packages/core/src/batch/operators/from-arrow.ts`.
68
+
69
+ Both classes also export **columnar JSON** — `toColumns()`, one plain array
70
+ per column with gaps as `null`, the exact `{ name, schema, columns }` envelope
71
+ `fromColumns()` takes back (`packages/core/src/batch/operators/to-columns.ts`).
72
+ A **two-edged key** (`timeRange` / `interval`) flattens into extra columns
73
+ named off it — `timeRange` + `timeRangeEnd`, `interval` + `intervalEnd` +
74
+ `intervalLabel` — the convention `toArrow` already emitted, now read by
75
+ `fromColumns` and by `fromArrow({ keyKind })` as well
76
+ (`packages/core/src/batch/operators/flat-keys.ts` owns the naming + collision
77
+ rules). Columnar wire types live beside their row siblings:
78
+ `TimeSeriesJsonColumns` / `FlatKeyColumns` / `TimeSeriesColumnarInput` /
79
+ `TimeSeriesColumnarOutput` in `packages/core/src/schema/json.ts`.
80
+
81
+ Going the other way, `TimeSeries.toArrow()` / `ValueSeries.toArrow()` export
82
+ the columns **in Arrow's memory layout with no copy** — pond's validity bitmap
83
+ is already LSB-first one-bit-per-value, numerics are a contiguous
84
+ `Float64Array`, booleans a packed bitmap, dict-encoded strings `Int32Array`
85
+ indices plus a dictionary. It returns `{ length, fields }` rather than an Arrow
86
+ `Table` (pond doesn't depend on `apache-arrow`; the caller assembles with
87
+ `makeData`/`makeVector`), so another columnar engine is a buffer handoff
88
+ instead of a re-ingest. Arrow-export types (`ArrowExport`, `ArrowExportField`,
89
+ `ArrowExportType`, `ToArrowOptions`) live in
90
+ `packages/core/src/batch/operators/to-arrow.ts`.
91
+
92
+ `ValueSeries` also exports rows (`toRows()`, `toObjects()`, `toJSON()`).
93
+ Value-axis wire types
94
+ (`ValueSeriesJsonInput`, `ValueSeriesJsonRow`, `ValueSeriesJsonObjectRow`,
95
+ `ValueSeriesJsonOutputArray`, `ValueSeriesJsonOutputObject`,
96
+ `ValueSeriesJsonCell`, `ValueSeriesRow`, `ValueSeriesObjectRow`,
97
+ `ValueSeriesJsonColumns`, `ValueSeriesColumnarInput`,
98
+ `ValueSeriesColumnarOutput`) live in `packages/core/src/schema/value-io.ts`.
99
+
100
+ ### Temporal keys & events
101
+
102
+ | Export | Purpose | Source |
103
+ | ------------- | --------------------------------------------- | -------------------------------------- |
104
+ | `Time` | Point-in-time event key | `packages/core/src/core/time.ts` |
105
+ | `TimeRange` | Interval event key (start/end) | `packages/core/src/core/time-range.ts` |
106
+ | `Interval` | Labeled time-interval event key | `packages/core/src/core/interval.ts` |
107
+ | `Event` | Immutable event: temporal key + typed payload | `packages/core/src/core/event.ts` |
108
+ | `toTimeRange` | Coerce temporal values to `TimeRange` | `packages/core/src/core/time-range.ts` |
109
+
110
+ ### TimeSeries methods (all in `packages/core/src/batch/time-series.ts`)
111
+
112
+ - **Query**: `at()`, `first()`, `last()`, `bisect(key)`, `includesKey(key)`,
113
+ `atOrBefore(key)`, `atOrAfter(key)`, `nearest(key)`, `find()`, `some()`,
114
+ `every()`
115
+ - **Export/access**: `column(name)`, `keyColumn()`, `toRows()`, `toObjects()`,
116
+ `toArray()`, `toJSON()`, `toColumns()`, `toArrow()`, `toPoints()`
117
+ - **Temporal range**: `timeRange()`, `overlaps()`, `contains()`,
118
+ `intersection()`, `overlapping(range)`, `containedBy(range)`, `trim(range)`,
119
+ `after()`, `before()`, `within()`, `tail(duration)`
120
+ - **Key-type conversion**: `asTime({ at })`, `asTimeRange()`, `asInterval()`
121
+ - **Filter/slice**: `filter()`, `sample(strategy)`, `slice(begin, end)`
122
+ - **Column reshape**: `select()`, `rename()`, `map()`, `mapColumns()`,
123
+ `withColumn()`, `collapse()`
124
+ - **Array columns**: `arrayContains()`, `arrayContainsAll()`,
125
+ `arrayContainsAny()`, `arrayAggregate()`, `arrayExplode()`
126
+ - **Gap fill / dedupe**: `fill()`, `materialize()`, `dedupe()`
127
+ - **Aggregate/group**: `aggregate(sequence, spec)`, `reduce()`, `groupBy()`,
128
+ `partitionBy()`, `byColumn()` (order-free, by column value),
129
+ `rollingByColumn()`, `byValue(axis)` (project onto a `ValueSeries`)
130
+ - **Windowing/smoothing**: `rolling(window, spec, opts)`, `smooth(column,
131
+ method)` (EMA / Butterworth / Savitzky-Golay), `align(method, opts)`
132
+ - **Differential/statistical**: `diff()`, `rate()`, `pctChange()`,
133
+ `cumulative()`, `scan()` (custom stateful reducer), `shift()`, `baseline()`
134
+ (rolling avg/sd/bands), `outliers()` (deviation from baseline)
135
+ - **Join/pivot**: `join(other, opts)`, `pivotByGroup(group, opts)`
136
+
137
+ ### ValueSeries methods (all in `packages/core/src/batch/value-series.ts`)
138
+
139
+ Deliberately small — the ordering-based slice of the algebra, no calendar ops
140
+ (see `docs/rfcs/value-axis.md`) — except for ingest/export, which is at full
141
+ `TimeSeries` parity.
142
+
143
+ - **Query/read**: `length`, `axisName`, `axisValues()`, `axisAt(i)`,
144
+ `column(name)`, `nearestIndex(value)`, `sliceByValue(lo, hi)`
145
+ - **Export**: `toRows()`, `toObjects()`, `toJSON({ rowFormat })`,
146
+ `toColumns()`, `toArrow(opts)`
147
+
148
+ ### Columnar layer & support
149
+
150
+ | Export | Purpose | Source |
151
+ | ---------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------------------------------- |
152
+ | `Float64Column` / `StringColumn` / `BooleanColumn` / `ArrayColumn` | Packed value-column storage per kind | `packages/core/src/columnar/` |
153
+ | `ChunkedFloat64Column` / `ChunkedStringColumn` / `ChunkedBooleanColumn` / `ChunkedArrayColumn` | Chunked variants (variable-length buffers) | `packages/core/src/columnar/chunked-column.ts` |
154
+ | `TimeKeyColumn` / `TimeRangeKeyColumn` / `IntervalKeyColumn` / `ValueKeyColumn` | Key-column storage per key kind | `packages/core/src/columnar/key-column.ts` |
155
+ | `top` | Reducer factory: top-N values | `packages/core/src/reducers/top.ts` |
156
+ | `ValidationError` | Error class thrown on invalid input | `packages/core/src/core/errors.ts` |
157
+
158
+ ### Key exported types (batch)
159
+
160
+ | Type group | Names | Source |
161
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
162
+ | Schema contract | `SeriesSchema`, `RowForSchema`, `EventForSchema`, `EventDataForSchema`, `EventKeyForSchema`, `TimeSeriesInput`, `TimeSeriesJsonInput` | `packages/core/src/schema/index.ts` |
163
+ | Aggregation specs | `AggregateReducer`, `AggregateMap`, `AggregateOutputMap`, `AggregateSchema`, `BinReducerName`, `BinOutput` | `packages/core/src/schema/index.ts`, `packages/core/src/column.ts` |
164
+ | Operation schemas | `RollingSchema`, `RollingAlignment`, `AlignSchema`, `DiffSchema`, `SmoothSchema`, `SmoothMethod`, `FillStrategy`, `FillMapping` | `packages/core/src/schema/index.ts` |
165
+ | Column/data kinds | `Column`, `KeyColumn`, `ColumnKind`, `ScalarKind`, `ScalarValue`, `ColumnValue`, `ArrayValue`, `ValidityBitmap` | `packages/core/src/columnar/` |
166
+ | JSON wire format | `JsonRowFormat`, `JsonRowForSchema`, `JsonObjectRowForSchema`, `JsonValueForKind`, `JsonTimestampInput`, `JsonTimeRangeInput`, `JsonIntervalInput` | `packages/core/src/schema/index.ts` |
167
+ | Temporal utility | `TemporalLike`, `DurationInput`, `CalendarUnit`, `TimeZoneOptions`, `KeyLike`, `BatchSampleStrategy` | `packages/core/src/core/`, `packages/core/src/sequence/` |
168
+
169
+ The `pond-ts/types` subpath re-exports the schema-as-contract types with zero
170
+ runtime (`packages/core/src/schema/public.ts`).
171
+
172
+ ---
173
+
174
+ ## pond-ts (core) — live / streaming
175
+
176
+ ### Classes
177
+
178
+ | Export | Purpose | Source |
179
+ | ----------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- |
180
+ | `LiveSeries` | Bounded in-memory buffer of time-keyed events with retention | `packages/core/src/live/live-series.ts` |
181
+ | `LiveView` | Stateful transformation view over a live source | `packages/core/src/live/live-view.ts` |
182
+ | `LivePartitionedSeries` | Routes events into per-partition sub-buffers by column value | `packages/core/src/live/live-partitioned-series.ts` |
183
+ | `LivePartitionedView` | Derived per-partition view over a partitioned series | `packages/core/src/live/live-partitioned-series.ts` |
184
+ | `LiveAggregation` | Emits aggregated buckets when `Sequence` boundaries cross | `packages/core/src/live/live-aggregation.ts` |
185
+ | `LiveRollingAggregation` | Single-window rolling aggregation, configurable trigger | `packages/core/src/live/live-rolling-aggregation.ts` |
186
+ | `LiveFusedRolling` | Multi-window rolling, shared deque, single ingest pass | `packages/core/src/live/live-fused-rolling.ts` |
187
+ | `LivePartitionedFusedRolling` | Fused rolling per partition, synchronized emission | `packages/core/src/live/live-partitioned-fused-rolling.ts` |
188
+ | `LiveReduce` | Reduce over current buffer; emits per trigger | `packages/core/src/live/live-reduce.ts` |
189
+ | `LiveColumnGroup` | Zero-copy column gather over a view slice | `packages/core/src/live/live-view.ts` |
190
+
191
+ ### Triggers
192
+
193
+ `Trigger` factory (`packages/core/src/live/triggers.ts`): `Trigger.event()`
194
+ (per-event, default), `Trigger.clock(sequence)` (boundary crossing),
195
+ `Trigger.every(duration)` (fixed cadence sugar), `Trigger.count(n)`. Types:
196
+ `EventTrigger`, `ClockTrigger`, `CountTrigger`.
197
+
198
+ ### Methods
199
+
200
+ - **`LiveSeries`** — static: `LiveSeries.fromJSON()`; ingest: `push()`,
201
+ `pushMany()`, `pushJson()`; query: same
202
+ key-query set as `TimeSeries` (`at`/`first`/`last`/`find`/`bisect`/
203
+ `atOrBefore`/`atOrAfter`/…); operators: `window()`, `aggregate()`,
204
+ `rolling()`, `reduce()`, `diff()`, `rate()`, `pctChange()`, `fill()`,
205
+ `cumulative()`, `partitionBy()`; snapshots: `toTimeSeries()`, `toRows()`;
206
+ subscription: `on()` (event/batch/evict) → unsubscribe fn; utilities:
207
+ `stats()`, `clear()`, `timeRange()`, `eventRate()`, `length`.
208
+ - **`LiveView`** — transform: `filter()`, `map()`, `select()`, `sample()`;
209
+ plus the same operator/query/snapshot/subscription surface as `LiveSeries`
210
+ (minus ingest).
211
+ - **`LivePartitionedSeries`** — `toMap()` (spawn all partitions), `apply()`
212
+ (per-partition factory), `collect()` (fan-in unified series), `sample()`,
213
+ `stats()`, `on()` (spawn callback).
214
+
215
+ ### Key exported types (live)
216
+
217
+ `LiveSeriesOptions` (`name`, `schema`, `ordering: 'strict' | 'drop' |
218
+ 'reorder'`, `graceWindow`, `retention: { maxEvents?, maxAge? }`),
219
+ `LivePartitionedOptions`, `LiveAggregationOptions`, `LiveRollingOptions`,
220
+ `RollingWindow`, `LiveFillStrategy`, `LiveFillMapping` — all under
221
+ `packages/core/src/live/`.
222
+
223
+ ---
224
+
225
+ ## @pond-ts/react
226
+
227
+ All hooks in `packages/react/src/<hookName>.ts`.
228
+
229
+ | Export | Signature gist | Purpose |
230
+ | ---------------- | --------------------------------------- | ---------------------------------------------------------------------------------- |
231
+ | `useLiveSeries` | `useLiveSeries(opts, hookOpts?)` | Create + own a `LiveSeries` for the component lifetime; returns it with a snapshot |
232
+ | `useTimeSeries` | `useTimeSeries(input, key?)` | Memoized `TimeSeries.fromJSON` for static/fetched data |
233
+ | `useSnapshot` | `useSnapshot(source, opts?)` | Subscribe to a live source, return a throttled `TimeSeries` snapshot |
234
+ | `useWindow` | `useWindow(source, size, opts?)` | Windowed view of a live source + throttled snapshot |
235
+ | `useDerived` | `useDerived(series, transform)` | Batch transform of a snapshot, recomputed on change |
236
+ | `useLiveQuery` | `useLiveQuery(build, deps, opts?)` | Build a derived live view, subscribe, return view + snapshot |
237
+ | `useLatest` | `useLatest(source, opts?)` | Only the latest event |
238
+ | `useCurrent` | `useCurrent(source, mapping, opts?)` | Current value of a reducer over the source |
239
+ | `useEventRate` | `useEventRate(source, duration, opts?)` | Events-per-second over a trailing window |
240
+ | `useLiveVersion` | `useLiveVersion(source, opts?)` | Change signal for reading columns without a snapshot |
241
+ | `takeSnapshot` | `takeSnapshot(source)` | Non-hook: snapshot any live source to a `TimeSeries` |
242
+
243
+ Types: `UseSnapshotOptions`, `SnapshotSource` (structural — covers
244
+ `LiveSeries`, `LiveView`, …), `UseCurrentOptions`, `UseLiveVersionOptions`.
245
+
246
+ ---
247
+
248
+ ## @pond-ts/charts
249
+
250
+ ### Components — layout & axes
251
+
252
+ | Component | Key props | Purpose | Source |
253
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
254
+ | `ChartContainer` | `width`, `range?`, `theme?`, `cursor?`, `panZoom?`, `xScale?`, `bounds?`, `showAxis?`, `calendar?`, `origin?`, `maxBandWidth?`/`bandAlign?`, `onTrackerChanged?`, `onDrawStats?` | Root: shared x-scale, interactions, annotations | `packages/charts/src/ChartContainer.tsx` |
255
+ | `ChartRow` | `height`, `cursor?` (deprecated — mount a cursor in the row) | One stacked plot band; owns its y-axes | `packages/charts/src/ChartRow.tsx` |
256
+ | `Layers` | children | Mandatory z-stack inside a row (back-to-front) | `packages/charts/src/Layers.tsx` |
257
+ | `YAxis` | `id` (req), `side?`, `scale?` (`'linear'` \| `'log'` \| `'symlog'`), `linearWindow?`, `min?`/`max?`, `format?`, `width?`, `hide?` | Y-axis gutter; layers bind via their `axis` prop | `packages/charts/src/YAxis.tsx` |
258
+ | `XAxis` | `side?`, `label?`, `format?`, `ticks?`, `transform?`, `dateStyle?` | Placeable x-axis strip; kind inferred from data | `packages/charts/src/XAxis.tsx` |
259
+ | `TimeAxis` / `CategoryAxis` | (XAxis props) | Thin `XAxis` presets | `packages/charts/src/TimeAxis.tsx`, `CategoryAxis.tsx` |
260
+ | `Canvas` | `width`, `height`, `draw` | Low-level DPR-aware canvas primitive | `packages/charts/src/Canvas.tsx` |
261
+ | `Selector` | `enabled?` (default `true`), `selected?` (mark \| set), `hovered?`, `onSelect?`, `onHover?`, `children?` | Wraps its scope; mounting enables click-select and owns the state it drives (RFC A10) | `packages/charts/src/selectors.tsx` |
262
+ | `MultiSelector` | `enabled?`, `selected?`, `hovered?`, `sequence?`, `onSelect?`, `onHover?`, `children?` | Sweep-select superset of `Selector`: drag sweeps marks, release reports `(hits, modifiers, spans)` — plural, one per swept layer (RFC A5.2) | `packages/charts/src/selectors.tsx` |
263
+
264
+ ### Components — draw layers
265
+
266
+ All take `series` plus an `as?` style identifier (theme lookup) and `axis?`
267
+ scale id — style and scale are separate channels; there are no per-component
268
+ color props (see Theming).
269
+
270
+ **Column props are schema-derived** ([PND-CHARTAPI]): a name that isn't a
271
+ numeric column of the series fails to compile, and `<BarChart>`'s props are a
272
+ union of its legal source modes, so mixing `series`/`bins`/`categories` or
273
+ `column`/`columns` is a compile error too. Two carve-outs:
274
+ a **loosely-typed** series (`TimeSeries<SeriesSchema>`) still accepts any name
275
+ (`packages/charts/src/column-names.ts` explains the `never` fallback that makes
276
+ this work), and `bins` names stay `string` (they name aggregate fields, not
277
+ schema columns). Because the union splits per series _kind_, a value typed as
278
+ **either** kind must be narrowed or cast at the call site.
279
+
280
+ | Component | Data props | Purpose | Source |
281
+ | -------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------- |
282
+ | `LineChart` | `column`, `gaps?`, `sessionBreaks?` | Gap-aware line | `packages/charts/src/LineChart.tsx` |
283
+ | `AreaChart` | `column`, `baseline?`, `gaps?`, `thresholds?`/`bandColors?` | Filled area | `packages/charts/src/AreaChart.tsx` |
284
+ | `BandChart` | `lower`, `upper` | Variance-band envelope | `packages/charts/src/BandChart.tsx` |
285
+ | `ScatterChart` | `column`, `id?` (selection), radius/color encodings | Points; data-driven size/colour | `packages/charts/src/ScatterChart.tsx` |
286
+ | `BarChart` | `column` \| `columns` \| `bins` \| `categories`, `orientation?`, `thresholds?`/`bandColors?` | Bars, stacked bars, histograms, categorical | `packages/charts/src/BarChart.tsx` |
287
+ | `HeatMap` | `series`, `columns` (rows), `colors`, `domain?`, `scale?`, `noData?`, `gap?`, `decimate?`, `orientation?` | Grid of colour-coded cells; bins on x, columns on y | `packages/charts/src/HeatMap.tsx` |
288
+ | `BoxPlot` | `lower`/`q1?`/`median?`/`q3?`/`upper`, `shape?` | Box-and-whisker from quantile columns | `packages/charts/src/BoxPlot.tsx` |
289
+ | `Candlestick` | OHLC columns, `variant?`, `colorBy?`, `showOHLC?` | First-class OHLC candles (TimeSeries only) | `packages/charts/src/Candlestick.tsx` |
290
+ | `Legend` | `placement?`, `items?`, `onRowClick?`, `onRowHover?` | Series key from registered layers' resolved styles | `packages/charts/src/Legend.tsx` |
291
+
292
+ ### Components — annotations & indicators
293
+
294
+ | Component | Key props | Purpose | Source |
295
+ | ---------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
296
+ | `Region` | `from`, `to`, `label?`, `id?`, `onChange?` | Shaded x-span; draggable when `onChange` given | `packages/charts/src/annotations.tsx` |
297
+ | `Baseline` | `value`, `axis?`, `label?`, `indicator?`, `onChange?` | Horizontal value line | `packages/charts/src/annotations.tsx` |
298
+ | `Marker` | `at`, `label?`, `indicator?`, `onChange?` | Vertical x line | `packages/charts/src/annotations.tsx` |
299
+ | `Zone` | `from`, `to`, `axis?`, `role?`, `label?`, `edges?` | Shaded y-span — a value-axis scale (AQI categories, HR zones); inert + edge-less by default, `±Infinity` for open ends | `packages/charts/src/annotations.tsx` |
300
+ | `YAxisIndicator` | `value?` \| `source?`, `axis?`, `format?` | Live value pill pinned to a y-axis edge | `packages/charts/src/indicators.tsx` |
301
+
302
+ ### Components — cursors (mounted presets)
303
+
304
+ The `cursor` string modes as components (interaction RFC §4/A4.1) — mount one
305
+ as a child of `<ChartContainer>` (the default for every row) or inside a
306
+ `<ChartRow>` (the per-row override). Render-only presets stack; one
307
+ gesture-owning cursor (`Crosshair`/`Range`) per scope. The `cursor` /
308
+ `cursorTime` / `crosshairSnap` / `cursorFormat` / `cursorSequence` /
309
+ `onRegionSelect` / `regionSelectModifier` props (and `<ChartRow cursor>`) are
310
+ **deprecated** — they keep working for one minor via an internal shim. The
311
+ underlying `CursorSpec` contract stays unpublished (Q3); every drag claim on
312
+ the plot (annotation-create, the range drag, pan) is arbitrated by one brush
313
+ recognizer with a documented precedence (`src/brush.tsx`, RFC A1.5/A2.7).
314
+
315
+ | Component | Key props | Purpose | Source |
316
+ | ----------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
317
+ | `LineCursor` | `showTime?` | The synced vertical line (`cursor="line"`, the legacy default) | `packages/charts/src/cursors.tsx` |
318
+ | `PointCursor` | `showTime?` | A dot on each series at the cursor (`"point"`) | `packages/charts/src/cursors.tsx` |
319
+ | `InlineCursor` | `showTime?` | Dots + a value chip beside each (`"inline"`) | `packages/charts/src/cursors.tsx` |
320
+ | `FlagCursor` | `showTime?` | Dots + staffed value flags stacked at the top (`"flag"`) | `packages/charts/src/cursors.tsx` |
321
+ | `CrosshairCursor` | `snap?`, `showTime?`, `format?` | The inspection reticle: dashed cross, y value pill, x time pill (`"crosshair"`) | `packages/charts/src/cursors.tsx` |
322
+ | `RangeCursor` | `sequence?`, `onDragRelease?`, `enableDrag?`, `dragModifier?` | The hover-time band + the drag: release fires once with a `RangeSpan`, then reverts (`"region"` + `onRegionSelect` successor) | `packages/charts/src/cursors.tsx` |
323
+
324
+ ### Components — standalone row lists (DOM tables, no `<ChartContainer>`)
325
+
326
+ One row per _entity_ (interface, split, symbol) on one shared value scale;
327
+ label + data cells, `sortBy`/`sort`, optional per-row expander. The in-plot
328
+ histogram stays `<BarChart orientation="horizontal">` — these are the table
329
+ shape (react-timeseries-charts' `HorizontalBarChart`).
330
+
331
+ | Component | Data props | Purpose | Source |
332
+ | --------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------- |
333
+ | `BarList` | `rows`, `columns` (`values` names), `barColors?`, `sortBy?`, `before?`/`after?`, `renderExpanded?` | Ranked bar list — one proportional bar line per column per row | `packages/charts/src/BarList.tsx` |
334
+ | `BoxList` | `rows`, `columns` (five-number names + `value?` tick), same table props | Distribution list — range band / q1→q3 body / median / current tick | `packages/charts/src/BoxList.tsx` |
335
+
336
+ Row/option types + readers (`packages/charts/src/list.ts`): `ListRow`,
337
+ `ListValue`, `ListCellSpec`, `ListMarker` (reference rule through every row,
338
+ label above; joins the auto domain fit), `ListSortDirection`, `BarListColumn`,
339
+ `BoxListColumn`, `ListRowsOptions`; `listRowsFromTimeSeries` /
340
+ `listRowsFromValueSeries` build one `ListRow` per event / axis key (numeric +
341
+ string columns land in `values`).
342
+
343
+ ### View builders (all in `packages/charts/src/data.ts`)
344
+
345
+ With a pond series, the components are the whole data contract (pass the
346
+ series directly) — these exports expose the chart-ready view shapes for
347
+ consumers writing custom draw code; no shipped layer needs their output.
348
+ The ValueSeries siblings (`fromValueSeries` etc.) are deliberately
349
+ **unexported** (adapters are internal; see [PND-VSADAPT]).
350
+
351
+ | Export | Signature gist | Feeds |
352
+ | -------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
353
+ | `fromTimeSeries` | `(series, column) → ChartSeries` | Line/Area/Scatter |
354
+ | `bandFromTimeSeries` | `(series, lower, upper) → BandSeries` | BandChart |
355
+ | `boxFromTimeSeries` | `(series, BoxColumns) → BoxSeries` | BoxPlot |
356
+ | `barsFromTimeSeries` | `(series, column) → BarSeries` | BarChart |
357
+ | `ohlcFromTimeSeries` | `(series, OhlcColumns) → OhlcSeries` | Candlestick |
358
+ | `stacksFromGroups` | `(Map<string, TimeSeries>, column) → StackedBarSeries` | Stacked bars from grouped series |
359
+ | `stacksFromColumns` | `(series, columns[]) → StackedBarSeries` | Stacked bars from wide columns |
360
+ | `barsFromBins` | `(bins, column, opts?) → BarSeries` | One-column histogram (single-series path, [PND-BARSEM]) |
361
+ | `stacksFromBins` | `(bins, columns[], opts?) → StackedBarSeries` | Multi-column histograms from `byColumn` output |
362
+ | `categoryStack` | `(CategoryDatum[]) → StackedBarSeries` | Categorical bars |
363
+ | `categoryStacks` | `(CategoryStackDatum[], columns) → StackedBarSeries` | Stacked categorical bars ([PND-CATSTACK]) |
364
+ | `bandedColor` | `(value, colors[], lo, hi) → string \| undefined` | The heat map's own banding — for a matching legend |
365
+ | `heatValueExtent` | `(StackedBarSeries) → [lo, hi] \| null` | Finite extent across a grid; `<HeatMap>`'s domain default |
366
+ | `transposeRow` | `(series, opts?) → CategoryDatum[]` | One row read across as categories |
367
+
368
+ Series shapes (same file): `ChartSeries`, `BandSeries`, `BoxSeries`,
369
+ `BarSeries`, `OhlcSeries`, `StackedBarSeries`; option types `BoxColumns`,
370
+ `OhlcColumns`, `BinRecord`, `StacksFromBinsOptions`, `CategoryDatum`,
371
+ `CategoryStackDatum`, `RowAt`,
372
+ `TransposeRowOptions`.
373
+
374
+ ### Theming
375
+
376
+ | Export | Purpose | Source |
377
+ | ------------------------------ | -------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
378
+ | `ChartTheme` | The one styling channel: role-keyed slots per draw layer + fixed chrome slots | `packages/charts/src/theme.ts` |
379
+ | `defaultTheme` / `estelaTheme` | Built-in themes (neutral light / dark estela palette) | `packages/charts/src/theme.ts` |
380
+ | `cssVarTheme` | `(base, resolve, opts?) → ChartTheme` — static CSS-custom-property overlay | `packages/charts/src/css-theme.ts` |
381
+ | `useChartTheme` | Hook: re-resolves on `data-theme`/`class` flips (MutationObserver) | `packages/charts/src/useChartTheme.ts` |
382
+ | Style types | `LineStyle`, `AreaStyle`, `BandStyle`, `ScatterStyle`, `BarStyle`, `BoxStyle`, `CandleStyle` | `packages/charts/src/theme.ts` |
383
+ | State types | `ScatterStates`, `BoxStates`, `BoxLadder`, `HeatStates` — the per-state sub-objects | `packages/charts/src/theme.ts` |
384
+ | Helper types | `ChartThemeOverrides`, `VarReader`, `UseChartThemeOptions` | `packages/charts/src/css-theme.ts`, `useChartTheme.ts` |
385
+
386
+ ### Live values, scales & key types
387
+
388
+ | Export | Purpose | Source |
389
+ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------------------- |
390
+ | `createLiveValue` / `LiveValue` | Imperative push channel for high-frequency indicator updates (isolated repaint) | `packages/charts/src/indicators.tsx` |
391
+ | `scaleTradingTime` / `TradingTimeScale` | Discontinuous time scale collapsing closed-market gaps | `packages/charts/src/tradingTimeScale.ts` |
392
+ | `DiscontinuityProvider` | Gap topology consumed by the trading-time scale | `packages/charts/src/tradingTimeScale.ts` |
393
+ | `scaleBand` / `ScaleBand` | Ordinal slot scale for the category axis | `packages/charts/src/bandScale.ts` |
394
+ | `GapMode` | `'none' \| 'empty' \| 'dashed' \| 'step' \| 'fade'` (Line/Area `gaps` prop) | `packages/charts/src/gaps.ts` |
395
+ | `DecimateOption` | `<LineChart decimate>` — M4 viewport decimation (`bool \| { threshold }`) | `packages/charts/src/decimate.ts` |
396
+ | `CursorMode` | `'none' \| 'line' \| 'point' \| 'inline' \| 'flag' \| 'crosshair' \| 'region'` | `packages/charts/src/context.ts` |
397
+ | `TrackerInfo` / `TrackerSample` | Hover readout payload (`onTrackerChanged`) | `packages/charts/src/context.ts` |
398
+ | `AnnotationKind` / `CreateSpec` | Annotation identity + draw-gesture payload (`onCreate`) | `packages/charts/src/context.ts` |
399
+ | `SelectInfo` | Selection/hover payload (`Selector`/`MultiSelector` `onSelect`/`onHover`) | `packages/charts/src/context.ts` |
400
+ | `SelectModifiers` | Keyboard modifiers on a click, 2nd arg to `onSelect` | `packages/charts/src/context.ts` |
401
+ | `SelectorProps` | `<Selector>`'s props — `enabled?` / `selected?` / `hovered?` / `onSelect?` / `onHover?` / `children?` | `packages/charts/src/selectors.tsx` |
402
+ | `MultiSelectorProps` | `<MultiSelector>`'s props — the above plus `sequence?`, with plural callbacks | `packages/charts/src/selectors.tsx` |
403
+ | `RangeSpan` | `<RangeCursor onDragRelease>` payload — `{ x: [lo, hi], y? }` in axis units | `packages/charts/src/context.ts` |
404
+ | `SpanSelection` | Range entry for `selected` — one layer's marks over `x`/`y`/`rows` (RFC A5.2) | `packages/charts/src/context.ts` |
405
+ | `SelectionEntry` | One `selected` array entry: `SelectInfo \| SpanSelection` | `packages/charts/src/context.ts` |
406
+ | `selectionContains` | Is a hit in a mixed selection? The same membership predicate the layers run | `packages/charts/src/span.ts` |
407
+ | `sameMark` | Are two hits the same mark? Full identity (`id`, `mark`-or-`key`, `label`) | `packages/charts/src/span.ts` |
408
+ | `isSpanSelection` | Entry discriminant — narrows a `SelectionEntry` to `SpanSelection` | `packages/charts/src/span.ts` |
409
+ | `DrawStatsFrame` / `LayerDrawInfo` | Per-repaint draw-cost + decimation stats (`ChartContainer` `onDrawStats`) | `packages/charts/src/context.ts` |
410
+ | `TimeGrain` | Coarse time unit for grain-aware formatting | `packages/charts/src/tickLadder.ts` |
411
+ | `SwatchSpec` / `LegendItemInput` | Legend swatch vocabulary + explicit-rows input (`<Legend items>`) | `packages/charts/src/swatch.ts` |
412
+ | `useChartLegend` | Headless legend hook: rows (items grouped by chart row) + `hover`/`select` verbs | `packages/charts/src/useChartLegend.ts` |
413
+ | `ChartLegend` / `LegendRow` / `LegendItem` | The hook's return shape (`rows` group `items`; items carry `selected`/`hovered`) | `packages/charts/src/useChartLegend.ts` |
414
+ | `useChartFrame` | Resolved plot geometry: plot rect, gutters, x scale, a row's y scales, band slot edges | `packages/charts/src/useChartFrame.ts` |
415
+ | `ChartFrame` / `ChartFrameRow` | The hook's return shape — container x half, plus a row y half that is `null` outside a `<ChartRow>` | `packages/charts/src/useChartFrame.ts` |
416
+ | `ChartBands` / `ChartBand` | Ordinal slot geometry on a category axis (`count`/`pitch`/`labels`/`at(i)`); `null` on time/value | `packages/charts/src/useChartFrame.ts` |
417
+ | `ChartXScale` | The union the container's shared x scale resolves to (time / linear / trading / band / elapsed) | `packages/charts/src/context.ts` |
418
+ | `LegendPlacement` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `packages/charts/src/Legend.tsx` |
419
+ | `Curve` | Path interpolation: `'linear' \| 'monotone' \| 'natural' \| 'basis' \| 'step'` | `packages/charts/src/curve.ts` |
420
+ | `RadiusEncoding` / `ColorEncoding` | Data-driven scatter size/colour | `packages/charts/src/encoding.ts` |
421
+ | `CandleVariant` / `ColorBy` | OHLC mark shape / colouring strategy | `packages/charts/src/ohlc.ts` |
422
+ | `AxisFormat` / `CursorFormat` | Tick and cursor-readout formatting (d3 specifier or fn) | `packages/charts/src/format.ts` |
423
+ | `AxisTransform` | Monotonic `to`/`from` pair for derived-unit x-axis relabeling | `packages/charts/src/derivedTicks.ts` |
424
+ | `Orientation` | Bar growth direction | `packages/charts/src/bars.ts` |
425
+
426
+ ---
427
+
428
+ ## @pond-ts/financial
429
+
430
+ ### Studies (each also a fluent method after `import '@pond-ts/financial/fluent'`)
431
+
432
+ All are pure `(series, options) → TimeSeries` appending output columns;
433
+ `column` defaults to `'close'`; periods are bar counts; warm-up is
434
+ length-preserving (`undefined` head rows).
435
+
436
+ | Study | Output column(s) | Options gist | Source |
437
+ | --------------------------- | ----------------------------------- | --------------------------------------------------- | -------------------------------------------------- |
438
+ | `sma` | `sma` | `{ period, column?, output? }` | `packages/financial/src/studies/moving-average.ts` |
439
+ | `ema` | `ema` | `{ period, column?, output? }` (α = 2/(period+1)) | `packages/financial/src/studies/moving-average.ts` |
440
+ | `bollinger` | `bbMiddle`, `bbUpper`, `bbLower` | `{ period, stdDev?, column?, prefix? }` | `packages/financial/src/studies/bollinger.ts` |
441
+ | `envelope` | `envMiddle`, `envUpper`, `envLower` | `{ period, percent?, maType?, column?, prefix? }` | `packages/financial/src/studies/envelope.ts` |
442
+ | `rollingStdev` | `stdev` | `{ period, column?, output? }` (population, ddof=0) | `packages/financial/src/studies/rolling-stat.ts` |
443
+ | `rollingMin` / `rollingMax` | `min` / `max` | `{ period, column?, output? }` (Donchian edges) | `packages/financial/src/studies/rolling-stat.ts` |
444
+ | `rollingPercentile` | `p{q}` (e.g. `p90`) | `{ period, q, column?, output? }` | `packages/financial/src/studies/rolling-stat.ts` |
445
+ | `zScore` | `zscore` | `{ period, column?, output? }` | `packages/financial/src/studies/z-score.ts` |
446
+ | `percentChange` | `pctChange` | `{ periods?, column?, output? }` | `packages/financial/src/studies/percent-change.ts` |
447
+
448
+ Adding a study? Follow `packages/financial/src/studies/README.md` (uniform
449
+ shape + pandas oracle case + fluent method are all REQUIRED).
450
+
451
+ ### Trading calendars & sessions
452
+
453
+ | Export | Purpose | Source |
454
+ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- |
455
+ | `TradingCalendar` | Query API: `.sessions()`, `.sessionOn()`, `.isTradingDay()`, `.isOpen()`, `.sessionsInRange()`, `.sessionSequence()`, `.barSequence(period)`, `.tagSessions()`, `.discontinuities()` | `packages/financial/src/calendar/` |
456
+ | `generateSessions` | `Session[]` from `SessionRules` over a date range (DST-correct) | `packages/financial/src/calendar/` |
457
+ | `normalizeSessions` | Validate + sort an explicit session list | `packages/financial/src/calendar/` |
458
+ | `identityDiscontinuity` / `segmentDiscontinuity` / `weekendSkip` | `DiscontinuityProvider`s for the trading-time axis | `packages/financial/src/calendar/` |
459
+ | Types | `Session`, `SessionBreak`, `SessionRules`, `DateRange`, `InstantRange`, `TaggedSchema`, `LiveSegment`, `DiscontinuityProvider` | `packages/financial/src/calendar/` |
460
+
461
+ ### Contract & constants
462
+
463
+ `OhlcvColumns` (column-name contract), `DEFAULT_OHLCV`
464
+ (`{ open, high, low, close, volume }`), `DEFAULT_SOURCE` (`'close'`) —
465
+ `packages/financial/src/contract/`. `RollingReducer` (reducer-name union used
466
+ by studies) — `packages/financial/src/kernels/rolling.ts`.
467
+
468
+ ---
469
+
470
+ ## @pond-ts/fit
471
+
472
+ | Group | Exports | Source |
473
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
474
+ | Activity model types | `ImportedActivity`, `ActivityMeta`, `ActivityStreams`, `Lap`, `GeoPoint`, `ActivitySource` | `packages/fit/src/types.ts` |
475
+ | Activity façade | `Activity` (`Activity.fromStreams(imported)`), `Section`, `ProfiledActivity`, `ProfiledSection`, `Sample`, `SectionMetrics` | `packages/fit/src/activity/` |
476
+ | Summary pipeline | `computeActivitySummary`, `prepareActivity` → `summaryFromPrepared` (reuse decode), `windowChannels` (zoom re-bucketing), `buildTrackFromStreams` (pond series from streams); types `ActivitySummary`, `PreparedActivity`, `ChannelProfile`, `ChannelSample`, `ChannelKey` | `packages/fit/src/summary/` |
477
+ | Track & geo | `Track` (`Track.of(points)`), `polylineCumulative`, `interpolateAtDistance`, `polylineSlice`, `boundsOf`, `bestEffortsByDistance`, `segmentsInRange` | `packages/fit/src/track/`, `packages/fit/src/geo/` |
478
+ | Power analytics | `computePower` (NP/IF/TSS/zones; `{ binWatts }` sets the histogram bucket width), `powerBestEfforts`; types `PowerSummary`, `PowerZone`, `PowerBin`, `PowerCurvePoint`, `PowerEffort`, `ComputePowerOptions`. `PowerBin`/`PowerZone` carry pond's canonical `start`/`end` bin edges, so they feed `@pond-ts/charts` unmapped | `packages/fit/src/power/` |
479
+ | Profile & zones | `Profile`, `hydrateProfile`, `profileAsOf`, `hrZonesFrom`, `paceZonesFrom`, `powerZonesFrom` (Coggan from FTP) | `packages/fit/src/profile/` |
480
+ | Zone distribution | `zoneDistributionByValue`, `hrZoneDistribution`, `paceZoneDistribution`, `ZoneTime` (canonical `start`/`end` edges + `openEnded`; chart-ready) | `packages/fit/src/zones/` |
481
+ | Quantities | Value objects with canonical units: `Distance`, `Elevation`, `Duration`, `Speed`, `Pace`, `Power`, `HeartRate`, `Cadence` | `packages/fit/src/quantities.ts` |
482
+ | Units | `convertDistance` / `convertElevation` / `convertTemperature` / `convertSpeed`, `metersToMiles`, `metersToFeet`, `formatDuration`, `formatPace`, `*UnitLabel` helpers, `DEFAULT_UNITS` | `packages/fit/src/units.ts` |
483
+
484
+ ---
485
+
486
+ ### `@pond-ts/financial/parallel` (Node-only, opt-in)
487
+
488
+ `withWorkers(series, { workers })` — opts a series into partitioned rolling
489
+ studies and returns it unchanged; `shutdownWorkers()`; `parallelDispatches()`;
490
+ `MIN_ROWS`; type `WithWorkersOptions`. Chosen **once at ingest**: the studies keep their
491
+ signatures and stay synchronous, and derived series inherit it (registration is
492
+ keyed on the key-column buffer). **Single-threaded remains the default** — the
493
+ main package never imports this. Node-only by construction: `Atomics.wait` on
494
+ the main thread is what keeps the studies synchronous, and browsers forbid it.
495
+
496
+ Accelerates any rolling study asking for `avg`/`stdev` off one column — `sma`,
497
+ `envelope`, `bollinger` — at 1.85×/1.35×/1.92×. **Partitioning does not change
498
+ the answer**: since [PND-PROCKERN] the kernel's accumulator rebuilds are pinned
499
+ to absolute row index, so a chunk reconstructs exactly the state a whole-column
500
+ pass held and the partitioned result is bit-identical. **`zScore` is not
501
+ accelerated**: [PND-SHIFTFRAME] moved it onto a shifted-frame kernel this pool
502
+ does not hook, so opting in neither speeds it up nor changes its answer. It used
503
+ to be the fastest entry here at 2.44×, and the only one whose error had no bound.
504
+ Below `MIN_ROWS` a registered series still runs sequentially and is
505
+ bit-identical. `parallelDispatches()` returns how many passes have actually run
506
+ on workers — acceleration is otherwise invisible, since a declined pass returns
507
+ the same answer, only slower than you expected. Source:
508
+ `packages/financial/src/parallel/`.
509
+
510
+ ## @pond-ts/process
511
+
512
+ **Experimental — published pre-1.0, API expected to move with friction
513
+ reports; pin an exact version.** The declarative plan layer is the consumer
514
+ surface (RFC [process.md](docs/rfcs/process.md)); the engine ships exported
515
+ beneath it — the [PND-PROCSUB] packaging decision, resolved at first publish.
516
+ Docs: [website/docs/process/](website/docs/process/). Tickets:
517
+ [PND_PROCESS_PLAN.md](docs/plans/PND_PROCESS_PLAN.md).
518
+
519
+ Typed dataflow graphs for pipelines whose **shape is data** (runtime-assembled,
520
+ user-edited, one computation fanned out to several consumers). Chaining stays
521
+ the default for pipelines known at authoring time — see the package README.
522
+
523
+ | Group | Exports | Source |
524
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
525
+ | Worker pool (Node) | `HostPool` (`start`, `run`, `close`, `size`, `inFlight`); types `HostPoolOptions`, `PoolSetup`, `PoolSetupConfig`; `toWire` / `fromWire`, types `WireResult`, `WireColumn` — subpath `@pond-ts/process/pool` | `packages/process/src/pool/index.ts` |
526
+ | Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
527
+ | Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
528
+ | Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
529
+ | 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` |
530
+ | Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
531
+ | 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` |
532
+ | 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` |
533
+ | 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` |
534
+ | 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` |
535
+ | Column values | `packColumn` (values → packed `Float64Column`, NaN = missing), `columnBytes` (retained size, for a byte budget), `appendColumn` (column → series; boxing-free when gapless), `columnBuffers` / `columnFromBuffers` (the buffer pair a column is, for an isolate boundary; type `ColumnBuffers`), `columnView` (zero-copy borrowed read view for in-process folds; type `ColumnView`) | `packages/process/src/column.ts` |
536
+ | Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams`, `byFamily`, `describe`, `toJsonSchema`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError` | `packages/process/src/plan/registry.ts`, `params.ts` |
537
+ | Plan — identity | `specId` (content-addressed, param-order invariant, defaults materialized), `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey` | `packages/process/src/plan/identity.ts` |
538
+ | Plan — types | `Spec`, `Plan`, `Input` (column name \| `Spec` \| `PickedOutput`), `SpecRef`, `Def` (`OpDef` \| `FoldDef`), `OpContext`, `OpResult`, `FoldContext`, `FactBody`, `isFold`, `ParamDef`, `Params`, `Units`, `InputDef`, `OutputDef` | `packages/process/src/plan/types.ts` |
539
+ | Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError` | `packages/process/src/plan/graph.ts`, `run.ts` |
540
+ | Plan — request/response | `RunRequest` (`PlanRequest` \| `SlotRequest`), `RunOptions`, `RunResult`, `Select` (`{ on, output?, name? }` — points at a node; what comes back is what that node produces), `ErrorPolicy`, `Fact` (carries `op`), `OutputInfo`, `Skipped`, `NodeTiming` (`slot`, `pulled`, `cached`, `ms`, `inputs`) | `packages/process/src/plan/run.ts` |
541
+ | Plan — host | `createHost({ registry, units, sources })` → `Host` (`add`, `has`, `datasets`, `graphFor`, `run`, `runAsync`), `toWire`, `UnknownDatasetError`; local-string `Envelope` (`PlanEnvelope` \| `SlotEnvelope`), remote-capable `AsyncEnvelope` (`AsyncPlanEnvelope` \| `AsyncSlotEnvelope` \| `Envelope`), `DatasetInfo`, `WireResult` | `packages/process/src/plan/host.ts` |
542
+ | Plan — slots | `expandSlots(slots, columns)` → `Map<slot, Spec>` (expands to the nested form, so ids match by construction; `slot#Output` picks one output), `SlotError`; types `SlotDef` (`{ op, params, in }`), `Slots` | `packages/process/src/plan/slots.ts` |
543
+ | Plan — builder | `plan(from)` → low-level `PlanBuilder`; `process(registry, from)` → typed fluent `ProcessBuilder` (`column`, op methods, `outputs`), `BuilderError`; types `NodeHandle`, `OutputHandle`, `FluentColumnRef`, `SingleColumnNode`, `MultiColumnNode`, `ColumnSelection`, `FactRef`, `BuiltRequest` | `packages/process/src/plan/builder.ts`, `fluent.ts` |
544
+ | Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
545
+ | Plan — folds | `STANDARD_FOLDS` and the four it holds — `last`, `extremes`, `percentileRank`, `shape` — pre-registered by `createRegistry()`; each a plain `FoldDef`, so a consumer can `define` over one | `packages/process/src/plan/folds.ts` |
546
+ | Errors | `ProcessError` (base), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
547
+ | Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
548
+
549
+ Note: this package's `npm test` includes a `test:dts` step that typechecks the
550
+ **emitted** `dist/*.d.ts` from a consumer's perspective (`test-dts/`,
551
+ `skipLibCheck: false`). The package's own build sets `skipLibCheck: true` and
552
+ never checks its own output, so a declaration referencing a type `stripInternal`
553
+ deleted builds green and breaks only downstream. If you mark something
554
+ `@internal`, confirm no public signature names it.
555
+
556
+ ---
557
+
558
+ ## Cross-package seams (where agents most often need the joint)
559
+
560
+ - **Batch → charts**: a draw layer takes a pond `series` + `column` directly;
561
+ the `data.ts` adapters are the explicit versions of what layers do
562
+ internally. Histogram path: `series.byColumn(...)` → `stacksFromBins(...)` →
563
+ `<BarChart bins>`.
564
+ - **Live → react → charts**: `LiveSeries` → `useSnapshot`/`useWindow` →
565
+ the same layer props a batch chart uses (no separate live-mode API).
566
+ - **Financial → charts**: `TradingCalendar.discontinuities()` →
567
+ `ChartContainer calendar` (trading-time axis); studies append columns that
568
+ `LineChart`/`BandChart` draw (`bbUpper`/`bbLower` → `BandChart`).
569
+ - **Core → financial**: studies compose on core kernels; fluent methods mutate
570
+ `TimeSeries.prototype` (runtime import of `@pond-ts/financial/fluent`
571
+ required).
572
+ - **Live → process**: `fromLive(liveSeries)` binds a live source as a graph
573
+ input. Events only mark the node dirty; the snapshot runs once at the next
574
+ pull, so per-event incremental work stays in the live layer and the graph
575
+ composes batch transforms over snapshots. The graph has **no partial
576
+ invalidation** — a dirty node recomputes from a whole snapshot — so for
577
+ windowed work bind the _aggregation_ (`fromLive(live.aggregate(...))`),
578
+ which materializes bucket count rather than event count (235x per pull on
579
+ a 50k buffer). Tradeoff: a live aggregation exposes closed buckets only,
580
+ so the in-progress bucket is invisible until it closes.
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.58.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.60.0...HEAD
12
+ [0.60.0]: https://github.com/pond-ts/pond/compare/v0.59.0...v0.60.0
13
+ [0.59.0]: https://github.com/pond-ts/pond/compare/v0.58.0...v0.59.0
12
14
  [0.58.0]: https://github.com/pond-ts/pond/compare/v0.57.0...v0.58.0
13
15
  [0.57.0]: https://github.com/pond-ts/pond/compare/v0.56.2...v0.57.0
14
16
  [0.56.2]: https://github.com/pond-ts/pond/compare/v0.56.1...v0.56.2
@@ -61,6 +63,342 @@ include new features and type-level changes; patch bumps are strictly additive.
61
63
 
62
64
  ## [Unreleased]
63
65
 
66
+ ## [0.60.0] — 2026-08-13
67
+
68
+ ### Added
69
+
70
+ - `@pond-ts/charts`: `<AreaChart thresholds>` + `bandColors` — threshold
71
+ banding along the area's height ([PND-BANDAREA]), the area counterpart of
72
+ `<BarChart thresholds>`. `n` breakpoints (absolute data values, magnitude-
73
+ mirrored below zero) make `n + 1` bands; fills resolve `bandColors` → the
74
+ new `AreaStyle.bands` theme token (the default theme ships the same
75
+ teal/amber/red ladder as the bar role). One hard-stop pixel-space gradient
76
+ carries the ladder for the fill **and** the outline, so the value line
77
+ switches hue exactly at each crossing and the banded area keeps one hit
78
+ region, one legend row and one readout identity. Composes with `curve`,
79
+ `gaps` and M4 decimation unchanged; a swept window keeps the band colours
80
+ (no `spanColor` swap). Ladder resolution + dev warnings shared with
81
+ `<BarChart>` via one internal hook.
82
+ - **charts: `<ChartContainer xScale="log" | "symlog">` — a logarithmic value x
83
+ axis** ([#649]). The x counterpart of `<YAxis scale>`, for a quantity spanning
84
+ orders of magnitude — a power–duration curve is watts against 1s · 5s · 1m ·
85
+ 20m · 3h, which is unreadable on a linear x.
86
+
87
+ ```tsx
88
+ <ChartContainer range={[1, 10800]} xScale="log">
89
+ ```
90
+
91
+ **Why the container and not `<XAxis scale>`**, where the `<YAxis>` mirror would
92
+ put it: there is one x scale shared by every row and the container builds it,
93
+ while every `<XAxis>` prop is presentational (`format`, `label`, `side`,
94
+ `ticks`, `align`, …). `<YAxis>` is the opposite — one scale per axis per row,
95
+ declared by the axis, which is why `min`/`max`/`pad`/`scale` live there. It
96
+ sits beside `origin`, `spacing` and `calendar`, which shape the same scale.
97
+
98
+ Ignored on a time or category axis. A `'log'` domain reaching zero **falls back
99
+ to linear and warns** rather than silently clamping — `log(0)` is undefined;
100
+ use `'symlog'` for data that crosses zero.
101
+
102
+ Nothing downstream branches: d3's log scales share the continuous-scale
103
+ surface, so draw layers are untouched. The tick ladder is the one the y axis
104
+ already built (`tickValues`, renamed from `yTickValues` now that both axes use
105
+ it) — d3's raw `scaleLog.ticks()` is nearly a step function.
106
+
107
+ [#649]: https://github.com/pond-ts/pond/issues/649
108
+
109
+ - **charts: `<BarList barColors>` — per-row bar colour** ([#650]). The list's
110
+ counterpart to `<BarChart binColors>`: `barColors[i]` aligned to the rows you
111
+ passed, an `undefined` or short entry falling back to the column's `as` /
112
+ theme fill.
113
+
114
+ ```tsx
115
+ <BarList rows={zones} columns={[{ column: 'frac' }]} barColors={ZONE_RAMP} />
116
+ ```
117
+
118
+ For the case `binColors` was built for and a list could not do — a zone table
119
+ where each row's bar carries its own step of a ramp. A `BarListColumn`'s
120
+ single `as` paints every row the same colour, so the ramp had to move onto the
121
+ row label, putting it on the wrong element: the bar is the natural carrier of
122
+ a magnitude.
123
+
124
+ **Colours key on the row, not its render position**, so they follow the data
125
+ through a `sortBy` rather than repainting the ramp onto whichever rows now sit
126
+ in those slots.
127
+
128
+ **A per-row colour makes the fill load-bearing, so its state treatment stands
129
+ down** — a coloured bar keeps its own colour while selected, and shows the
130
+ state in opacity instead. That is the rule a multi-metric row already
131
+ followed, and the one `binColors` follows on the canvas: recolouring a bar
132
+ that means something trades a distinction the reader needs for one the band
133
+ and rail already give them.
134
+
135
+ [#650]: https://github.com/pond-ts/pond/issues/650
136
+
137
+ - **charts: `<ChartContainer categories>` — the ordinal axis as a
138
+ container-level choice** ([PND-IGNITECAT]). Declare the slot names on the
139
+ container and **any value-keyed layer can live on them** — a target line, a
140
+ point mark or a filled envelope over categorical bars, which was previously
141
+ not expressible at all.
142
+
143
+ The band scale used to be reachable only _through a layer_: `<BarChart
144
+ categories>` and a **horizontal** heat map reported `xKind: 'category'`, every other layer
145
+ reported `'time'` or `'value'`, and a container throws on a mixed kind. The
146
+ workaround — key every layer to a synthetic integer index and hand-supply the
147
+ tick labels — forfeits two features the ordinal axis already implements, and
148
+ both came back as their own friction entries: `<XAxis>` label thinning (gated
149
+ on a category axis with no custom ticks) and the `maxBandWidth` / `bandAlign`
150
+ slot packing. Declaring the categories on the container keeps both.
151
+
152
+ The change is small because the scale was already built for it: `scaleBand`'s
153
+ domain is **numeric** (`[0, n]`, slot `i` at `[i, i+1]`) with a linear pixel
154
+ mapping, so a `ValueSeries` keyed on slot coordinates already lands where the
155
+ bars do. **Slot `i`'s centre is `i + 0.5`** — the same number
156
+ `ScaleBand.ticks()` returns and where `<XAxis>` puts the tick.
157
+
158
+ Two things error, deliberately: a **time-keyed layer** (a timestamp has no
159
+ slot), and a **category layer that disagrees** with the prop in content or
160
+ order — the prop is authoritative, and a silent mismatch would draw bars
161
+ under the wrong labels. The pre-existing mixed-kind error now names the prop
162
+ as the fix.
163
+
164
+ Declaring it has two costs, both already true of an inferred category axis
165
+ and now reachable from a previously-continuous container: **x pan and zoom
166
+ stop** (`panZoom` keeps working on y), and **`range` stops applying to x**
167
+ (the domain is `[0, n]` from the slot count, so an x range is a no-op).
168
+
169
+ One hazard is documented rather than enforced: **a value-keyed layer is taken
170
+ at its word**, so a layer whose x means something other than a slot
171
+ coordinate — a horizontal categorical `<BarChart>`, whose x is bar _length_ —
172
+ will draw in the wrong place. A guard for that case was written and removed
173
+ after review: it tested `binCategories`, which is the generic "my _y_ is
174
+ ordinal" channel that a **vertical heat map** sets for its rows, so it
175
+ rejected a slot-keyed grid with named columns on x — a wanted layout, since
176
+ ordinal rows plus ordinal columns is just a 2-D grid. Nothing distinguishes
177
+ "my x is a coordinate" from "my x is a magnitude", so there is no
178
+ contradiction to detect.
179
+
180
+ `categories={[]}` is an ordinal axis with **no slots yet**, not a fallback to
181
+ time — so the kind doesn't flip and rebuild every scale when data arrives.
182
+
183
+ Omitting `categories` leaves the inferred behaviour exactly as it was.
184
+
185
+ - **charts: `useChartFrame()` — the resolved plot geometry, published**
186
+ ([PND-IGNITEFRAME]). A hook returning what the container already worked out:
187
+ the plot rect (`plot.x` / `plot.width`), the reserved axis `gutters`, the
188
+ shared `xScale` and its `xKind`, a row's `yScales` and top inset, and — on a
189
+ category axis — the ordinal slot edges (`bands.at(i)`, `pitch`, `labels`).
190
+
191
+ Consumers aligning DOM chrome to the plot (per-slot header tables, column
192
+ summary strips, cards pinned over a band, a colour ramp keyed to the plot's
193
+ own scale) previously had to re-derive all of it: pin every axis gutter to a
194
+ fixed width so it stops depending on label content, measure the outer box,
195
+ subtract, and re-implement the band packing. **That duplicate is not merely
196
+ verbose — it is wrong over time.** It holds only until the library changes
197
+ how a gutter is sized or how bands are packed, at which point the chrome
198
+ slides out of alignment with the plot it labels, with no type error and no
199
+ failing test.
200
+
201
+ Two shape notes. **The x/y split is the library's own** — the container owns
202
+ one shared x scale, rows own their y scales — so `plot` carries x and `row`
203
+ carries y, and `row` is `null` when the hook is called outside a
204
+ `<ChartRow>`. That `null` is deliberate: the common case (a header strip
205
+ beside the rows) genuinely has no y geometry, and reporting `height: 0`
206
+ instead would be the same silent misalignment the hook exists to remove.
207
+ **Scope follows placement**, exactly as `useChartLegend` already does.
208
+
209
+ `useChartLegend`'s `gutters` is unchanged and still the right call for a
210
+ legend; it now documents `useChartFrame()` as the fuller surface. It was the
211
+ only geometry the library published, for one consumer, on a hook named for
212
+ something else — which is why this exists.
213
+
214
+ - **charts: `<ChartContainer width="auto">` — fill the available width**
215
+ ([PND-WIDTH]). `width` now accepts `'auto'`, and an omitted `width` means the
216
+ same; a number still skips the measure pass and paints on the first render.
217
+ The container renders a plain full-width box, measures it with a
218
+ `ResizeObserver`, and mounts the chart at that pixel width — the canvas
219
+ renderer needs real pixels to lay out ticks and slots, so this is measurement
220
+ moved inside the library rather than a percentage handed to a canvas. Nothing
221
+ paints until a real width exists.
222
+
223
+ This is the shipped
224
+ [responsive-width recipe](https://pond-ts.github.io/pond/docs/recipes/responsive-width)
225
+ become the implementation, and it closes that recipe's sharpest edge by
226
+ construction: the measured box is one the library owns, so it can never be
227
+ the caller's padded or bordered box (whose border-box width overflowed the
228
+ chart by exactly the padding, silently clipped when the box also hid
229
+ overflow). Style your own wrapper freely.
230
+
231
+ Two behaviours worth knowing. A container **hidden** by an ancestor's
232
+ `display: none` keeps the last width it measured and stays mounted, so a tab
233
+ switch does not discard pan/zoom position, selection or hover — writing the
234
+ zero measurement through would unmount and rebuild all of it. And `'auto'`
235
+ needs a parent with a **definite** width: a parent sized by its own content
236
+ (a float, an `inline-block`, a grid `auto` track, a flex child without
237
+ `min-width: 0`) measures 0, and the chart is the content that would have
238
+ given it a width, so the chart stays blank with no error.
239
+
240
+ Three independent consumers reported the explicit-pixel requirement; the
241
+ third was multiplying the same ~25-line measure-and-gate hook across seven
242
+ panes.
243
+
244
+ ### Changed
245
+
246
+ - **charts: `ChartContainerProps.width` is now `number | 'auto'` and optional**
247
+ (was a required `number`). Strictly additive for callers passing a number.
248
+
249
+ ### Fixed
250
+
251
+ - **charts: value-axis pan and zoom no longer snap to whole integers**
252
+ (shipped inside [#653]). `panRange`/`zoomRange` were written for a
253
+ millisecond axis and silently assumed every axis was one, so a value domain
254
+ of `[0.5, 10800]` snapped its floor to `0` and `[0.001, 1]` collapsed to
255
+ `[0, 1]` — fatal under a log axis, where `log(0)` is undefined. Gestures now
256
+ snap only on a **time** axis (`ViewportOptions.snap`), and a log x axis pans
257
+ by ratio and zooms in log space, keeping the value under the cursor fixed.
258
+ Time-axis behaviour is unchanged.
259
+
260
+ ## [0.59.0] — 2026-08-11
261
+
262
+ ### Added
263
+
264
+ - **charts: `<YAxis scale="symlog">` — linear through zero, logarithmic beyond**
265
+ ([PND-SYMLOG]). The third `scale` kind, for a **diverging** measure spanning
266
+ orders of magnitude on both sides of zero. `scale="log"` cannot express that
267
+ domain at all (no zero, no negatives) and `scale="linear"` flattens everything
268
+ outside the top decade onto the axis line — so the small and mid-range values,
269
+ usually the finding, become unreadable.
270
+
271
+ The knee is set by the new **`linearWindow`** prop as a _fraction of the
272
+ domain's largest magnitude_ (default `0.02`): on a ±1M domain the axis is
273
+ linear through ±20k and logarithmic beyond. Relative rather than absolute so
274
+ it survives a domain change with no arithmetic at the call site. Values are
275
+ strictly monotonic across the knee, and zero has a real position. A fraction
276
+ outside `(0, 1]` is unusable as a knee, so the axis draws with the default and
277
+ dev-warns which window is in force.
278
+
279
+ **The tick ladder is pond's, not d3's.** `scaleSymlog` supplies the transform
280
+ but ticks it _linearly_, which puts every label in the top decade and none in
281
+ the linear window the scale exists to open up. `<YAxis scale="symlog">` grids
282
+ zero, ±the knee, and mirrored decades beyond it, thinned to the tick budget
283
+ the same way the log path thins its decades, clipped to the domain. When
284
+ `linearWindow` swallows the domain there is nothing left to grid
285
+ logarithmically, and the axis defers to the linear ticks — which is correct,
286
+ not a fallback: inside the knee, symlog _is_ linear.
287
+
288
+ It removes a workaround whose cost was **silence**: pre-transforming values
289
+ into a ±1 plot space with a linear axis pinned to `[-1, 1]` leaves tick
290
+ positions in plot space while their labels must read in real units, so
291
+ computing the two by different routes yields a chart that confidently labels
292
+ positions it does not occupy — no exception, no visual artifact.
293
+
294
+ **If you are replacing a hand-rolled curve, the shape will shift.** `symlog` is
295
+ the single smooth `sign(x) · log1p(|x / knee|)`, not two joined segments; a
296
+ hand-rolled curve that is exactly linear below the knee and `log10` above is the
297
+ same family with a different shape. Migrating one, a consumer measured small
298
+ values at **roughly half** their former height (on a ±9M domain, 283k moved from
299
+ 0.44 to 0.24 of the half-plot above the zero line) with order, tail dominance and
300
+ the several-fold lift over a linear axis all preserved. No `linearWindow` recovers the piecewise shape — the
301
+ difference is the curve, not the knee.
302
+
303
+ - **charts: `<BarChart maxBarWidth>` — cap a bar's ink independently of its slot**
304
+ ([PND-BARWIDTH]). Applied after the `gap` inset and centred in the slot, with
305
+ `theme.bar[as].maxWidth` as the fallback (the same relationship `gap` has) and
306
+ uncapped when neither is set.
307
+
308
+ It is the **absolute** half of the width vocabulary. `gap` is _relative_, so
309
+ with it alone bar width is always `slot - gap` and fattens as the plot widens
310
+ — and a fixed ink width is what makes a measure comparable **between** panes,
311
+ since bars that widen with their pane read as different weights of the same
312
+ thing. Neither existing spelling expresses "spread the slots, pin the bar":
313
+ `maxBandWidth = barWidth + gap` pins the bar but stops the slots spreading,
314
+ and `maxBandWidth = slotCap` spreads them but lets the bar grow. The
315
+ workaround was to compute `gap` from the band width you predicted the library
316
+ would pick — a re-derivation of pond's layout arithmetic in consumer code,
317
+ which goes silently wrong the moment that rule changes on either side.
318
+
319
+ Pairs with `<ChartContainer maxBandWidth>` (which caps the **slot**) and
320
+ `minWidth` still wins if the two bounds would invert. **A single-series bar's
321
+ hit target stays its whole slot**, so narrow ink costs nothing in clickability;
322
+ on a **stacked** chart the cap does narrow the target, because a stack must
323
+ hit-test its drawn segment rect to resolve which segment.
324
+
325
+ - **charts: `<BarChart categories columns>` — a first-class stacked category
326
+ chart** ([PND-CATSTACK]). Each datum is `{ label, values }` and `columns`
327
+ names the groups to stack bottom → top, the same relationship
328
+ `series` + `columns` already has. New `categoryStacks` reader and
329
+ `CategoryStackDatum` type; geometry, `marks` and the categorical axis are
330
+ unchanged from the single-value case, so this reaches the shipped
331
+ `drawStacks` path with no new draw code. A missing or non-finite group reads
332
+ as a **gap**, not a zero.
333
+
334
+ **It removes a workaround with three costs**, the third only visible since
335
+ 0.58.0: composing the picture from one `categories` layer per _cumulative
336
+ total_ (drawn outermost-first so each overpaints the one beneath) meant a
337
+ hand-assembled legend, label thinning blind to the sibling layers, and — because
338
+ a selection entry keys on `(layer id, mark)` — a controlled set replicated
339
+ across every segment layer, where missing one made a selected bar recede
340
+ **from the waist up**. Because `marks` is indexed by **bin**, one entry naming
341
+ `(id, mark)` now matches every segment of a bar, so that failure is not
342
+ expressible rather than merely fixed.
343
+
344
+ ### Fixed
345
+
346
+ - **charts: a selected segment of a stack with `colors` no longer collapses to
347
+ the flat `highlight`.** `StackStyle.groupColored` — the "a selected segment
348
+ keeps its own fill" exclusion — was gated on the _theme ramp_ having painted
349
+ the stack, so a call site passing `colors` lost it and both segments of a
350
+ selected bar went one `highlight` blue, losing the segment distinction exactly
351
+ where the reader is looking. The gate's stated reason ("a ramp entry the call
352
+ site overrode is no longer the ramp's colour, so its receded counterpart would
353
+ be wrong") applies to the _derived_ companions `dimmedFills` / `hoverFills`,
354
+ which must invent a per-group colour; `groupColored` derives nothing. It now
355
+ gates on **whether the resolved fills actually differ**, so a `colors` map keeps
356
+ its colours under selection, while a multi-group stack under a theme that gives
357
+ its groups no distinct colours at all (no ramp, no roles, no `colors` — e.g.
358
+ `estelaTheme`) still takes the themed `highlight`, because there is no
359
+ meaning-carrying colour there to preserve and suppressing the highlight would
360
+ leave selection invisible. Found building [PND-CATSTACK], where the old gate made
361
+ the first-class stack render _worse_ under selection than the workaround it
362
+ replaces; the second half was found in review, since every story and test renders
363
+ `defaultTheme`, whose ramp hides the difference.
364
+
365
+ - **all packages: `API.md` now ships inside the npm tarball.** The agent-facing
366
+ map of every public export across the six packages — one line per export with
367
+ its purpose and source path — was repo-only, so an agent working in a
368
+ _consuming_ repo had to crawl `node_modules/*/dist/*.d.ts` or go to the
369
+ network to learn the surface. It is now copied in by each package's existing
370
+ `prepack` (the same mechanism that already ships `README`, `LICENSE` and
371
+ `CHANGELOG`) and listed in `files`. ~69kB per tarball.
372
+
373
+ Every package carries the same **monorepo-wide** copy rather than a
374
+ per-package slice, deliberately: the packages compose, and knowing what is
375
+ next door is most of the value. The header now names its audience and
376
+ resolves repo-relative source paths against GitHub, since inside
377
+ `node_modules` a bare `packages/core/src/…` points nowhere.
378
+
379
+ - **charts: `BarStyle.dimmed`'s precedence over per-bar and per-band colour is
380
+ documented, and pinned.** A consumer migrating onto 0.58.0 read
381
+ `BarStyle.hover`'s documented `binColors` exclusion ("pops each bar's _own_
382
+ fill"), reasonably generalized it to `dimmed`, concluded their `binColors` and
383
+ `thresholds` charts would get no de-emphasis, and was about to hand-dim inside
384
+ their own colour arrays. The opposite is true: **an unselected bar takes
385
+ `dimmed`, discarding its per-bar colour, and a banded bar draws flat rather
386
+ than dimming each band.** The asymmetry is deliberate — emphasis preserves a
387
+ per-bar colour because that colour is what the value means, while a receded
388
+ bar's job is to stop competing over meaning — but only `hover` said anything,
389
+ so generalizing was the natural read. `dimmed` now spells out all three paths
390
+ (`binColors`/`binFills`, bands/thresholds, and the per-group stack fallback
391
+ through `StackStyle.dimmedFills`), `hover` scopes its exclusion to the live
392
+ states, and three tests pin the behaviour.
393
+
394
+ - **charts: the `theme.list` register no longer reads as if it carries
395
+ `dimmed`.** Its doc mentioned `highlight`/`dimmed` while explaining that a
396
+ list resolves glyph state through the bar tokens — accurate, but sitting in a
397
+ sentence about per-metric resolution it read as a field list, and cost the
398
+ same consumer a couple of passes to rule out a missing `list.dimmed`. Now
399
+ states explicitly that those are `BarStyle` tokens resolved via
400
+ `theme.bar[as]`, and that this register carries exactly its five values.
401
+
64
402
  ## [0.58.0] — 2026-08-10
65
403
 
66
404
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.58.0",
3
+ "version": "0.60.0",
4
4
  "description": "React hooks for pond-ts live time series",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,17 +23,18 @@
23
23
  },
24
24
  "files": [
25
25
  "dist",
26
- "CHANGELOG.md"
26
+ "CHANGELOG.md",
27
+ "API.md"
27
28
  ],
28
29
  "scripts": {
29
30
  "build": "tsc -p tsconfig.json",
30
- "prepack": "cp ../../README.md ./README.md && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build && cp cjs-fallback.cjs dist/cjs-fallback.cjs && find dist -name '*.map' -delete",
31
+ "prepack": "cp ../../README.md ./README.md && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && cp ../../API.md ./API.md && npm run build && cp cjs-fallback.cjs dist/cjs-fallback.cjs && find dist -name '*.map' -delete",
31
32
  "test": "npm run test:type && npm run test:runtime",
32
33
  "test:type": "tsc -p tsconfig.types.json",
33
34
  "test:runtime": "vitest run"
34
35
  },
35
36
  "peerDependencies": {
36
- "pond-ts": "^0.58.0",
37
+ "pond-ts": "^0.60.0",
37
38
  "react": "^18.0.0 || ^19.0.0"
38
39
  },
39
40
  "devDependencies": {