@pond-ts/fit 0.70.0 → 0.72.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 +30 -31
  3. package/CHANGELOG.md +143 -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` |
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.70.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.72.0...HEAD
12
+ [0.72.0]: https://github.com/pond-ts/pond/compare/v0.71.0...v0.72.0
13
+ [0.71.0]: https://github.com/pond-ts/pond/compare/v0.70.0...v0.71.0
12
14
  [0.70.0]: https://github.com/pond-ts/pond/compare/v0.69.0...v0.70.0
13
15
  [0.69.0]: https://github.com/pond-ts/pond/compare/v0.68.0...v0.69.0
14
16
  [0.68.0]: https://github.com/pond-ts/pond/compare/v0.67.0...v0.68.0
@@ -73,6 +75,146 @@ include new features and type-level changes; patch bumps are strictly additive.
73
75
 
74
76
  ## [Unreleased]
75
77
 
78
+ ## [0.72.0] — 2026-10-08
79
+
80
+ ### Changed
81
+
82
+ - `pond-ts`: **`join` and `joinMany` work on columns now, not events** — 20
83
+ to 190× faster in the cases below, with the same output. `join` used to
84
+ build an event for every row on both sides, merge them row by row and
85
+ rebuild the columns. Now it walks the two key columns once, then either
86
+ adopts each column as is or gathers it in one pass. A side's value columns
87
+ are adopted as is whenever all its rows land in the output once and in order:
88
+ always the left side of a `left` join, the right side of a `right` join, and
89
+ both sides when the keys match one for one (`joinMany` over a shared grid).
90
+ One year of 1-minute bars, ~97.5k rows a side, `type: 'left'` unless noted
91
+ (`packages/core/scripts/perf-join.mjs`):
92
+
93
+ | Value columns per side | Before | After |
94
+ | ------------------------ | -------- | -------- |
95
+ | 59 and 59 | 1,868 ms | 24 ms |
96
+ | 5 and 5 | 168 ms | 4.2 ms |
97
+ | 59 and 2 | 766 ms | 4.1 ms |
98
+ | `joinMany`, 4 × 5, outer | 457 ms | 15–20 ms |
99
+
100
+ A left join now costs about as much as the **other** side is wide. To bring
101
+ across only the columns you read, narrow that side first; `select` and
102
+ `rename` don't copy:
103
+ `bars.join(spy.select('close').rename({ close: 'spy' }), { type: 'left' })`.
104
+
105
+ One visible difference: on a row with no match, `event.data()` now lists the
106
+ other side's fields as `undefined`, as every other operator's events do. It
107
+ used to leave them out. `get()` returns `undefined` either way.
108
+
109
+ ## [0.71.0] — 2026-09-24
110
+
111
+ ### Added
112
+
113
+ - `@pond-ts/charts`: **`format` on `<LineCursor>`, `<PointCursor>`,
114
+ `<InlineCursor>` and `<FlagCursor>`** — the readout format that used to be
115
+ the container's `cursorFormat`, now on whichever cursor you mount (it was
116
+ already on `<CrosshairCursor>`). The chart still has one readout channel:
117
+ the first mounted cursor that sets `format` shapes it.
118
+
119
+ - `@pond-ts/charts`: **`<CrosshairCursor onSnap>`** — tells you what the
120
+ crosshair is snapped to: the series (`label`, `color`, `axisId`) and the
121
+ point (`x`, `value`, `formatted`, plus `readout` when the layer has one), as
122
+ a new exported **`CursorSnap`** type. Fires only when the snapped point
123
+ changes, and with `null` when the pointer leaves. With `snap={false}` the
124
+ reticle follows the pointer rather than a series, so it stays `null`; a
125
+ reticle drawn by a controlled `trackerPosition` with no pointer on the chart
126
+ also reports `null`. Story `Cursors/Crosshair / SnapReadout`.
127
+
128
+ - `@pond-ts/charts`: **`<CrosshairCursor>` snaps to box plots.** Its vertical
129
+ line lands on the centre of the box under the pointer and its horizontal
130
+ line on the quantile nearest the pointer (`upper` / `q3` / `median` / `q1` /
131
+ `lower`), with that value on the y-axis pill and reported by `onSnap`. A box
132
+ used to be skipped by the crosshair entirely: the line sat wherever the
133
+ pointer was and there was no value to read. The flag cursor still shows the
134
+ box's one consolidated flag, and the point / inline / flag cursors still draw
135
+ no per-quantile dots on a box. In a row with a box and a line, the vertical
136
+ line snaps to whichever of the two is drawn on top, the same rule as for two
137
+ lines. Story `Cursors/Crosshair / BoxPlot`.
138
+
139
+ ### Changed
140
+
141
+ - `@pond-ts/charts`: the **crosshair's centre dot is drawn in the snapped
142
+ series' colour** rather than the cursor ink, so the reticle shows which line
143
+ it is reading. The free reticle (`snap={false}`) has no series under it and
144
+ keeps the cursor ink.
145
+
146
+ - **charts:** **`<AreaChart>` fills to zero by default.** An omitted `baseline`
147
+ used to rest the fill on the bottom of the plot, which on auto-fit data sits
148
+ just under the lowest value — so a series running 50–90 drew 60 as a sliver
149
+ and 90 as a slab several times its size, and the fill's height said nothing
150
+ about the value. The default is now `0`: zero is pulled into the auto-fit
151
+ domain and each fill is as tall as its value, the usual reading of an area.
152
+ A number still sets another reference level. The old look is one prop away:
153
+ **`baseline="floor"`** rests the fill on the bottom of the plot and adds
154
+ nothing to the domain — for a price or an elevation profile, where starting
155
+ at zero would flatten the shape. **Migration:** an `<AreaChart>` with no
156
+ `baseline` whose data sits far from zero will now show zero on its axis; add
157
+ `baseline="floor"` to keep the previous rendering. Charts that already wrote
158
+ `baseline={0}`, or whose axis already started at zero, are unchanged. On a
159
+ log axis nothing changes: zero has no position there, so it rests on the
160
+ floor and is not pulled into the domain. On an axis pinned above zero
161
+ (`<YAxis min={40}>`) the baseline is clamped to the axis floor, as a bar's is,
162
+ so the fill's fade stays on the plot.
163
+
164
+ ### Removed
165
+
166
+ - `@pond-ts/charts` (**breaking**): **the old cursor props are gone — a chart
167
+ shows a cursor only when you mount one.** Removed: `<ChartContainer>`'s
168
+ `cursor`, `cursorTime`, `crosshairSnap`, `cursorFormat`, `cursorSequence`,
169
+ `onRegionSelect` and `regionSelectModifier`, `<ChartRow cursor>`, and the
170
+ `CursorMode` type. They were deprecated in 0.58.0. The behaviour change that
171
+ matters most: a chart with no cursor component used to get a vertical line
172
+ cursor anyway; it now gets **no cursor** (hover still reports through
173
+ `onTrackerChanged`). Closes
174
+ [#647](https://github.com/pond-ts/pond/issues/647). **Migration:**
175
+
176
+ | Before | After |
177
+ | ------------------------------------------------ | ---------------------------------------------------------------------------- |
178
+ | no `cursor` prop (the implicit line) | `<LineCursor />` (not on a `<MultiSelector>` row — see below) |
179
+ | `cursor="line" \| "point" \| "inline" \| "flag"` | `<LineCursor />` / `<PointCursor />` / `<InlineCursor />` / `<FlagCursor />` |
180
+ | `cursor="crosshair"` + `crosshairSnap={false}` | `<CrosshairCursor snap={false} />` |
181
+ | `cursorTime` | `showTime` on the cursor |
182
+ | `cursorFormat="…"` | `format="…"` on the cursor |
183
+ | `cursor="region"` + `cursorSequence={seq}` | `<RangeCursor sequence={seq} />` |
184
+ | `onRegionSelect={([a, b]) => …}` | `<RangeCursor onDragRelease={({ x: [a, b] }) => …} />` |
185
+ | `regionSelectModifier="shift"` | `<RangeCursor dragModifier="shift" />` |
186
+ | `cursor="none"` | mount nothing |
187
+ | `<ChartRow cursor="…">` | mount the cursor inside that `<ChartRow>` |
188
+
189
+ Mount the cursor as a child of `<ChartContainer>` for every row, or inside
190
+ one `<ChartRow>` for that row only. A row that should have no cursor while
191
+ its siblings have one: mount the cursors per row instead of at the
192
+ container. **Don't add `<LineCursor />` to a row with a `<MultiSelector>`**:
193
+ there the selector's resting band is the cursor (as it already was under the
194
+ implicit line), and any mounted cursor replaces the band.
195
+
196
+ ### Fixed
197
+
198
+ - `@pond-ts/charts`: **`<RangeCursor>` on a category axis now shades the bar
199
+ under the pointer.** It used to draw nothing there, so mounting one left the
200
+ row with no cursor at all. The band covers the whole slot, the way a
201
+ bucketed band covers a bucket on a time axis. The drag stays off on a
202
+ category axis (`onDragRelease` never fires there, and the chart now warns
203
+ in development when it is wired); dragging across bars to get them back is
204
+ what `<MultiSelector>` does. Story `Cursors/Range / CategoryAxis`.
205
+
206
+ - **charts:** **A selectable `<AreaChart>` on a log axis with `baseline={0}`
207
+ counted every point over its x span as a hit.** Zero maps to `NaN` on a log
208
+ scale; the draw already fell back to the axis floor, but the hit test used
209
+ the `NaN` pixel, and a `NaN` bound fails neither range check. It now resolves
210
+ the baseline the same way the draw does.
211
+ - **charts:** **`<AreaChart baseline={0}>` on an auto-fit log axis clipped
212
+ most of its series.** The zero baseline was pulled into the extent, which
213
+ left the log fit with no positive low end, so the domain collapsed around the
214
+ largest value (data from 10 to 1e5 fitted `[1e4, 1e6]`). A baseline at or
215
+ below zero is now left out of a log axis's fit. A layer's `yExtent` receives
216
+ the axis's scale kind for this.
217
+
76
218
  ## [0.70.0] — 2026-09-18
77
219
 
78
220
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/fit",
3
- "version": "0.70.0",
3
+ "version": "0.72.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.70.0"
62
+ "pond-ts": "^0.72.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "typescript": "^5.6.3",