@pond-ts/process 0.59.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 +20 -16
  2. package/CHANGELOG.md +196 -1
  3. package/package.json +2 -2
package/API.md CHANGED
@@ -249,17 +249,17 @@ Types: `UseSnapshotOptions`, `SnapshotSource` (structural — covers
249
249
 
250
250
  ### Components — layout & axes
251
251
 
252
- | Component | Key props | Purpose | Source |
253
- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
254
- | `ChartContainer` | `width`, `range?`, `theme?`, `cursor?`, `panZoom?`, `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` |
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
263
 
264
264
  ### Components — draw layers
265
265
 
@@ -280,7 +280,7 @@ schema columns). Because the union splits per series _kind_, a value typed as
280
280
  | Component | Data props | Purpose | Source |
281
281
  | -------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------- |
282
282
  | `LineChart` | `column`, `gaps?`, `sessionBreaks?` | Gap-aware line | `packages/charts/src/LineChart.tsx` |
283
- | `AreaChart` | `column`, `baseline?`, `gaps?` | Filled area | `packages/charts/src/AreaChart.tsx` |
283
+ | `AreaChart` | `column`, `baseline?`, `gaps?`, `thresholds?`/`bandColors?` | Filled area | `packages/charts/src/AreaChart.tsx` |
284
284
  | `BandChart` | `lower`, `upper` | Variance-band envelope | `packages/charts/src/BandChart.tsx` |
285
285
  | `ScatterChart` | `column`, `id?` (selection), radius/color encodings | Points; data-driven size/colour | `packages/charts/src/ScatterChart.tsx` |
286
286
  | `BarChart` | `column` \| `columns` \| `bins` \| `categories`, `orientation?`, `thresholds?`/`bandColors?` | Bars, stacked bars, histograms, categorical | `packages/charts/src/BarChart.tsx` |
@@ -328,10 +328,10 @@ label + data cells, `sortBy`/`sort`, optional per-row expander. The in-plot
328
328
  histogram stays `<BarChart orientation="horizontal">` — these are the table
329
329
  shape (react-timeseries-charts' `HorizontalBarChart`).
330
330
 
331
- | Component | Data props | Purpose | Source |
332
- | --------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | --------------------------------- |
333
- | `BarList` | `rows`, `columns` (`values` names), `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` |
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
335
 
336
336
  Row/option types + readers (`packages/charts/src/list.ts`): `ListRow`,
337
337
  `ListValue`, `ListCellSpec`, `ListMarker` (reference rule through every row,
@@ -411,6 +411,10 @@ Series shapes (same file): `ChartSeries`, `BandSeries`, `BoxSeries`,
411
411
  | `SwatchSpec` / `LegendItemInput` | Legend swatch vocabulary + explicit-rows input (`<Legend items>`) | `packages/charts/src/swatch.ts` |
412
412
  | `useChartLegend` | Headless legend hook: rows (items grouped by chart row) + `hover`/`select` verbs | `packages/charts/src/useChartLegend.ts` |
413
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` |
414
418
  | `LegendPlacement` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `packages/charts/src/Legend.tsx` |
415
419
  | `Curve` | Path interpolation: `'linear' \| 'monotone' \| 'natural' \| 'basis' \| 'step'` | `packages/charts/src/curve.ts` |
416
420
  | `RadiusEncoding` / `ColorEncoding` | Data-driven scatter size/colour | `packages/charts/src/encoding.ts` |
package/CHANGELOG.md CHANGED
@@ -8,7 +8,8 @@ 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.59.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
12
13
  [0.59.0]: https://github.com/pond-ts/pond/compare/v0.58.0...v0.59.0
13
14
  [0.58.0]: https://github.com/pond-ts/pond/compare/v0.57.0...v0.58.0
14
15
  [0.57.0]: https://github.com/pond-ts/pond/compare/v0.56.2...v0.57.0
@@ -62,6 +63,200 @@ include new features and type-level changes; patch bumps are strictly additive.
62
63
 
63
64
  ## [Unreleased]
64
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
+
65
260
  ## [0.59.0] — 2026-08-11
66
261
 
67
262
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/process",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "Computations as data over pond-ts: processing graphs authored fluently or composed as JSON, resolved against a declared op vocabulary with content-addressed caching, provenance, and per-node timings. Experimental, pre-1.0.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "verify": "npm run format:check && npm run build && npm test"
43
43
  },
44
44
  "peerDependencies": {
45
- "pond-ts": "^0.59.0"
45
+ "pond-ts": "^0.60.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "typescript": "^5.6.3",