@pond-ts/fit 0.69.0 → 0.71.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 (4) hide show
  1. package/AGENTS.md +3 -1
  2. package/API.md +55 -56
  3. package/CHANGELOG.md +177 -1
  4. package/package.json +2 -2
package/AGENTS.md CHANGED
@@ -159,6 +159,7 @@ import {
159
159
  Layers,
160
160
  LineChart,
161
161
  YAxis,
162
+ CrosshairCursor,
162
163
  } from '@pond-ts/charts';
163
164
 
164
165
  const [live, snap] = useLiveSeries({
@@ -167,7 +168,8 @@ const [live, snap] = useLiveSeries({
167
168
  retention: { maxAge: '10m' },
168
169
  });
169
170
 
170
- <ChartContainer width={800} cursor="crosshair" panZoom>
171
+ <ChartContainer width={800} panZoom>
172
+ <CrosshairCursor />
171
173
  <ChartRow height={240}>
172
174
  <YAxis id="ms" />
173
175
  <Layers>
package/API.md CHANGED
@@ -251,17 +251,17 @@ Types: `UseSnapshotOptions`, `SnapshotSource` (structural — covers
251
251
 
252
252
  ### Components — layout & axes
253
253
 
254
- | Component | Key props | Purpose | Source |
255
- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
256
- | `ChartContainer` | `width`, `range?`, `theme?`, `cursor?`, `panZoom?`, `xScale?`, `bounds?`, `showAxis?`, `calendar?`, `timeZone?`, `origin?`, `maxBandWidth?`/`bandAlign?`, `onTrackerChanged?`, `onDrawStats?` | Root: shared x-scale, interactions, annotations; `timeZone` renders the time axis in an IANA zone (default: viewer-local; a `calendar.timeZone` supplies the default) | `packages/charts/src/ChartContainer.tsx` |
257
- | `ChartRow` | `height`, `cursor?` (deprecated — mount a cursor in the row) | One stacked plot band; owns its y-axes | `packages/charts/src/ChartRow.tsx` |
258
- | `Layers` | children | Mandatory z-stack inside a row (back-to-front) | `packages/charts/src/Layers.tsx` |
259
- | `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` |
260
- | `XAxis` | `side?`, `label?`, `format?`, `ticks?`, `transform?`, `dateStyle?`, `timeZone?` (this strip in another IANA zone) | Placeable x-axis strip; kind inferred from data | `packages/charts/src/XAxis.tsx` |
261
- | `TimeAxis` / `CategoryAxis` | (XAxis props) | Thin `XAxis` presets | `packages/charts/src/TimeAxis.tsx`, `CategoryAxis.tsx` |
262
- | `Canvas` | `width`, `height`, `draw` | Low-level DPR-aware canvas primitive | `packages/charts/src/Canvas.tsx` |
263
- | `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` |
264
- | `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` |
254
+ | Component | Key props | Purpose | Source |
255
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
256
+ | `ChartContainer` | `width`, `range?`, `theme?`, `panZoom?`, `xScale?`, `bounds?`, `showAxis?`, `calendar?`, `timeZone?`, `origin?`, `maxBandWidth?`/`bandAlign?`, `onTrackerChanged?`, `onDrawStats?` | Root: shared x-scale, interactions, annotations; `timeZone` renders the time axis in an IANA zone (default: viewer-local; a `calendar.timeZone` supplies the default) | `packages/charts/src/ChartContainer.tsx` |
257
+ | `ChartRow` | `height` | One stacked plot band; owns its y-axes | `packages/charts/src/ChartRow.tsx` |
258
+ | `Layers` | children | Mandatory z-stack inside a row (back-to-front) | `packages/charts/src/Layers.tsx` |
259
+ | `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` |
260
+ | `XAxis` | `side?`, `label?`, `format?`, `ticks?`, `transform?`, `dateStyle?`, `timeZone?` (this strip in another IANA zone) | Placeable x-axis strip; kind inferred from data | `packages/charts/src/XAxis.tsx` |
261
+ | `TimeAxis` / `CategoryAxis` | (XAxis props) | Thin `XAxis` presets | `packages/charts/src/TimeAxis.tsx`, `CategoryAxis.tsx` |
262
+ | `Canvas` | `width`, `height`, `draw` | Low-level DPR-aware canvas primitive | `packages/charts/src/Canvas.tsx` |
263
+ | `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` |
264
+ | `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` |
265
265
 
266
266
  ### Components — draw layers
267
267
 
@@ -303,25 +303,24 @@ schema columns). Because the union splits per series _kind_, a value typed as
303
303
 
304
304
  ### Components — cursors (mounted presets)
305
305
 
306
- The `cursor` string modes as components (interaction RFC §4/A4.1) — mount one
307
- as a child of `<ChartContainer>` (the default for every row) or inside a
308
- `<ChartRow>` (the per-row override). Render-only presets stack; one
309
- gesture-owning cursor (`Crosshair`/`Range`) per scope. The `cursor` /
310
- `cursorTime` / `crosshairSnap` / `cursorFormat` / `cursorSequence` /
311
- `onRegionSelect` / `regionSelectModifier` props (and `<ChartRow cursor>`) are
312
- **deprecated** — they keep working for one minor via an internal shim. The
313
- underlying `CursorSpec` contract stays unpublished (Q3); every drag claim on
314
- the plot (annotation-create, the range drag, pan) is arbitrated by one brush
315
- recognizer with a documented precedence (`src/brush.tsx`, RFC A1.5/A2.7).
316
-
317
- | Component | Key props | Purpose | Source |
318
- | ----------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
319
- | `LineCursor` | `showTime?` | The synced vertical line (`cursor="line"`, the legacy default) | `packages/charts/src/cursors.tsx` |
320
- | `PointCursor` | `showTime?` | A dot on each series at the cursor (`"point"`) | `packages/charts/src/cursors.tsx` |
321
- | `InlineCursor` | `showTime?` | Dots + a value chip beside each (`"inline"`) | `packages/charts/src/cursors.tsx` |
322
- | `FlagCursor` | `showTime?` | Dots + staffed value flags stacked at the top (`"flag"`) | `packages/charts/src/cursors.tsx` |
323
- | `CrosshairCursor` | `snap?`, `showTime?`, `format?` | The inspection reticle: dashed cross, y value pill, x time pill (`"crosshair"`) | `packages/charts/src/cursors.tsx` |
324
- | `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` |
306
+ Cursor presets (interaction RFC §4/A4.1). **A chart shows a cursor only when
307
+ one is mounted** — mount one as a child of `<ChartContainer>` (the default for
308
+ every row) or inside a `<ChartRow>` (the per-row override). Render-only
309
+ presets stack; one gesture-owning cursor (`Crosshair`/`Range`) per scope. The
310
+ first mounted cursor that sets `format` shapes the chart's readout channel.
311
+ The underlying `CursorSpec` contract stays unpublished (Q3); every drag claim
312
+ on the plot (annotation-create, the range drag, pan) is arbitrated by one
313
+ brush recognizer with a documented precedence (`src/brush.tsx`, RFC
314
+ A1.5/A2.7).
315
+
316
+ | Component | Key props | Purpose | Source |
317
+ | ----------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- |
318
+ | `LineCursor` | `showTime?`, `format?` | The synced vertical line | `packages/charts/src/cursors.tsx` |
319
+ | `PointCursor` | `showTime?`, `format?` | A dot on each series at the cursor | `packages/charts/src/cursors.tsx` |
320
+ | `InlineCursor` | `showTime?`, `format?` | Dots + a value chip beside each | `packages/charts/src/cursors.tsx` |
321
+ | `FlagCursor` | `showTime?`, `format?` | Dots + staffed value flags stacked at the top | `packages/charts/src/cursors.tsx` |
322
+ | `CrosshairCursor` | `snap?`, `showTime?`, `format?`, `onSnap?` | The inspection reticle: dashed cross, centre dot in the snapped series' colour, y value pill, x time pill; `onSnap` reports the snapped series + point | `packages/charts/src/cursors.tsx` |
323
+ | `RangeCursor` | `sequence?`, `onDragRelease?`, `enableDrag?`, `dragModifier?` | The hover-time band + the drag: release fires once with a `RangeSpan`, then reverts; on a category axis the band shades the slot and the drag is off | `packages/charts/src/cursors.tsx` |
325
324
 
326
325
  ### Components — standalone row lists (DOM tables, no `<ChartContainer>`)
327
326
 
@@ -398,8 +397,8 @@ Series shapes (same file): `ChartSeries`, `BandSeries`, `BoxSeries`,
398
397
  | `scaleBand` / `ScaleBand` | Ordinal slot scale for the category axis | `packages/charts/src/bandScale.ts` |
399
398
  | `GapMode` | `'none' \| 'empty' \| 'dashed' \| 'step' \| 'fade'` (Line/Area `gaps` prop) | `packages/charts/src/gaps.ts` |
400
399
  | `DecimateOption` | `<LineChart decimate>` — M4 viewport decimation (`bool \| { threshold }`) | `packages/charts/src/decimate.ts` |
401
- | `CursorMode` | `'none' \| 'line' \| 'point' \| 'inline' \| 'flag' \| 'crosshair' \| 'region'` | `packages/charts/src/context.ts` |
402
400
  | `TrackerInfo` / `TrackerSample` | Hover readout payload (`onTrackerChanged`) | `packages/charts/src/context.ts` |
401
+ | `CursorSnap` | The point a `<CrosshairCursor>` snapped to (`onSnap`): a `TrackerSample` + `axisId` + `formatted` | `packages/charts/src/context.ts` |
403
402
  | `AnnotationKind` / `CreateSpec` | Annotation identity + draw-gesture payload (`onCreate`) | `packages/charts/src/context.ts` |
404
403
  | `SelectInfo` | Selection/hover payload (`Selector`/`MultiSelector` `onSelect`/`onHover`) | `packages/charts/src/context.ts` |
405
404
  | `SelectModifiers` | Keyboard modifiers on a click, 2nd arg to `onSelect` | `packages/charts/src/context.ts` |
@@ -716,31 +715,31 @@ Typed dataflow graphs for pipelines whose **shape is data** (runtime-assembled,
716
715
  user-edited, one computation fanned out to several consumers). Chaining stays
717
716
  the default for pipelines known at authoring time — see the package README.
718
717
 
719
- | Group | Exports | Source |
720
- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
721
- | 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` |
722
- | Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
723
- | Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
724
- | Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
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` |
726
- | Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
727
- | 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` |
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` |
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` |
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` |
731
- | 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` |
732
- | Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams` (`{ validate: false }` applies defaults and skips every check), `byFamily`, `describe`, `toJsonSchema`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError` | `packages/process/src/plan/registry.ts`, `params.ts` |
733
- | Plan — identity | `specId(registry, spec, { validate })` (content-addressed, param-order invariant, defaults materialized; `validate: false` is **total** — names a spec that would not compile in a separate `p1?:` namespace that cannot collide with a valid id; a valid spec's id is identical either way), `SpecIdOptions`, `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey` | `packages/process/src/plan/identity.ts` |
734
- | 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` |
735
- | Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError`, `UnknownColumnError` (a raw string input naming no column of the bound series, checked over the whole closure and re-checked on the warm path) | `packages/process/src/plan/graph.ts`, `run.ts` |
736
- | 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` (`spec`, `select`, `reason`, `code` — the failure's kind, matching the error class a throw would have carried), `NodeTiming` (`slot`, `pulled`, `cached`, `ms`, `inputs`) | `packages/process/src/plan/run.ts` |
737
- | 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` |
738
- | 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` |
739
- | 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` |
740
- | Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
741
- | 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` |
742
- | Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
743
- | Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
718
+ | Group | Exports | Source |
719
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
720
+ | 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` |
721
+ | Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
722
+ | Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
723
+ | Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
724
+ | 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
+ | 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
+ | 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
+ | 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
+ | 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` |
730
+ | 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` |
731
+ | Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams` (`{ validate: false }` applies defaults and skips every check), `byFamily`, `describe`, `toJsonSchema`, `checkArity`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError`, `ArityError` | `packages/process/src/plan/registry.ts`, `params.ts` |
732
+ | Plan — identity | `specId(registry, spec, { validate })` (content-addressed, param-order invariant, defaults materialized; `validate: false` is **total over arbitrary JSON** — names a spec that would not compile, including malformed shapes, in a separate `p1?:` namespace that cannot collide with a valid id; a valid spec's id is identical either way. Judges op existence, params **and arity**), `SpecIdOptions`, `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey` | `packages/process/src/plan/identity.ts` |
733
+ | 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` |
734
+ | Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError`, `UnknownColumnError` (a raw string input naming no column of the bound series, checked over the whole closure and re-checked on the warm path) | `packages/process/src/plan/graph.ts`, `run.ts` |
735
+ | 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` (`spec` — echoed verbatim, `params`/`inputs` typed `unknown`; `select`, `reason`, `code` — the failure's kind, matching the error class a throw would have carried), `NodeTiming` (`slot`, `pulled`, `cached`, `ms`, `inputs`) | `packages/process/src/plan/run.ts` |
736
+ | 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` |
737
+ | 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` |
738
+ | 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` |
739
+ | Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
740
+ | 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` |
741
+ | Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
742
+ | Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
744
743
 
745
744
  Note: this package's `npm test` includes a `test:dts` step that typechecks the
746
745
  **emitted** `dist/*.d.ts` from a consumer's perspective (`test-dts/`,
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.69.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.71.0...HEAD
12
+ [0.71.0]: https://github.com/pond-ts/pond/compare/v0.70.0...v0.71.0
13
+ [0.70.0]: https://github.com/pond-ts/pond/compare/v0.69.0...v0.70.0
12
14
  [0.69.0]: https://github.com/pond-ts/pond/compare/v0.68.0...v0.69.0
13
15
  [0.68.0]: https://github.com/pond-ts/pond/compare/v0.67.0...v0.68.0
14
16
  [0.67.0]: https://github.com/pond-ts/pond/compare/v0.66.0...v0.67.0
@@ -72,6 +74,180 @@ include new features and type-level changes; patch bumps are strictly additive.
72
74
 
73
75
  ## [Unreleased]
74
76
 
77
+ ## [0.71.0] — 2026-09-24
78
+
79
+ ### Added
80
+
81
+ - `@pond-ts/charts`: **`format` on `<LineCursor>`, `<PointCursor>`,
82
+ `<InlineCursor>` and `<FlagCursor>`** — the readout format that used to be
83
+ the container's `cursorFormat`, now on whichever cursor you mount (it was
84
+ already on `<CrosshairCursor>`). The chart still has one readout channel:
85
+ the first mounted cursor that sets `format` shapes it.
86
+
87
+ - `@pond-ts/charts`: **`<CrosshairCursor onSnap>`** — tells you what the
88
+ crosshair is snapped to: the series (`label`, `color`, `axisId`) and the
89
+ point (`x`, `value`, `formatted`, plus `readout` when the layer has one), as
90
+ a new exported **`CursorSnap`** type. Fires only when the snapped point
91
+ changes, and with `null` when the pointer leaves. With `snap={false}` the
92
+ reticle follows the pointer rather than a series, so it stays `null`; a
93
+ reticle drawn by a controlled `trackerPosition` with no pointer on the chart
94
+ also reports `null`. Story `Cursors/Crosshair / SnapReadout`.
95
+
96
+ - `@pond-ts/charts`: **`<CrosshairCursor>` snaps to box plots.** Its vertical
97
+ line lands on the centre of the box under the pointer and its horizontal
98
+ line on the quantile nearest the pointer (`upper` / `q3` / `median` / `q1` /
99
+ `lower`), with that value on the y-axis pill and reported by `onSnap`. A box
100
+ used to be skipped by the crosshair entirely: the line sat wherever the
101
+ pointer was and there was no value to read. The flag cursor still shows the
102
+ box's one consolidated flag, and the point / inline / flag cursors still draw
103
+ no per-quantile dots on a box. In a row with a box and a line, the vertical
104
+ line snaps to whichever of the two is drawn on top, the same rule as for two
105
+ lines. Story `Cursors/Crosshair / BoxPlot`.
106
+
107
+ ### Changed
108
+
109
+ - `@pond-ts/charts`: the **crosshair's centre dot is drawn in the snapped
110
+ series' colour** rather than the cursor ink, so the reticle shows which line
111
+ it is reading. The free reticle (`snap={false}`) has no series under it and
112
+ keeps the cursor ink.
113
+
114
+ - **charts:** **`<AreaChart>` fills to zero by default.** An omitted `baseline`
115
+ used to rest the fill on the bottom of the plot, which on auto-fit data sits
116
+ just under the lowest value — so a series running 50–90 drew 60 as a sliver
117
+ and 90 as a slab several times its size, and the fill's height said nothing
118
+ about the value. The default is now `0`: zero is pulled into the auto-fit
119
+ domain and each fill is as tall as its value, the usual reading of an area.
120
+ A number still sets another reference level. The old look is one prop away:
121
+ **`baseline="floor"`** rests the fill on the bottom of the plot and adds
122
+ nothing to the domain — for a price or an elevation profile, where starting
123
+ at zero would flatten the shape. **Migration:** an `<AreaChart>` with no
124
+ `baseline` whose data sits far from zero will now show zero on its axis; add
125
+ `baseline="floor"` to keep the previous rendering. Charts that already wrote
126
+ `baseline={0}`, or whose axis already started at zero, are unchanged. On a
127
+ log axis nothing changes: zero has no position there, so it rests on the
128
+ floor and is not pulled into the domain. On an axis pinned above zero
129
+ (`<YAxis min={40}>`) the baseline is clamped to the axis floor, as a bar's is,
130
+ so the fill's fade stays on the plot.
131
+
132
+ ### Removed
133
+
134
+ - `@pond-ts/charts` (**breaking**): **the old cursor props are gone — a chart
135
+ shows a cursor only when you mount one.** Removed: `<ChartContainer>`'s
136
+ `cursor`, `cursorTime`, `crosshairSnap`, `cursorFormat`, `cursorSequence`,
137
+ `onRegionSelect` and `regionSelectModifier`, `<ChartRow cursor>`, and the
138
+ `CursorMode` type. They were deprecated in 0.58.0. The behaviour change that
139
+ matters most: a chart with no cursor component used to get a vertical line
140
+ cursor anyway; it now gets **no cursor** (hover still reports through
141
+ `onTrackerChanged`). Closes
142
+ [#647](https://github.com/pond-ts/pond/issues/647). **Migration:**
143
+
144
+ | Before | After |
145
+ | ------------------------------------------------ | ---------------------------------------------------------------------------- |
146
+ | no `cursor` prop (the implicit line) | `<LineCursor />` (not on a `<MultiSelector>` row — see below) |
147
+ | `cursor="line" \| "point" \| "inline" \| "flag"` | `<LineCursor />` / `<PointCursor />` / `<InlineCursor />` / `<FlagCursor />` |
148
+ | `cursor="crosshair"` + `crosshairSnap={false}` | `<CrosshairCursor snap={false} />` |
149
+ | `cursorTime` | `showTime` on the cursor |
150
+ | `cursorFormat="…"` | `format="…"` on the cursor |
151
+ | `cursor="region"` + `cursorSequence={seq}` | `<RangeCursor sequence={seq} />` |
152
+ | `onRegionSelect={([a, b]) => …}` | `<RangeCursor onDragRelease={({ x: [a, b] }) => …} />` |
153
+ | `regionSelectModifier="shift"` | `<RangeCursor dragModifier="shift" />` |
154
+ | `cursor="none"` | mount nothing |
155
+ | `<ChartRow cursor="…">` | mount the cursor inside that `<ChartRow>` |
156
+
157
+ Mount the cursor as a child of `<ChartContainer>` for every row, or inside
158
+ one `<ChartRow>` for that row only. A row that should have no cursor while
159
+ its siblings have one: mount the cursors per row instead of at the
160
+ container. **Don't add `<LineCursor />` to a row with a `<MultiSelector>`**:
161
+ there the selector's resting band is the cursor (as it already was under the
162
+ implicit line), and any mounted cursor replaces the band.
163
+
164
+ ### Fixed
165
+
166
+ - `@pond-ts/charts`: **`<RangeCursor>` on a category axis now shades the bar
167
+ under the pointer.** It used to draw nothing there, so mounting one left the
168
+ row with no cursor at all. The band covers the whole slot, the way a
169
+ bucketed band covers a bucket on a time axis. The drag stays off on a
170
+ category axis (`onDragRelease` never fires there, and the chart now warns
171
+ in development when it is wired); dragging across bars to get them back is
172
+ what `<MultiSelector>` does. Story `Cursors/Range / CategoryAxis`.
173
+
174
+ - **charts:** **A selectable `<AreaChart>` on a log axis with `baseline={0}`
175
+ counted every point over its x span as a hit.** Zero maps to `NaN` on a log
176
+ scale; the draw already fell back to the axis floor, but the hit test used
177
+ the `NaN` pixel, and a `NaN` bound fails neither range check. It now resolves
178
+ the baseline the same way the draw does.
179
+ - **charts:** **`<AreaChart baseline={0}>` on an auto-fit log axis clipped
180
+ most of its series.** The zero baseline was pulled into the extent, which
181
+ left the log fit with no positive low end, so the domain collapsed around the
182
+ largest value (data from 10 to 1e5 fitted `[1e4, 1e6]`). A baseline at or
183
+ below zero is now left out of a log axis's fit. A layer's `yExtent` receives
184
+ the axis's scale kind for this.
185
+
186
+ ## [0.70.0] — 2026-09-18
187
+
188
+ ### Added
189
+
190
+ - `@pond-ts/charts`: **`<BandChart sessionBreaks>`** — the same trading-axis
191
+ session break `<LineChart>` has had since v0.45.0. On a discontinuous
192
+ (`discontinuities` / `calendar`) axis the fill previously ran a near-vertical
193
+ sliver from one session's last sample to the next session's first, because
194
+ the collapsed overnight gap put them a pixel apart; `sessionBreaks` ends the
195
+ envelope at the close and re-starts it at the open, so a band and its centre
196
+ line break in step. A **scale** break, orthogonal to the NaN **data** gaps a
197
+ band always breaks at; default `false` (unchanged output). Decimation
198
+ composes: `decimateBand` folds each break instant into the pixel-column
199
+ edges and bakes a `NaN` sample at it, so no column merges two sessions'
200
+ envelopes. Stories `Axes/TradingTimeAxis / SessionBreaksBand` and
201
+ `Performance/Decimation / TradingSessionBreaksBand`.
202
+ - `@pond-ts/process`: **`ArityError`**, and **arity is now part of what `specId`
203
+ can judge**. A spec whose `inputs` count does not match the op's — including
204
+ one carrying no `inputs` at all — was named `p1:sma(;period=20)`, a _valid_
205
+ id for something that cannot compile, and then died at `compile` as a bare
206
+ `TypeError` reading `.length` of `undefined`, reaching the consumer as a
207
+ `Skipped` with **no `code`** (which under that contract means "op code
208
+ threw"). Arity is decidable from the registry alone, so strict `specId` now
209
+ raises `ArityError` and lenient mode marks the id `p1?:`.
210
+ `Registry.checkArity(op, inputs)` is the shared check `compile` uses too.
211
+
212
+ ### Changed
213
+
214
+ - `@pond-ts/financial`: **`StudyOutput.id` documents the `@pond-ts/process`
215
+ bridge**, and a new `test/catalog-process.test.ts` pins it. The field is a
216
+ *financial column suffix*; process's `OutputDef.id` is a *process outlet
217
+ id*; they share a name and are different namespaces. Process names its own
218
+ columns (`specId + OutputDef.id`) and matches an op's return to its outputs
219
+ positionally, so a study's own column names never reach it — which means a
220
+ registry bridging the two chooses its own suffixes, and for the **twelve**
221
+ multi-output studies that claim the bare prefix (`trix`, `superTrend`,
222
+ `klinger`, …) it must: process rejects `''` on a multi-output op, because
223
+ there the column would be named exactly the spec id, itself a legal column
224
+ reference. The map is `outputs.length > 1 && id === ''` → `'value'` — not
225
+ `id === ''`, which would rename all seventy-odd single-output columns for
226
+ nothing. Reported by a consumer that hit the throw at module load and
227
+ worked around it by hand. No API change: the descriptors, the studies and
228
+ the guard are all unchanged, and the round-trip test is what stops the two
229
+ packages drifting — `catalog.test.ts` validates a descriptor against its
230
+ *study*, so it is structurally blind to a cross-package disagreement.
231
+ - `@pond-ts/process`: **`Skipped.spec` echoes the request verbatim**, and its
232
+ `params` / `inputs` are typed `unknown` accordingly. The plan pass normalized
233
+ `params: null` to `{}`, so recomputing an id from the echo produced the
234
+ _defaulted spec's valid id_ — keying a broken persisted entry's report onto a
235
+ legitimate node. The selector pass echoed the original all along, so the two
236
+ passes disagreed. **Migration:** a consumer reading `entry.spec.params` now
237
+ narrows it (`entry.spec.params as Record<string, unknown>`, or a guard) —
238
+ which is the point, since the value may be exactly the malformed thing that
239
+ failed.
240
+ - `@pond-ts/process`: `specId(…, { validate: false })` is **total over
241
+ arbitrary JSON**, not just over well-typed specs. `params: null`, a
242
+ non-array `inputs`, and an input entry that is neither a column name nor a
243
+ spec each used to raise a `TypeError`; they are now named in the `p1?:`
244
+ namespace, distinctly enough that two differently-broken specs stay two ids.
245
+ Strict mode reports them as `ParamError` / `ArityError` / `ProcessError`
246
+ rather than crashing.
247
+
248
+ (All three reported by Tidal against 0.62.0, after adopting it.)
249
+
250
+
75
251
  ## [0.69.0] — 2026-09-13
76
252
 
77
253
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/fit",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "private": false,
5
5
  "description": "Fitness & activity domain library on pond-ts: typed quantities, canonical activity series, and analytics (geo distance/elevation, power NP/IF/TSS, zones, splits)",
6
6
  "keywords": [
@@ -59,7 +59,7 @@
59
59
  "verify": "npm run format:check && npm run build && npm test"
60
60
  },
61
61
  "peerDependencies": {
62
- "pond-ts": "^0.69.0"
62
+ "pond-ts": "^0.71.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "typescript": "^5.6.3",