pond-ts 0.41.0 → 0.42.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.
- package/CHANGELOG.md +74 -5
- package/dist/column.d.ts +32 -4
- package/dist/column.js +252 -60
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
-
The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
7
|
-
`@pond-ts/fit` — release together under a single `v*`
|
|
8
|
-
them all. Pre-1.0: minor bumps may include new features
|
|
9
|
-
patch bumps are strictly additive.
|
|
6
|
+
The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
7
|
+
`@pond-ts/fit`, and `@pond-ts/financial` — release together under a single `v*`
|
|
8
|
+
tag, so this file covers them all. Pre-1.0: minor bumps may include new features
|
|
9
|
+
and type-level changes; patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.42.0...HEAD
|
|
12
|
+
[0.42.0]: https://github.com/pjm17971/pond-ts/compare/v0.41.0...v0.42.0
|
|
12
13
|
[0.41.0]: https://github.com/pjm17971/pond-ts/compare/v0.40.0...v0.41.0
|
|
13
14
|
[0.40.0]: https://github.com/pjm17971/pond-ts/compare/v0.39.0...v0.40.0
|
|
14
15
|
[0.39.0]: https://github.com/pjm17971/pond-ts/compare/v0.38.0...v0.39.0
|
|
@@ -39,6 +40,74 @@ patch bumps are strictly additive.
|
|
|
39
40
|
|
|
40
41
|
## [Unreleased]
|
|
41
42
|
|
|
43
|
+
## [0.42.0] — 2026-07-10
|
|
44
|
+
|
|
45
|
+
The **trading-calendar** release: a new `@pond-ts/financial` package (its first
|
|
46
|
+
publish) and a discontinuous **trading-time x axis** in `@pond-ts/charts` that
|
|
47
|
+
collapses closed-market time (weekends, holidays, overnight, lunch breaks).
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`@pond-ts/financial` — new package (first release).** A calendar/analytics
|
|
52
|
+
layer on `pond-ts` (peer dep; ESM, no React; `@js-temporal/polyfill` for
|
|
53
|
+
DST-correct session generation):
|
|
54
|
+
- **`TradingCalendar`** — `fromSessions` (explicit schedule) and `fromRules`
|
|
55
|
+
(weekmask / holidays / early-closes / breaks, DST-correct via Temporal);
|
|
56
|
+
query surface (`sessionOn`, `sessionContaining`, `isOpen`, `sessionsInRange`,
|
|
57
|
+
`nextSession`, `previousSession`).
|
|
58
|
+
- **Bucketing seam** — `sessionSequence()` / `barSequence(period)` return a
|
|
59
|
+
`BoundedSequence` that flows straight through `aggregate` / `materialize`,
|
|
60
|
+
so every bucket is a real trading session/bar (no weekend/holiday buckets,
|
|
61
|
+
no bucket spanning a closure). Zero core edits.
|
|
62
|
+
- **`tagSessions(series, { column?, stamped? })`** — appends a session-id
|
|
63
|
+
column (`number | undefined`) for `partitionBy` so stateful ops don't bridge
|
|
64
|
+
a session boundary. `stamped: 'close'` bins a bar stamped at its close into
|
|
65
|
+
the closing session (`(open, close]`) for OHLC feeds.
|
|
66
|
+
- **`DiscontinuityProvider`** — the d3fc-style 5-method provider
|
|
67
|
+
(`clampUp`/`clampDown`/`distance`/`offset`/`copy` + optional `boundaries`);
|
|
68
|
+
`identityDiscontinuity`, `weekendSkip` (bundled reference), and
|
|
69
|
+
`segmentDiscontinuity(segments, { spacing })`.
|
|
70
|
+
- **`TradingCalendar.discontinuities({ range?, spacing?, period? })`** — the
|
|
71
|
+
chart-ready provider; `spacing: 'proportional'` (default, true-time) or
|
|
72
|
+
`'uniform'` (equal-width per session/bar, the TradingView ordinal look).
|
|
73
|
+
- **`@pond-ts/charts`: trading-time x axis.** Pass a `@pond-ts/financial`
|
|
74
|
+
provider (structurally — charts never imports that package) to collapse
|
|
75
|
+
closed-market gaps:
|
|
76
|
+
- **`ChartContainer` `discontinuities` prop** (low-level) and **`calendar` +
|
|
77
|
+
`spacing` props** (high-level sugar; `calendar` is a structural
|
|
78
|
+
`TradingCalendarLike`, `spacing` defaults to proportional).
|
|
79
|
+
- **`scaleTradingTime`** — a d3-scale-shaped discontinuous time scale; ticks
|
|
80
|
+
coarsen to a **calendar grain** (week/month/quarter/year starts) with
|
|
81
|
+
date/year labels, and **session dividers** draw at the collapse points
|
|
82
|
+
(`theme.axis.sessionDivider`), aligned with the labels.
|
|
83
|
+
- `Charts/TradingTimeAxis` stories (weekend/holiday/half-day/intraday,
|
|
84
|
+
continuous-vs-trading, daily-months, proportional-vs-uniform).
|
|
85
|
+
- **`@pond-ts/charts`:** selection now has a stable series identity. `SelectInfo`
|
|
86
|
+
carries an `id`, and `BarChart` / `ScatterChart` take an optional `id` prop —
|
|
87
|
+
the series identity used for selection + hover. An `id` **gates interactivity**:
|
|
88
|
+
a layer is selectable/hoverable only when given one (a layer with no `id`
|
|
89
|
+
renders and reads out but can't be selected). A dev-warning fires when
|
|
90
|
+
`selected`/`onSelect` is wired but no layer carries an `id`. First slice of the
|
|
91
|
+
selection RFC (`docs/rfcs/selection.md`, Amendments 2–3).
|
|
92
|
+
- **`pond-ts`: `bin(W, 'minMaxFirstLast')`** — the four-channel M4 downsampling
|
|
93
|
+
reducer (per-bin min/max/first/last, validity-aware, chunked-delegating); the
|
|
94
|
+
foundation for the charts decimator wave.
|
|
95
|
+
- **`pond-ts`: `binBy`** — key-domain bucketed reduction (the M4 gappy-data
|
|
96
|
+
decimation path).
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- **BREAKING (`@pond-ts/charts`):** `SelectInfo` gained a required `id` field
|
|
101
|
+
(`{ id, key, value, color, label }`) — `id` is the selection identity, `key` /
|
|
102
|
+
`value` are now click provenance. Code that constructs a `SelectInfo` by hand
|
|
103
|
+
must add `id`, and selection equality/dedup now keys on `id`, not the sample
|
|
104
|
+
`begin`.
|
|
105
|
+
- **BREAKING (`@pond-ts/charts`):** `BarChart` / `ScatterChart` selection now
|
|
106
|
+
requires an explicit `id` prop — the previous implicit `as ?? column` selection
|
|
107
|
+
identity is gone. A selectable bar/scatter layer must add `id` (e.g.
|
|
108
|
+
`<BarChart series={s} column="v" id="v" />`); without it the layer is
|
|
109
|
+
display-only.
|
|
110
|
+
|
|
42
111
|
## [0.41.0] — 2026-07-06
|
|
43
112
|
|
|
44
113
|
### Added
|
package/dist/column.d.ts
CHANGED
|
@@ -69,9 +69,14 @@ export type PublicColumnForKind<K extends 'number' | 'boolean' | 'string' | 'arr
|
|
|
69
69
|
* convention (e.g. `'p95'`, `'p99.9'`) where `q` is in `[0, 100]` —
|
|
70
70
|
* runtime check enforces the range. The fused `'minMax'` is special:
|
|
71
71
|
* it returns a two-channel `{ lo, hi }` rather than a single
|
|
72
|
-
* `Float64Array`.
|
|
72
|
+
* `Float64Array`. The fused `'minMaxFirstLast'` extends that to four
|
|
73
|
+
* channels `{ lo, hi, first, last }` — the M4 downsampling reducer
|
|
74
|
+
* (Jugel et al., VLDB 2014): `first`/`last` are the first/last
|
|
75
|
+
* defined value in each bin, which keep a decimated polyline
|
|
76
|
+
* continuous across bin boundaries (the line enters at `first`,
|
|
77
|
+
* exits at `last`).
|
|
73
78
|
*/
|
|
74
|
-
export type BinReducerName = 'min' | 'max' | 'sum' | 'mean' | 'stdev' | 'median' | 'count' | `p${number}` | 'minMax';
|
|
79
|
+
export type BinReducerName = 'min' | 'max' | 'sum' | 'mean' | 'stdev' | 'median' | 'count' | `p${number}` | 'minMax' | 'minMaxFirstLast';
|
|
75
80
|
/**
|
|
76
81
|
* Output type for `Float64Column.bin(W, reducer)`. Narrows
|
|
77
82
|
* on the reducer name so consumers don't need a runtime cast:
|
|
@@ -84,6 +89,12 @@ export type BinReducerName = 'min' | 'max' | 'sum' | 'mean' | 'stdev' | 'median'
|
|
|
84
89
|
* — stride-1 access per channel matches the canvas-2D inner draw
|
|
85
90
|
* loop's per-pixel `lo[px]` / `hi[px]` reads. Empty bins on both
|
|
86
91
|
* channels are `NaN`.
|
|
92
|
+
* - `'minMaxFirstLast'` produces the four-channel
|
|
93
|
+
* `{ lo, hi, first, last }` (all `Float64Array(W)`) — the M4
|
|
94
|
+
* reducer. `first`/`last` carry the bin's first/last defined
|
|
95
|
+
* value so a decimated line stays continuous at bin seams. Empty
|
|
96
|
+
* bins are `NaN` on all four channels (canvas-friendly: a `NaN`
|
|
97
|
+
* vertex breaks the sub-path, the correct "no data here" visual).
|
|
87
98
|
*
|
|
88
99
|
* Generalized for future multi-point reducers (e.g. LTTB) — those
|
|
89
100
|
* would land as their own output shape (e.g. `{ keys, values }`
|
|
@@ -92,6 +103,11 @@ export type BinReducerName = 'min' | 'max' | 'sum' | 'mean' | 'stdev' | 'median'
|
|
|
92
103
|
export type BinOutput<R extends BinReducerName> = R extends 'minMax' ? {
|
|
93
104
|
lo: Float64Array;
|
|
94
105
|
hi: Float64Array;
|
|
106
|
+
} : R extends 'minMaxFirstLast' ? {
|
|
107
|
+
lo: Float64Array;
|
|
108
|
+
hi: Float64Array;
|
|
109
|
+
first: Float64Array;
|
|
110
|
+
last: Float64Array;
|
|
95
111
|
} : Float64Array;
|
|
96
112
|
/**
|
|
97
113
|
* Public key-column class for a single first-column kind.
|
|
@@ -239,8 +255,9 @@ declare module './columnar/column.js' {
|
|
|
239
255
|
* and §8 worked example.
|
|
240
256
|
*
|
|
241
257
|
* Returns a fresh `Float64Array(bins)` for scalar reducers,
|
|
242
|
-
*
|
|
243
|
-
* `
|
|
258
|
+
* `{ lo, hi }` for `'minMax'`, or the four-channel
|
|
259
|
+
* `{ lo, hi, first, last }` for `'minMaxFirstLast'` (the M4
|
|
260
|
+
* downsampling reducer). An earlier revision exposed an optional
|
|
244
261
|
* `{ out }` parameter for buffer reuse; the
|
|
245
262
|
* chart-experiment M2.3 measurement showed it contributed
|
|
246
263
|
* nothing on top of the chart-side Y-from-bin-output
|
|
@@ -250,6 +267,16 @@ declare module './columnar/column.js' {
|
|
|
250
267
|
* V8 nursery GC budget.
|
|
251
268
|
*/
|
|
252
269
|
bin<R extends BinReducerName>(bins: number, reducer: R): BinOutput<R>;
|
|
270
|
+
/**
|
|
271
|
+
* Key-domain bucketed reduction — buckets by explicit sorted
|
|
272
|
+
* `edges` over a parallel monotonic `key`, rather than by equal
|
|
273
|
+
* index count. The M4 decimator's correct form for irregular /
|
|
274
|
+
* gappy data (a bin maps to a range of the key axis, not of
|
|
275
|
+
* indices). Same reducers and output shapes as `bin`. O(n + W).
|
|
276
|
+
* See the impl JSDoc for the merge-walk and edge/precondition
|
|
277
|
+
* semantics.
|
|
278
|
+
*/
|
|
279
|
+
binBy<R extends BinReducerName>(key: ArrayLike<number>, edges: ArrayLike<number>, reducer: R): BinOutput<R>;
|
|
253
280
|
}
|
|
254
281
|
interface BooleanColumn {
|
|
255
282
|
at(i: number): boolean | undefined;
|
|
@@ -320,6 +347,7 @@ declare module './columnar/chunked-column.js' {
|
|
|
320
347
|
*/
|
|
321
348
|
toFloat64Array(): Float64Array;
|
|
322
349
|
bin<R extends BinReducerName>(bins: number, reducer: R): BinOutput<R>;
|
|
350
|
+
binBy<R extends BinReducerName>(key: ArrayLike<number>, edges: ArrayLike<number>, reducer: R): BinOutput<R>;
|
|
323
351
|
}
|
|
324
352
|
interface ChunkedBooleanColumn {
|
|
325
353
|
at(i: number): boolean | undefined;
|
package/dist/column.js
CHANGED
|
@@ -233,43 +233,117 @@ Float64Column.prototype.bin = function (bins, reducer) {
|
|
|
233
233
|
throw new RangeError(`Float64Column.bin: bins must be a positive integer, got ${bins}`);
|
|
234
234
|
}
|
|
235
235
|
const n = this.length;
|
|
236
|
+
// Equal-width **index** boundaries: bucket b spans the index range
|
|
237
|
+
// [bounds[b], bounds[b + 1]). `Math.floor((b * n) / bins)` is the
|
|
238
|
+
// same split the per-bin loops used inline before the boundary
|
|
239
|
+
// derivation was factored out (see `reduceFloat64ByBounds`).
|
|
240
|
+
const bounds = new Int32Array(bins + 1);
|
|
241
|
+
for (let b = 1; b < bins; b += 1)
|
|
242
|
+
bounds[b] = Math.floor((b * n) / bins);
|
|
243
|
+
bounds[bins] = n;
|
|
244
|
+
return reduceFloat64ByBounds(this, bounds, reducer, 'bin');
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Key-domain bucketed reduction — the M4 decimator's correct form
|
|
248
|
+
* for **irregular / gappy** data. Where `bin` splits by equal index
|
|
249
|
+
* count (right only for uniformly spaced samples — see its
|
|
250
|
+
* uniform-sampling precondition), `binBy` buckets by an explicit set
|
|
251
|
+
* of sorted `edges` over a parallel monotonic `key`, so a bin maps
|
|
252
|
+
* to a range of the **key axis** (pixel-time, or a value axis), not
|
|
253
|
+
* a range of indices. Chart use:
|
|
254
|
+
*
|
|
255
|
+
* ```ts
|
|
256
|
+
* // edges = W+1 device-pixel column boundaries in time, unioned
|
|
257
|
+
* // with any gap edges (so no bucket spans a gap).
|
|
258
|
+
* const { lo, hi, first, last } = valueCol.binBy(
|
|
259
|
+
* timeKeys, edges, 'minMaxFirstLast',
|
|
260
|
+
* );
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* Because `key` is non-decreasing, every key-interval maps to a
|
|
264
|
+
* **contiguous** index range, so `binBy` derives per-bucket index
|
|
265
|
+
* boundaries in one O(n + W) merge walk and then runs the exact same
|
|
266
|
+
* per-bucket reducer engine as `bin` — only the boundary derivation
|
|
267
|
+
* differs. Bucket `b` covers samples with `edges[b] <= key <
|
|
268
|
+
* edges[b + 1]`; the **final** upper edge is inclusive so a sample
|
|
269
|
+
* exactly at the max edge lands in the last bucket. An empty bucket
|
|
270
|
+
* (a gap with no samples) reduces to `NaN` — the canvas
|
|
271
|
+
* sub-path-break sentinel, same as `bin`'s empty bins.
|
|
272
|
+
*
|
|
273
|
+
* **Preconditions** (documented, not re-asserted per frame — this is
|
|
274
|
+
* a per-pixel hot path): `key` is non-decreasing and `key.length ===
|
|
275
|
+
* this.length`; `edges` is ascending. A non-ascending `edges` is
|
|
276
|
+
* caught (cheap, once per bucket); a non-monotonic `key` is the
|
|
277
|
+
* caller's responsibility (the chart adapter and `byValue` both
|
|
278
|
+
* guarantee it by construction).
|
|
279
|
+
*
|
|
280
|
+
* Reducers and output shapes are identical to `bin` (`'minMax'` →
|
|
281
|
+
* `{ lo, hi }`, `'minMaxFirstLast'` → `{ lo, hi, first, last }`,
|
|
282
|
+
* scalars → `Float64Array(W)`).
|
|
283
|
+
*/
|
|
284
|
+
Float64Column.prototype.binBy = function (key, edges, reducer) {
|
|
285
|
+
const n = this.length;
|
|
286
|
+
if (key.length !== n) {
|
|
287
|
+
throw new RangeError(`Float64Column.binBy: key length ${key.length} must equal column length ${n}`);
|
|
288
|
+
}
|
|
289
|
+
const W = edges.length - 1;
|
|
290
|
+
if (W < 1) {
|
|
291
|
+
throw new RangeError(`Float64Column.binBy: edges must have at least 2 entries (1 bucket), got length ${edges.length}`);
|
|
292
|
+
}
|
|
293
|
+
// Merge-walk the monotonic key against the sorted edges → per-bucket
|
|
294
|
+
// index boundaries. `i` only moves forward (both key and edges
|
|
295
|
+
// ascend), so this is a single O(n + W) pass. bounds[b] = first
|
|
296
|
+
// index with key >= edges[b]; the final bound uses key > edges[W]
|
|
297
|
+
// so the max edge is inclusive.
|
|
298
|
+
const bounds = new Int32Array(W + 1);
|
|
299
|
+
let i = 0;
|
|
300
|
+
let prevEdge = edges[0];
|
|
301
|
+
for (let b = 0; b <= W; b += 1) {
|
|
302
|
+
const e = edges[b];
|
|
303
|
+
if (b > 0 && e < prevEdge) {
|
|
304
|
+
throw new RangeError(`Float64Column.binBy: edges must be ascending; edges[${b}]=${e} < edges[${b - 1}]=${prevEdge}`);
|
|
305
|
+
}
|
|
306
|
+
prevEdge = e;
|
|
307
|
+
if (b < W) {
|
|
308
|
+
while (i < n && key[i] < e)
|
|
309
|
+
i += 1;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
while (i < n && key[i] <= e)
|
|
313
|
+
i += 1;
|
|
314
|
+
}
|
|
315
|
+
bounds[b] = i;
|
|
316
|
+
}
|
|
317
|
+
return reduceFloat64ByBounds(this, bounds, reducer, 'binBy');
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* Shared per-bucket reduction engine behind `bin` and `binBy`. Given
|
|
321
|
+
* ascending index boundaries (`bounds[b]..bounds[b + 1]` is bucket
|
|
322
|
+
* b's index range) and a reducer, produces the per-bucket output.
|
|
323
|
+
* `bin` supplies equal-index boundaries; `binBy` supplies
|
|
324
|
+
* key-edge-derived boundaries — the reduction is identical either
|
|
325
|
+
* way. `method` names the caller for error messages.
|
|
326
|
+
*
|
|
327
|
+
* The three fused-`minMax*` paths hoist the validity / finiteness
|
|
328
|
+
* branch **outside** the bucket loop (it's the same for every
|
|
329
|
+
* bucket), which is why they're inlined here rather than dispatching
|
|
330
|
+
* through `sliceByRange().minMax()` per bucket.
|
|
331
|
+
*/
|
|
332
|
+
function reduceFloat64ByBounds(col, bounds, reducer, method) {
|
|
333
|
+
const W = bounds.length - 1;
|
|
334
|
+
const values = col._values;
|
|
335
|
+
const validity = col.validity;
|
|
236
336
|
// ─── Fused minMax — two-channel output ─────────────────────────
|
|
237
|
-
//
|
|
238
|
-
// Special-cased because it's the chart's per-pixel hot path and
|
|
239
|
-
// its output shape ({lo, hi}) doesn't fit the scalar Float64Array
|
|
240
|
-
// mold. Each bin walks once, fuses both reductions, writes both
|
|
241
|
-
// channels — half the memory traffic of `[min(), max()]`.
|
|
242
337
|
if (reducer === 'minMax') {
|
|
243
|
-
const lo = new Float64Array(
|
|
244
|
-
const hi = new Float64Array(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
// Measured wins (`scripts/perf-bin.mjs`, median of 30 × 3
|
|
254
|
-
// runs): ~23% on fine-bins (N=100k, W=1024) where per-bin
|
|
255
|
-
// overhead is the biggest fraction of work; ~9% on chart-
|
|
256
|
-
// typical (N=1M, W=1024); within noise (~5%) on N=10M where
|
|
257
|
-
// the inner buffer walk dominates.
|
|
258
|
-
//
|
|
259
|
-
// The inner-loop math is byte-identical to
|
|
260
|
-
// `Float64Column.prototype.minMax` (same NaN parity, same
|
|
261
|
-
// empty handling) — just over a (start, end) slice of the
|
|
262
|
-
// underlying buffer.
|
|
263
|
-
const values = this._values;
|
|
264
|
-
const validity = this.validity;
|
|
265
|
-
if (!this.allFinite) {
|
|
266
|
-
// Guarded path: skip non-finite values (reducer non-finite policy —
|
|
267
|
-
// docs/notes/reducer-nan-policy.md), matching `minMax()` / the reducer
|
|
268
|
-
// min/max. A bin with no finite value (all-missing or all-non-finite)
|
|
269
|
-
// keeps the NaN empty-bin sentinel.
|
|
270
|
-
for (let b = 0; b < bins; b += 1) {
|
|
271
|
-
const start = Math.floor((b * n) / bins);
|
|
272
|
-
const end = Math.floor(((b + 1) * n) / bins);
|
|
338
|
+
const lo = new Float64Array(W);
|
|
339
|
+
const hi = new Float64Array(W);
|
|
340
|
+
if (!col.allFinite) {
|
|
341
|
+
// Guarded path: skip non-finite values (reducer non-finite
|
|
342
|
+
// policy — docs/notes/reducer-nan-policy.md). A bucket with no
|
|
343
|
+
// finite value keeps the NaN empty-bucket sentinel.
|
|
344
|
+
for (let b = 0; b < W; b += 1) {
|
|
345
|
+
const start = bounds[b];
|
|
346
|
+
const end = bounds[b + 1];
|
|
273
347
|
let loVal;
|
|
274
348
|
let hiVal;
|
|
275
349
|
for (let i = start; i < end; i += 1) {
|
|
@@ -301,9 +375,9 @@ Float64Column.prototype.bin = function (bins, reducer) {
|
|
|
301
375
|
return { lo, hi };
|
|
302
376
|
}
|
|
303
377
|
if (validity === undefined) {
|
|
304
|
-
for (let b = 0; b <
|
|
305
|
-
const start =
|
|
306
|
-
const end =
|
|
378
|
+
for (let b = 0; b < W; b += 1) {
|
|
379
|
+
const start = bounds[b];
|
|
380
|
+
const end = bounds[b + 1];
|
|
307
381
|
if (end <= start) {
|
|
308
382
|
lo[b] = NaN;
|
|
309
383
|
hi[b] = NaN;
|
|
@@ -321,12 +395,10 @@ Float64Column.prototype.bin = function (bins, reducer) {
|
|
|
321
395
|
}
|
|
322
396
|
}
|
|
323
397
|
else {
|
|
324
|
-
// Validity path —
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const start = Math.floor((b * n) / bins);
|
|
329
|
-
const end = Math.floor(((b + 1) * n) / bins);
|
|
398
|
+
// Validity path — isDefined directly on the original bitmap.
|
|
399
|
+
for (let b = 0; b < W; b += 1) {
|
|
400
|
+
const start = bounds[b];
|
|
401
|
+
const end = bounds[b + 1];
|
|
330
402
|
if (end <= start) {
|
|
331
403
|
lo[b] = NaN;
|
|
332
404
|
hi[b] = NaN;
|
|
@@ -336,7 +408,6 @@ Float64Column.prototype.bin = function (bins, reducer) {
|
|
|
336
408
|
while (i < end && !validity.isDefined(i))
|
|
337
409
|
i += 1;
|
|
338
410
|
if (i >= end) {
|
|
339
|
-
// All cells in this bin are undefined.
|
|
340
411
|
lo[b] = NaN;
|
|
341
412
|
hi[b] = NaN;
|
|
342
413
|
continue;
|
|
@@ -356,39 +427,154 @@ Float64Column.prototype.bin = function (bins, reducer) {
|
|
|
356
427
|
}
|
|
357
428
|
return { lo, hi };
|
|
358
429
|
}
|
|
430
|
+
// ─── Fused minMaxFirstLast — four-channel output (M4) ──────────
|
|
431
|
+
//
|
|
432
|
+
// Extends fused minMax with per-bucket first/last (first/last
|
|
433
|
+
// defined-and-finite value), so a downsampled polyline enters each
|
|
434
|
+
// bucket at `first` and leaves at `last` — continuous across bucket
|
|
435
|
+
// seams. All four channels agree on which cells count; an empty
|
|
436
|
+
// bucket is NaN on all four.
|
|
437
|
+
if (reducer === 'minMaxFirstLast') {
|
|
438
|
+
const lo = new Float64Array(W);
|
|
439
|
+
const hi = new Float64Array(W);
|
|
440
|
+
const first = new Float64Array(W);
|
|
441
|
+
const last = new Float64Array(W);
|
|
442
|
+
if (!col.allFinite) {
|
|
443
|
+
for (let b = 0; b < W; b += 1) {
|
|
444
|
+
const start = bounds[b];
|
|
445
|
+
const end = bounds[b + 1];
|
|
446
|
+
let loVal;
|
|
447
|
+
let hiVal = 0;
|
|
448
|
+
let firstVal = 0;
|
|
449
|
+
let lastVal = 0;
|
|
450
|
+
for (let i = start; i < end; i += 1) {
|
|
451
|
+
if (validity !== undefined && !validity.isDefined(i))
|
|
452
|
+
continue;
|
|
453
|
+
const x = values[i];
|
|
454
|
+
if (!Number.isFinite(x))
|
|
455
|
+
continue;
|
|
456
|
+
if (loVal === undefined) {
|
|
457
|
+
loVal = x;
|
|
458
|
+
hiVal = x;
|
|
459
|
+
firstVal = x;
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
if (x < loVal)
|
|
463
|
+
loVal = x;
|
|
464
|
+
if (x > hiVal)
|
|
465
|
+
hiVal = x;
|
|
466
|
+
}
|
|
467
|
+
lastVal = x;
|
|
468
|
+
}
|
|
469
|
+
if (loVal === undefined) {
|
|
470
|
+
lo[b] = NaN;
|
|
471
|
+
hi[b] = NaN;
|
|
472
|
+
first[b] = NaN;
|
|
473
|
+
last[b] = NaN;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
lo[b] = loVal;
|
|
477
|
+
hi[b] = hiVal;
|
|
478
|
+
first[b] = firstVal;
|
|
479
|
+
last[b] = lastVal;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return { lo, hi, first, last };
|
|
483
|
+
}
|
|
484
|
+
if (validity === undefined) {
|
|
485
|
+
for (let b = 0; b < W; b += 1) {
|
|
486
|
+
const start = bounds[b];
|
|
487
|
+
const end = bounds[b + 1];
|
|
488
|
+
if (end <= start) {
|
|
489
|
+
lo[b] = NaN;
|
|
490
|
+
hi[b] = NaN;
|
|
491
|
+
first[b] = NaN;
|
|
492
|
+
last[b] = NaN;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
let loVal = values[start];
|
|
496
|
+
let hiVal = loVal;
|
|
497
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
498
|
+
const x = values[i];
|
|
499
|
+
loVal = loVal <= x ? loVal : x;
|
|
500
|
+
hiVal = hiVal >= x ? hiVal : x;
|
|
501
|
+
}
|
|
502
|
+
lo[b] = loVal;
|
|
503
|
+
hi[b] = hiVal;
|
|
504
|
+
first[b] = values[start];
|
|
505
|
+
last[b] = values[end - 1];
|
|
506
|
+
}
|
|
507
|
+
return { lo, hi, first, last };
|
|
508
|
+
}
|
|
509
|
+
for (let b = 0; b < W; b += 1) {
|
|
510
|
+
const start = bounds[b];
|
|
511
|
+
const end = bounds[b + 1];
|
|
512
|
+
if (end <= start) {
|
|
513
|
+
lo[b] = NaN;
|
|
514
|
+
hi[b] = NaN;
|
|
515
|
+
first[b] = NaN;
|
|
516
|
+
last[b] = NaN;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
let i = start;
|
|
520
|
+
while (i < end && !validity.isDefined(i))
|
|
521
|
+
i += 1;
|
|
522
|
+
if (i >= end) {
|
|
523
|
+
lo[b] = NaN;
|
|
524
|
+
hi[b] = NaN;
|
|
525
|
+
first[b] = NaN;
|
|
526
|
+
last[b] = NaN;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const firstVal = values[i];
|
|
530
|
+
let loVal = firstVal;
|
|
531
|
+
let hiVal = firstVal;
|
|
532
|
+
let lastVal = firstVal;
|
|
533
|
+
for (i += 1; i < end; i += 1) {
|
|
534
|
+
if (!validity.isDefined(i))
|
|
535
|
+
continue;
|
|
536
|
+
const x = values[i];
|
|
537
|
+
loVal = loVal <= x ? loVal : x;
|
|
538
|
+
hiVal = hiVal >= x ? hiVal : x;
|
|
539
|
+
lastVal = x;
|
|
540
|
+
}
|
|
541
|
+
lo[b] = loVal;
|
|
542
|
+
hi[b] = hiVal;
|
|
543
|
+
first[b] = firstVal;
|
|
544
|
+
last[b] = lastVal;
|
|
545
|
+
}
|
|
546
|
+
return { lo, hi, first, last };
|
|
547
|
+
}
|
|
359
548
|
// ─── Scalar reducers ──────────────────────────────────────────
|
|
360
549
|
//
|
|
361
|
-
// Dispatch to the registered reducer's reduceColumn fast path
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// ('p95', 'p99.9', etc.) routes through resolveReducer's
|
|
365
|
-
// parsePercentile.
|
|
550
|
+
// Dispatch to the registered reducer's reduceColumn fast path once
|
|
551
|
+
// per bucket. 'mean' → 'avg' mirrors the Float64Column.mean() shim;
|
|
552
|
+
// percentile-via-string ('p95', …) routes through resolveReducer.
|
|
366
553
|
const internalName = reducer === 'mean' ? 'avg' : reducer;
|
|
367
554
|
let reducerDef;
|
|
368
555
|
try {
|
|
369
556
|
reducerDef = resolveReducer(internalName);
|
|
370
557
|
}
|
|
371
558
|
catch {
|
|
372
|
-
throw new TypeError(`Float64Column
|
|
559
|
+
throw new TypeError(`Float64Column.${method}: unknown reducer '${reducer}'`);
|
|
373
560
|
}
|
|
374
561
|
if (reducerDef.reduceColumn === undefined) {
|
|
375
|
-
throw new TypeError(`Float64Column
|
|
562
|
+
throw new TypeError(`Float64Column.${method}: reducer '${reducer}' has no reduceColumn fast path`);
|
|
376
563
|
}
|
|
377
|
-
const out = new Float64Array(
|
|
378
|
-
for (let b = 0; b <
|
|
379
|
-
const start =
|
|
380
|
-
const end =
|
|
564
|
+
const out = new Float64Array(W);
|
|
565
|
+
for (let b = 0; b < W; b += 1) {
|
|
566
|
+
const start = bounds[b];
|
|
567
|
+
const end = bounds[b + 1];
|
|
381
568
|
if (end <= start) {
|
|
382
|
-
//
|
|
383
|
-
// NaN for reducers whose empty is undefined.
|
|
569
|
+
// Mathematical-empty for sum / count (0); NaN otherwise.
|
|
384
570
|
out[b] = reducer === 'sum' || reducer === 'count' ? 0 : NaN;
|
|
385
571
|
continue;
|
|
386
572
|
}
|
|
387
|
-
const result = reducerDef.reduceColumn(
|
|
573
|
+
const result = reducerDef.reduceColumn(col.sliceByRange(start, end));
|
|
388
574
|
out[b] = typeof result === 'number' ? result : NaN;
|
|
389
575
|
}
|
|
390
576
|
return out;
|
|
391
|
-
}
|
|
577
|
+
}
|
|
392
578
|
// ─── BooleanColumn runtime implementations ───────────────────────
|
|
393
579
|
BooleanColumn.prototype.at = function (i) {
|
|
394
580
|
return this.read(i);
|
|
@@ -691,6 +877,12 @@ ChunkedFloat64Column.prototype.bin = function (bins, reducer) {
|
|
|
691
877
|
// equal-sized chunks at a chart-friendly bin count).
|
|
692
878
|
return materializeChunkedFloat64(this).bin(bins, reducer);
|
|
693
879
|
};
|
|
880
|
+
ChunkedFloat64Column.prototype.binBy = function (key, edges, reducer) {
|
|
881
|
+
// v1: materialize then delegate, mirroring `bin`. The key array is
|
|
882
|
+
// parallel to the logical row order, so it needs no per-chunk
|
|
883
|
+
// remap — materialize preserves row order.
|
|
884
|
+
return materializeChunkedFloat64(this).binBy(key, edges, reducer);
|
|
885
|
+
};
|
|
694
886
|
// ─── ChunkedBooleanColumn runtime implementations ────────────────
|
|
695
887
|
ChunkedBooleanColumn.prototype.at = function (i) {
|
|
696
888
|
return this.read(i);
|