@pond-ts/fit 0.54.0 → 0.56.2

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 (2) hide show
  1. package/CHANGELOG.md +451 -1
  2. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -8,7 +8,11 @@ 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.54.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.56.2...HEAD
12
+ [0.56.2]: https://github.com/pond-ts/pond/compare/v0.56.1...v0.56.2
13
+ [0.56.1]: https://github.com/pond-ts/pond/compare/v0.56.0...v0.56.1
14
+ [0.56.0]: https://github.com/pond-ts/pond/compare/v0.55.0...v0.56.0
15
+ [0.55.0]: https://github.com/pond-ts/pond/compare/v0.54.0...v0.55.0
12
16
  [0.54.0]: https://github.com/pond-ts/pond/compare/v0.53.1...v0.54.0
13
17
  [0.53.1]: https://github.com/pond-ts/pond/compare/v0.53.0...v0.53.1
14
18
  [0.53.0]: https://github.com/pond-ts/pond/compare/v0.52.0...v0.53.0
@@ -55,8 +59,454 @@ include new features and type-level changes; patch bumps are strictly additive.
55
59
 
56
60
  ## [Unreleased]
57
61
 
62
+ ## [0.56.2] — 2026-08-05
63
+
64
+ ### Fixed
65
+
66
+ - **charts (tests only, no shipped change):** the log-axis rendered-label test
67
+ is no longer an exact-set assertion over every digit-bearing node in the
68
+ render tree. It passed on Node 22 and failed on CI's Node 18 with one extra
69
+ element, blocking the publish twice. The discrepancy is **unreproduced and
70
+ still open** — recorded as `[PND-LOGTICK-N18]` in `PND_CHARTS_PLAN.md` with
71
+ everything measured about it. The assertion now checks that every chosen tick
72
+ renders in order, which is the wiring this test exists to cover; the tick
73
+ _selection_ it was really about is pinned deterministically by the
74
+ `yTickValues` unit tests.
75
+
76
+ ## [0.56.1] — 2026-08-05
77
+
78
+ ### Fixed
79
+
80
+ - **charts (tests only, no shipped change):** a log-axis test asserted on
81
+ rendered label _text_, parsing numbers back out of the DOM to infer scale
82
+ behaviour. It passed locally and failed in CI on a value it could not have
83
+ produced there, which blocked the v0.56.0 publish. The root cause was never
84
+ reproduced; rather than guess at it, the assertion now compares the rendered
85
+ labels against the ticks the axis is specified to draw, formatted through the
86
+ same formatter — deterministic regardless of locale, formatting or DOM
87
+ differences, and the numeric guarantee itself was already pinned directly by
88
+ the `yTickValues` unit tests. The published artifact is identical to what
89
+ v0.56.0 would have been.
90
+
91
+ ## [0.56.0] — 2026-08-05
92
+
93
+ ### Added
94
+
95
+ - **charts:** **`<YAxis scale="log">` — a base-10 logarithmic y axis.** Every y
96
+ scale was `scaleLinear`, so data spanning orders of magnitude was
97
+ undrawable: on a linear axis everything below the top decade collapses onto
98
+ the baseline. Set `scale="log"` and the axis maps by ratio, ticking the
99
+ decades. `format` still formats the **value**, so a readout says `1.2 PB`
100
+ rather than its logarithm — the transform is in the scale, not in the data,
101
+ which is what keeps it transparent to every draw layer, annotation and
102
+ cursor readout.
103
+
104
+ A log domain cannot contain zero, and d3 maps a non-positive value to
105
+ **`NaN`** — a coordinate the canvas silently _drops_, which is why every
106
+ consequence below is about something failing invisibly rather than throwing.
107
+ So the axis is deliberate about it:
108
+ - **Domain policy matches the linear axis exactly.** Auto-fit takes the
109
+ smallest **positive** extent (one zero sample can't collapse the axis, and
110
+ a `BarChart` — whose extent always widens to include zero — can still share
111
+ it); a positive explicit `min`/`max` is honoured verbatim and never
112
+ discarded, with the _auto-fit_ side moving if the domain would otherwise
113
+ invert; a fully auto-fit domain is `.nice()`d out to whole powers of ten, so
114
+ the extremes get headroom instead of sitting clipped on the plot edge. A
115
+ non-positive bound has no position and is refused in favour of the data.
116
+ `pad` is applied multiplicatively, adding the same fraction of a decade at
117
+ both ends.
118
+ - **A value with no position on the axis renders as a gap.** Previously the
119
+ gap test was `Number.isFinite(value)`, and `0` is finite — so the coordinate
120
+ became `NaN`, the canvas dropped the path op without breaking the path, and
121
+ the two neighbours were joined by a straight line _over_ the missing data.
122
+ Lines, area fills and outlines, and band envelopes now all break there.
123
+ - **Layers that reach for a baseline rest on the axis floor.** `AreaChart`
124
+ resolves an out-of-domain `baseline` there (writing `baseline={0}` is
125
+ natural and correct on a linear axis), and a **stacked** bar layer starts
126
+ its first segment there — starting at zero made the bottom segment of every
127
+ stack both invisible and unhittable. Unchanged on a linear axis, where zero
128
+ clamped into the domain _is_ zero.
129
+ - **The dev-mode warning names only unambiguous mistakes**: a refused
130
+ `min`/`max`, negative data, or an axis with no positive data at all. It
131
+ deliberately says nothing about an extent of exactly `[0, hi]`, which a
132
+ line touching zero and a bar layer on strictly positive data both report
133
+ identically — warning there fired on _every_ bar chart on a log axis. It
134
+ warns once per distinct complaint rather than on every repaint.
135
+
136
+ - **charts:** **pan and zoom now yield whole-millisecond view ranges.** A
137
+ wheel-zoom derives its range from pixel positions through `xScale.invert()`,
138
+ so the result was fractional by construction — an ordinary scroll produced
139
+ `1.7e12 + 0.37`. The epoch millisecond is this model's atomic unit and
140
+ consumers are entitled to assume it; one did, and a calendar `cursorSequence`
141
+ threw on a plain scroll. `zoomRange` / `panRange` round both ends, and never
142
+ collapse a positive span to zero width in doing so. (Core's fractional-instant
143
+ fix covers the same crash from the other side; this closes the class.)
144
+
145
+ - **charts:** **`AreaStyle.flatFill` — stacked areas that read as slabs.** An
146
+ area's fill has always graded to transparent at the baseline, which is right
147
+ for the elevation form and wrong for a stack: every band showed the one
148
+ beneath it through the fade, so a stacked area was not really drawable. Set
149
+ `flatFill` and the fill is flat; omitted, the gradient is unchanged, so no
150
+ existing theme shifts. The docs theme's `seq1…seq8` area roles set it, since
151
+ stacking is what they exist for.
152
+
153
+ - **docs theme:** **a sequential ramp — `seq1…seq8` — for charts with more
154
+ series than the categorical set has hues.** `--pond-viz-1…5` were, and
155
+ remain, the categorical set; a chart needing more slots (an eight-source
156
+ stack, a wall of climate stripes) now steps **tonally** through the brand
157
+ teal instead of introducing competing hues. Eight steps, evenly spaced
158
+ (~ΔL\* 9 in CIELAB), defined for light and dark, each mode's ramp containing
159
+ that mode's `--pond-viz-1` exactly. Exposed as `line` / `area` / `bar` theme
160
+ roles on `docsTheme` (Storybook) and the docs site's `useSiteChartTheme`,
161
+ and as an array from the site's `useSequentialRamp()`. Dev-only: the ramp
162
+ lives in the `docs-theme.fixture.ts` Storybook fixture and the website's
163
+ CSS, both excluded from the published `@pond-ts/charts` build — the library
164
+ still ships no palette.
165
+
166
+ ### Fixed
167
+
168
+ - **core:** **a fractional epoch millisecond no longer crashes calendar
169
+ math.** `Temporal.Instant` refuses a non-integer epoch ms outright
170
+ (`epoch milliseconds must be an integer`), and `toPlainDateStart` passed
171
+ whatever it was given straight through — so realizing a `Sequence.calendar`
172
+ over a fractional range threw, and in a React app the exception unmounted the
173
+ page. A fraction is not a caller error: a chart's wheel-zoom derives its view
174
+ range from pixel positions via `xScale.invert()`, so an ordinary scroll
175
+ produces `1.7e12 + 0.37`. The instant is now floored to the millisecond
176
+ containing it — the epoch millisecond is this model's atomic unit and
177
+ calendar boundaries are themselves whole milliseconds, so the bucket
178
+ containing `t` and the one containing `t + 0.37` are necessarily the same,
179
+ and integer inputs are untouched. (`Math.floor`, not `Math.trunc`: pre-1970
180
+ they disagree, and `-5.5` lies inside the millisecond spanning `[-6, -5)`.)
181
+
182
+ - **charts:** toggling **`<ChartContainer grid>`** now repaints immediately.
183
+ `Layers`' draw callback read `container.grid` but didn't depend on it, so
184
+ switching gridlines off changed nothing until an unrelated dependency moved —
185
+ in practice you had to pan or zoom a little to force the update. The same
186
+ omission covered `sessionDividers` and `xKind`.
187
+
188
+ - **charts:** the log axis's dev-mode warning no longer requires **node's
189
+ ambient types**. It was guarded by a bare `process.env.NODE_ENV`, which
190
+ typechecks only when a tool happens to resolve `@types/node` from a parent
191
+ `node_modules` — so `tsc` inside the package passed while running the _same_
192
+ tsconfig from a consumer's directory failed with `TS2591: Cannot find name
193
+ 'process'`. That took out the docs site's TypeDoc step, and would equally hit
194
+ any consumer typechecking the package's sources. The guard now lives in
195
+ `src/dev.ts` behind a local declaration and a `typeof` check, so a browser
196
+ bundle with no `process` global doesn't throw at import either.
197
+
198
+ ## [0.55.0] — 2026-08-04
199
+
200
+ ### Added
201
+
202
+ - **process:** **`@pond-ts/process` publishes for the first time —
203
+ experimental, pre-1.0.** Computations as data over pond-ts: a processing
204
+ graph authored fluently in application code (or composed as JSON by a saved
205
+ view or a tool-calling model) resolves against a declared op vocabulary and
206
+ runs over a bound `TimeSeries`, with content-addressed caching, provenance,
207
+ and per-node timings on every response. Two entry points: the plan layer at
208
+ `.` and the Node worker pool at `./pool`. The docs section
209
+ ([pond-ts.org/docs/process](https://pond-ts.org/docs/process/)) is listed on
210
+ the site with a TypeDoc API reference; the publication follows the 2026-08
211
+ external audit hardening (all P1 findings fixed and regression-pinned). The
212
+ API is expected to move as friction reports land — pin an exact version.
213
+
214
+ - **charts:** **`<BarChart categories>` now works horizontally** ([PND-HCAT],
215
+ the 2026-08 API review's #3 item) — the funnel / ranking / comparison shape.
216
+ `orientation="horizontal"` puts the categories on the **y** axis as unit
217
+ slots with the value on x, and a `<YAxis>` with no explicit `ticks` **derives
218
+ one label per category by itself**, so the chart no longer needs a
219
+ hand-built `i + 0.5` tick list. It previously threw ("horizontal category
220
+ axes are not yet supported"), which forced consumers to convert their
221
+ categories into ordinal `bins` records _and_ hand-place the labels — the
222
+ workaround the gallery funnel documents.
223
+
224
+ Explicit `<YAxis ticks>` still wins, and vertical categorical charts are
225
+ untouched (categories stay on the container's ordinal x band scale). A new
226
+ internal `RowLayer.binCategories()` channel carries the names to whichever
227
+ axis they land on.
228
+
229
+ - **charts:** **the list family's series door** — `<BarList series={splits}
230
+ label={…}>` / `<BoxList series>` take a `TimeSeries` / `ValueSeries`
231
+ directly (one row per event; exactly-one-of with `rows`), closing the
232
+ "required adapter" gap the 2026-08 API review named: starting from a pond
233
+ series there is no shaping step. The `listRowsFrom*` readers remain for
234
+ record rows. Docs across the charts hub, cheat sheet, and type pages now
235
+ present the series as the whole data contract, with the exported `from*`
236
+ builders re-documented as **interop escape hatches for non-pond data**.
237
+
238
+ - **charts:** **`<BarList>` + `<BoxList>` — standalone ranked row lists** (the
239
+ react-timeseries-charts `HorizontalBarChart` shape, rebuilt as what it
240
+ always was: a table). One DOM row per _entity_ — an interface, a split, a
241
+ symbol — with a label cell (any node, links included), one glyph line per
242
+ configured column on **one shared value scale**, optional data cells
243
+ before/after the glyphs, `sortBy`/`sortDirection` or a full custom
244
+ comparator (missing values sort last either direction), an optional per-row
245
+ expander (`renderExpanded`, keyed on row identity so it survives a re-sort),
246
+ and consumer-owned row selection with an accent edge in the marks register.
247
+ A vertical **baseline rule** at the scale origin anchors the rows to one
248
+ reference (on by default for `<BoxList>`, whose lines float at their lower
249
+ quantile; opt-in for `<BarList>`, whose tracks already show zero), and
250
+ reference **`markers`** (`{ value, label? }`) draw a labelled dotted rule
251
+ through every row in the annotation register — an SLA / capacity line —
252
+ with marker values joining the auto domain fit.
253
+ `<BarList>` draws proportional value bars; its sister `<BoxList>` draws a
254
+ five-number distribution per line — range band, `q1`→`q3` body, median line
255
+ — plus an optional **current-value tick** with a formatted inline label (the
256
+ esnet traffic-by-interface look), using the same quantile vocabulary as the
257
+ canvas `<BoxPlot>` (`lower`/`q1`/`median`/`q3`/`upper`, both-or-neither
258
+ body, quantiles computed upstream — `reduce` facts — never by the chart).
259
+ Styling stays on the one channel: bars resolve `theme.bar[as]`, boxes
260
+ `theme.box[as]`; both built-in themes gain a `box.secondary` role for the
261
+ paired-direction case. Readers `listRowsFromTimeSeries` /
262
+ `listRowsFromValueSeries` build one row per event / axis key. The in-plot
263
+ histogram remains `<BarChart orientation="horizontal">` — the lists are for
264
+ the table-shaped cases it can't be (link labels, cells, expanders, custom
265
+ sort).
266
+
267
+ - **charts:** **`BarStyle.hover` — a distinct hover colour for bars**
268
+ ([#577](https://github.com/pond-ts/pond/issues/577)). A theme may now give
269
+ bars a three-step emphasis — `fill` at rest → `hover` under the pointer →
270
+ `highlight` (plus the outline) when selected. Previously one `highlight`
271
+ served both live states, so hover and select differed only by the presence of
272
+ an outline; `ScatterStyle` has carried distinct rest / selected treatments
273
+ (`outline` vs `selectedOutline`) all along, making bars the less expressive
274
+ layer for the same two-state interaction.
275
+
276
+ **Optional, with a `highlight` fallback**, so no existing theme changes
277
+ meaning or rendering — a theme that wants the distinction adds one colour.
278
+ Selection outranks hover on a bar that is both. Single-series only: a stacked
279
+ or per-bin-coloured bar has no separate highlight colour to replace (it pops
280
+ its _own_ fill, so a red/green volume bar keeps its meaning while live), and
281
+ that convention is unchanged.
282
+
283
+ ### Changed
284
+
285
+ - **charts:** **a bar's capabilities now follow the mark it draws, not the
286
+ prop that fed it** ([PND-BARSEM], the 2026-08 API review's #2 item). A
287
+ **one-column vertical** histogram (`bins` + a single `column`) and a
288
+ one-entry `columns` draw exactly the mark a `series` + `column` chart
289
+ draws, but they used to route through the stacked path purely because of
290
+ which prop supplied them — and so silently lost whole-slot hit-testing
291
+ (#584), the `BarStyle.hover` colour, the cursor readout, stable per-bar
292
+ identity and per-bar decimation. They now take the single-series path, so
293
+ visually identical bars behave identically.
294
+
295
+ **What this changes in practice:** on a one-column histogram, hover and
296
+ click now hit the bar's **whole slot** rather than only the drawn
297
+ rectangle (so the space above a short bar is live, and slots tile the
298
+ axis); the layer gains a cursor readout; and `theme.bar.hover` applies.
299
+ A genuine multi-group stack, `categories`, and horizontal charts are
300
+ unchanged — their segments share a bin's x-range, so only y distinguishes
301
+ them. New reader `barsFromBins` backs the normalized path.
302
+
303
+ **Not a pure widening, in one respect:** dense-bar envelope decimation is
304
+ now live on a one-column histogram (it was single-series-only). It engages
305
+ only once bars fall under ~1px, where it is visually lossless, but at that
306
+ density the per-bar `gap` and highlight give way to envelope rects — pass
307
+ `decimate={false}` to keep every bar drawn. The `colors` map, the
308
+ `theme.bar[<column>]` role and `SelectInfo.label` are all preserved across
309
+ the reroute (each was a silent regression caught in review).
310
+
311
+ This shrinks `BarStyle.hover`'s scope warning from a list of five
312
+ path-accidents to the two real exclusions (a multi-group stack has no
313
+ hover channel on `StackStyle`; `binColors` keeps each bar's own colour by
314
+ design) — which was the acceptance test the task set itself.
315
+
316
+ - **charts:** **column names and source modes are now checked at compile
317
+ time** ([PND-CHARTAPI], the 2026-08 API review's #1 item). Every draw
318
+ layer's column props are derived from the series' schema, so
319
+ `<LineChart series={cpu} column="cpuu" />` — and a numeric prop pointed at
320
+ a string column, where the schema has some other numeric column — fail to
321
+ **compile** instead of throwing at render; the
322
+ same holds for `readout`, the band edges, the box quantiles, and the OHLC
323
+ prices. `<BarChart>`'s props became a **union of its legal source modes**,
324
+ so mixing sources (`series` + `bins`) or column forms (`column` +
325
+ `columns`), or passing `categories` a `column`, are compile errors too.
326
+ `<BarList>` / `<BoxList>` get the same treatment for `rows` XOR `series`,
327
+ which additionally closes the row-type hole #590 documented (annotating a
328
+ callback with a custom row type while passing `series` claimed a shape the
329
+ series door cannot produce).
330
+
331
+ **This narrows what compiles — deliberately.** Code carrying a typo, an
332
+ illegal mode mix, or a lying row annotation stops building; that is the
333
+ point, and each case was already a runtime failure. Two compatibility
334
+ behaviours are preserved on purpose: a **loosely-typed** series
335
+ (`TimeSeries<SeriesSchema>`, e.g. from a helper that doesn't narrow) still
336
+ accepts any column name, because an unparameterized schema leaves the name
337
+ union open and nothing can be checked against it; and `bins` column names
338
+ stay `string`, since they name aggregate fields of a bin record rather than
339
+ schema columns. Note the deliberate distinction: a schema that _does_ name
340
+ its columns but has **no numeric one** rejects every name — there is nothing
341
+ numeric to plot — which is not the same as the loose case.
342
+
343
+ **One new limitation.** Because a layer's props are a union _per series
344
+ kind_, a value typed as _either_ kind (`TimeSeries<A> | ValueSeries<B>` — a
345
+ wrapper that forwards whatever it is given) matches no single member and
346
+ must be narrowed or cast at the boundary. `DurationAxis.stories.tsx` is the
347
+ worked example. The alternative design (one generic over the series type)
348
+ handles that case but changes every props type's public generic parameters;
349
+ the trade is recorded in `spikes/charts-type-seam/REPORT.md`.
350
+
351
+ - **charts:** **a bar's hover / click target is now its whole slot**, not the
352
+ rectangle it draws. A bar _is_ the full width of its interval; the `gap` that
353
+ separates adjacent columns is a display affordance. Hit-testing the drawn
354
+ rect made that affordance interactive — the gap was a dead channel you could
355
+ point at and select nothing, and so was the empty plot space above a short
356
+ bar, even though the x-scrub cursor at that same x reported the bar quite
357
+ happily. `barAt` now tests the bar's full interval width and the full plot
358
+ height, so hover, click and the cursor readout all agree on which bar you are
359
+ on, and slots tile the axis.
360
+
361
+ **Widening, with one exception.** Points that previously selected _nothing_
362
+ now select the bar whose slot they fall in. The exception: a bar whose value
363
+ exceeds an explicit `<YAxis max>` used to draw — and be clickable — above the
364
+ plot top, in the strip a `'top'` axis title reserves; the slot stops at the
365
+ axis domain, so that sliver no longer hits. Everything inside the plot that
366
+ hit before still hits.
367
+
368
+ **It reaches across the full plot height, so it can shadow layers beneath
369
+ it.** The topmost hit wins, so a `<BarChart>` declared _after_ a
370
+ `<ScatterChart>` / `<BoxPlot>` / another `<BarChart>` in the same row now
371
+ claims every hit in its x-range at any y. Declare a bar layer **below** the
372
+ marks that should stay clickable.
373
+
374
+ **Single-series vertical only** — a stacked, `bins`, `categories` or
375
+ horizontal chart still hit-tests the drawn segment, because a stack's
376
+ segments share a bin's x-range and only y tells them apart.
377
+
378
+ Unchanged: a genuine hole between non-contiguous intervals still misses (the
379
+ change makes the drawing gap hittable, it doesn't invent coverage the data
380
+ lacks), a gap (`NaN`) bar owns no slot, and a shared edge goes to the left
381
+ bar — the rule `barIndexAtTime` already documented, so the two now agree by
382
+ construction.
383
+
384
+ ### Fixed
385
+
386
+ - **process:** **audit hardening — five wrong-answer / silent-acceptance paths
387
+ in the plan layer closed** (external Codex audit, 2026-08; all reproduced,
388
+ all regression-pinned). Unit validation of a **picked output** read
389
+ `outputs[0]` instead of the selected output, so a picked `variance` was
390
+ refused where variance was demanded and — worse, silently — accepted where
391
+ price was. `Registry.define()` now rejects duplicate input roles and
392
+ duplicate output ids (both used to _collapse_ at run time rather than fail:
393
+ inputs resolved to the last role, outputs dropped the earlier column),
394
+ invalid param defaults, and `dependsOn` naming unknown params. An op result
395
+ whose length does not match the bound series is refused at the producer —
396
+ it used to ride out unchecked whenever `assemble: false` skipped the only
397
+ length check. The column-selection loop now honours `onError` (an operator
398
+ exception escaped `'collect'`), and a selector naming a nonexistent output
399
+ is a `skipped` entry instead of silently surfacing nothing. Fact provenance
400
+ (`id`, `name`, `op`, `unit`) now wins over a custom fold body's fields, and
401
+ `columnBytes` sums a chunked column's chunks instead of reporting 0 — which
402
+ a byte budget would read as "free".
403
+
404
+ Also: the derived fold slots in both builders now key by **params** —
405
+ `shape({points: 100})` after `shape({points: 20})` silently returned the
406
+ 20-point node — and `shape` itself uses a `ceil` stride, so 200 points
407
+ asked of 399 rows returns ≤200 rather than all 399. The nested JSON Schema
408
+ projection can now express the `PickedOutput` input form, `Host` accepts
409
+ `budgetBytes` (the [PND-PROCCACHE] cap was unreachable from the long-lived
410
+ host shape) and grows `remove(id)`, and CI's package-content check covers
411
+ `@pond-ts/process`. Package remains **unpublished** (`private: true`).
412
+
413
+ A second audit round tightened the same seams. `columnBytes` now counts
414
+ what is actually retained: the **backing buffer's capacity** rather than
415
+ the column's logical length (core documents `_values` as possibly
416
+ oversized, so a one-row column viewing a 1000-slot buffer retains 8,000
417
+ bytes, not 8 — an undercount that defeats the budget), the chunk-offset
418
+ index and the bitmap's real bytes on chunked columns. The
419
+ **request-driven half of a `Host`'s footprint is now boundable**:
420
+ `runAsync` binds a graph per distinct caller-supplied `SourceRef`, so
421
+ `maxSources` caps registry-loaded sources LRU (author-added datasets are
422
+ never evicted), and a `remove()` racing an in-flight load now wins — the
423
+ landing load discards its result instead of resurrecting the dataset.
424
+ And an omitted param now unifies with its explicit default in the fluent
425
+ layer's derived fold slots (`shape()` ≡ `shape({points: 40})`, the same
426
+ rule `specId` applies), while the response labels a computation with the
427
+ **first** slot that named it rather than whichever was declared last.
428
+
429
+ - **charts:** **a hovered or selected single-series bar now pops to full
430
+ opacity** ([#576](https://github.com/pond-ts/pond/issues/576)). `drawBars`
431
+ set `globalAlpha` once to the resting `style.opacity` and never lifted it for
432
+ the highlight **fill** on the single-series path — only for the selected
433
+ bar's outline. The per-bar-`binColors` branch in the same function and
434
+ `drawStacks` both already popped to 1, and `drawBars`' own docstring claimed
435
+ it did too. So on a theme with `opacity < 1` a **hovered** bar (which has no
436
+ outline) barely changed, and a **selected** one read only by its outline.
437
+ All three paths now treat the highlight fill identically.
438
+
439
+ **This changes pixels** on any single-series `<BarChart>` with an alpha'd
440
+ theme: highlighted bars are brighter. A theme that raised its base `opacity`
441
+ to compensate may now want it back down.
442
+
443
+ **It also flattens select against hover**, which is worth knowing before you
444
+ upgrade. The selected bar's outline strokes in `highlight` — previously that
445
+ read as a brighter ring over an alpha'd fill, and it was the main thing
446
+ separating a selected bar from a hovered one. Now the fill underneath is the
447
+ same colour at the same alpha, so only the half of the stroke falling
448
+ outside the rect distinguishes them. Hover is no longer nearly invisible,
449
+ but the two live states are closer together. A theme that needs them clearly
450
+ apart should set the new `BarStyle.hover`.
451
+
58
452
  ## [0.54.0] — 2026-08-02
59
453
 
454
+ ### Fixed
455
+
456
+ - **core:** **`fromArrow` now reads a field's declared Arrow type instead of
457
+ guessing from the runtime shape of `toArray()`** — closing a
458
+ silent-corruption class. The reader worked out what a column held from what
459
+ `toArray()` handed back, which is correct for the types it supports and
460
+ quietly wrong outside them, because Arrow's physical layouts do not all store
461
+ one machine word per logical value. Measured, before the fix: **`Float16`
462
+ ingested `1.5` as `15872`** (its half-float bit pattern — the length matched,
463
+ so nothing caught it), and a **`Decimal128` column with a single null
464
+ ingested `123.45` as `12345`** (the per-element path produced exactly `rows`
465
+ values, so the length check never fired). A dense `Decimal` merely threw the
466
+ wrong error, blaming a length mismatch.
467
+
468
+ The readable set is now an explicit allowlist — `Int` (any width),
469
+ `Float32`/`Float64`, `Date32`/`Date64`, `Time32`/`Time64`, `Timestamp`,
470
+ `Utf8`/`LargeUtf8`/`Utf8View`, `Null` (an all-missing value column), and a
471
+ `Dictionary` of any of those (the encoding is transparent; readability
472
+ follows the value type) — checked per field, on the key and value columns of every
473
+ Arrow door (`TimeSeries.fromArrow`, `ValueSeries.fromArrow`, and the
474
+ flattened key edges). Anything else is refused **by name**, with the cast
475
+ that would fix it: `Decimal` names the float64 precision trade-off, `Float16`
476
+ says to cast, `Bool` names the real reason (the columnar ingest engine
477
+ carries `number` and `string` value columns only). A duck-typed stand-in
478
+ carrying no `typeId` keeps working — the `ArrowTableLike` contract is
479
+ deliberately structural — and gains a width check that catches the Decimal
480
+ shape anyway.
481
+
482
+ Behavioural change worth noting: a `Utf8` **key** now throws on its declared
483
+ type rather than on its shape, so the message names the type and points at
484
+ passing it as a value column instead.
485
+
486
+ ### Changed
487
+
488
+ - **A fold no longer builds a `TimeSeries`** ([PND-PROCTERM]). Every node's
489
+ `compute` widened the source with `appendColumn` for each nested input, so
490
+ an op could call the corpus normally — the studies take
491
+ `(series, { column })`. For a fold that was waste twice over: the column it
492
+ reads is already in its inputs, and it was being packed into a series only
493
+ to be read straight back out.
494
+
495
+ The cost was not incidental. `appendColumn` **boxes a gapped column** on
496
+ the way in, because core's `withColumn` takes values rather than a column —
497
+ 22.4 ms per column at 1M rows. Every rolling study is gapped, so the
498
+ expensive path was the ordinary one.
499
+
500
+ 20 folds × 500k rows, on top of the columnar fold context below:
501
+ **383 → 129 ms** (2.96×), rss 173 → 113 MB. Against the boxed, assembling
502
+ baseline the two changes together are **606 → 129 ms**.
503
+
504
+ A facts-only request now returns no `series` at all, and the upstream
505
+ column still resolves through the node graph rather than the terminal's
506
+ `needed` set — so the failure the plan warned about, a fact silently
507
+ coming back with no value because its column was never selected, cannot
508
+ happen.
509
+
60
510
  ### Added
61
511
 
62
512
  - **`ctx.out` — prepared output buffers for a ranged recompute**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/fit",
3
- "version": "0.54.0",
3
+ "version": "0.56.2",
4
4
  "private": false,
5
5
  "description": "Fitness & activity domain library on pond-ts — quantities, canonical activity series, and analytics (geo, power, zones, splits)",
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "verify": "npm run format:check && npm run build && npm test"
38
38
  },
39
39
  "peerDependencies": {
40
- "pond-ts": "^0.54.0"
40
+ "pond-ts": "^0.56.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "typescript": "^5.6.3",