pond-ts 0.47.0 → 0.48.1

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.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  tag, so this file covers them all. Pre-1.0: minor bumps may include new features
9
9
  and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pond-ts/pond/compare/v0.47.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.48.1...HEAD
12
+ [0.48.1]: https://github.com/pond-ts/pond/compare/v0.48.0...v0.48.1
13
+ [0.48.0]: https://github.com/pond-ts/pond/compare/v0.47.0...v0.48.0
12
14
  [0.47.0]: https://github.com/pond-ts/pond/compare/v0.46.0...v0.47.0
13
15
  [0.46.0]: https://github.com/pjm17971/pond-ts/compare/v0.45.0...v0.46.0
14
16
  [0.45.0]: https://github.com/pjm17971/pond-ts/compare/v0.44.1...v0.45.0
@@ -46,6 +48,48 @@ and type-level changes; patch bumps are strictly additive.
46
48
 
47
49
  ## [Unreleased]
48
50
 
51
+ ## [0.48.1] — 2026-07-19
52
+
53
+ ### Fixed
54
+
55
+ - `smooth(column, 'loess', …)` is now numerically stable on absolute epoch-ms
56
+ timestamps. The local weighted regression is now conditioned on `x` centred at
57
+ the query point; previously the un-centred normal equations lost precision to
58
+ floating-point cancellation (anchors ~1.7e12 → `x²` terms ~3e24). Severity
59
+ scales with (anchor magnitude / window width)²: on finely-spaced series
60
+ (second-level cadence) the fit overshot wildly outside the data range instead
61
+ of smoothing, while coarser day-spaced series erred only subtly. Output is now
62
+ shift-invariant to the time origin. Series built from small integer anchors
63
+ (e.g. in unit tests) were unaffected, which is why this hid.
64
+
65
+ ## [0.48.0] — 2026-07-17
66
+
67
+ ### Added
68
+
69
+ - **charts:** `<ChartContainer>` gains **`cursorFormat`** — an independent
70
+ format for the **cursor / marker readout** (the crosshair time pill, marker
71
+ axis indicators, annotation auto-labels), separate from the tick-label
72
+ `timeFormat` / `format`. Unlike `timeFormat` (which _owns the labels_ and so
73
+ opts the axis out of the `dateStyle` ladder by design), `cursorFormat` shapes
74
+ only the readout and **keeps the flat / stacked date style** — resolving the
75
+ "one knob, two concerns" bind where the only way to fix the pill was to give
76
+ up the styled axis. A d3 specifier **string** formats uniformly; a
77
+ **function** `(epochMs, { grain, defaultText }) => string` is handed the
78
+ axis's resolved coarse **`TimeGrain`** (`year` … `second`) and the
79
+ grain-aware default text, so it can branch on zoom and pass the default
80
+ through — no re-deriving the grain from the range. New public types
81
+ **`CursorFormat`** / **`TimeGrain`**; `TradingTimeScale` gains
82
+ `readoutFormat(count)` and `grain(count)`.
83
+
84
+ ### Fixed
85
+
86
+ - **charts:** the cursor / marker time readout now formats at the axis's
87
+ **own grain** by default instead of d3's multi-scale default — a
88
+ day-or-coarser axis reads a **date** (`Sep 14, 2026`), a sub-day axis reads
89
+ date + clock. Fixes the 0.47.0 flat-axis regression where a daily bar at a
90
+ foreign-timezone midnight rendered as a bare time-of-day (`02 AM`) in the
91
+ crosshair pill (Tidal F-charts-7). Tick labels are unchanged.
92
+
49
93
  ## [0.47.0] — 2026-07-17
50
94
 
51
95
  ### Added
@@ -187,10 +187,16 @@ function loessAt(x, anchors, values, span) {
187
187
  let sumWY = 0;
188
188
  let sumWXX = 0;
189
189
  let sumWXY = 0;
190
+ // Regress in local coordinates centred on the query point (`dx = pointX - x`).
191
+ // Anchors are typically absolute epoch-ms (~1.7e12), so accumulating
192
+ // `pointX * pointX` (~3e24) into the normal equations loses all precision to
193
+ // floating-point cancellation and the fit overshoots wildly. Centring keeps
194
+ // every term small; the fitted value at the query point is then just the
195
+ // intercept, since `dx = 0` there.
190
196
  for (let index = start; index < end; index++) {
191
- const pointX = anchors[index];
197
+ const dx = anchors[index] - x;
192
198
  const pointY = values[index];
193
- const distance = Math.abs(pointX - x);
199
+ const distance = Math.abs(dx);
194
200
  const ratio = distance / bandwidth;
195
201
  const weight = ratio >= 1 ? 0 : (1 - ratio ** 3) ** 3;
196
202
  if (weight === 0) {
@@ -198,10 +204,10 @@ function loessAt(x, anchors, values, span) {
198
204
  }
199
205
  weightedCount += 1;
200
206
  sumW += weight;
201
- sumWX += weight * pointX;
207
+ sumWX += weight * dx;
202
208
  sumWY += weight * pointY;
203
- sumWXX += weight * pointX * pointX;
204
- sumWXY += weight * pointX * pointY;
209
+ sumWXX += weight * dx * dx;
210
+ sumWXY += weight * dx * pointY;
205
211
  }
206
212
  if (weightedCount === 0 || sumW === 0) {
207
213
  return undefined;
@@ -210,9 +216,8 @@ function loessAt(x, anchors, values, span) {
210
216
  if (Math.abs(denominator) < Number.EPSILON) {
211
217
  return sumWY / sumW;
212
218
  }
213
- const intercept = (sumWY * sumWXX - sumWX * sumWXY) / denominator;
214
- const slope = (sumW * sumWXY - sumWX * sumWY) / denominator;
215
- return intercept + slope * x;
219
+ // Value at the query point: intercept + slope * dx, with dx = 0.
220
+ return (sumWY * sumWXX - sumWX * sumWXY) / denominator;
216
221
  }
217
222
  function lowerBound(values, target) {
218
223
  let low = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pond-ts",
3
- "version": "0.47.0",
3
+ "version": "0.48.1",
4
4
  "description": "TypeScript-first time series primitives",
5
5
  "license": "MIT",
6
6
  "repository": {