pond-ts 0.50.0 → 0.52.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 +187 -1
- package/dist/batch/aggregate-columns.d.ts +30 -1
- package/dist/batch/aggregate-columns.js +136 -1
- package/dist/batch/operators/from-arrow.d.ts +101 -0
- package/dist/batch/operators/from-arrow.js +297 -0
- package/dist/batch/operators/ingest-columns.d.ts +15 -8
- package/dist/batch/operators/ingest-columns.js +51 -16
- package/dist/batch/time-series.d.ts +43 -4
- package/dist/batch/time-series.js +134 -10
- package/dist/batch/value-series.d.ts +3 -3
- package/dist/batch/value-series.js +2 -2
- package/dist/index.d.ts +1 -0
- package/package.json +2 -1
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.
|
|
11
|
+
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.52.0...HEAD
|
|
12
|
+
[0.52.0]: https://github.com/pond-ts/pond/compare/v0.51.0...v0.52.0
|
|
13
|
+
[0.51.0]: https://github.com/pond-ts/pond/compare/v0.50.0...v0.51.0
|
|
12
14
|
[0.50.0]: https://github.com/pond-ts/pond/compare/v0.49.0...v0.50.0
|
|
13
15
|
[0.49.0]: https://github.com/pond-ts/pond/compare/v0.48.1...v0.49.0
|
|
14
16
|
[0.48.1]: https://github.com/pond-ts/pond/compare/v0.48.0...v0.48.1
|
|
@@ -50,6 +52,190 @@ and type-level changes; patch bumps are strictly additive.
|
|
|
50
52
|
|
|
51
53
|
## [Unreleased]
|
|
52
54
|
|
|
55
|
+
## [0.52.0] — 2026-07-23
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- **core / financial:** **Market-scale studies are now typed-array fast**
|
|
60
|
+
(the "SMA/EMA at 1M bars costs hundreds of ms" report). Three cuts along
|
|
61
|
+
the same path, all behaviour-preserving (identical values, warm-ups,
|
|
62
|
+
missing-cell semantics, and rejection errors; every fast path falls back
|
|
63
|
+
to the original sweep when it doesn't apply):
|
|
64
|
+
- **`smooth('ema')` columnar fast path** — on a packed numeric source
|
|
65
|
+
column the EMA recurrence runs straight off the typed buffer into a
|
|
66
|
+
typed result column via trusted construction (key + untouched columns
|
|
67
|
+
pass through zero-copy), replacing the per-row Event/tuple rebuild +
|
|
68
|
+
full-series intake re-pack. 1M rows: **530 ms → 4.4 ms (~120×)**.
|
|
69
|
+
- **`rolling({ count })` numeric fast path** — an all-built-in numeric
|
|
70
|
+
mapping over packed sources feeds the shared incremental reducer states
|
|
71
|
+
directly from the typed buffers and writes snapshots into typed columns
|
|
72
|
+
(no per-row snapshot arrays, no boxed accumulators, no post-pass
|
|
73
|
+
assert/re-pack). 1M rows, `avg`: **135 ms → 32 ms (~4×)**.
|
|
74
|
+
- **financial kernel reads columns, not events** — `rollingColumns` /
|
|
75
|
+
`columnValues` now read study inputs/outputs off the public column API
|
|
76
|
+
instead of materializing `series.events` (an Event + data object per
|
|
77
|
+
row, ~400 ms of pure overhead at 1M rows).
|
|
78
|
+
- End-to-end at 1M bars: `ema()` **603 ms → 2.5 ms (~240×)**, `sma()`
|
|
79
|
+
**569 ms → 56 ms (~10×)**, `bollinger()` **748 ms → 162 ms (~4.6×)**.
|
|
80
|
+
Durable benchmarks: `packages/core/scripts/perf-smooth-ema.mjs`,
|
|
81
|
+
`packages/financial/scripts/perf-studies.mjs`.
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- **core:** **`TimeSeries.fromArrow(table, options?)` — ingest a decoded Apache
|
|
86
|
+
Arrow `Table`.** pond stays zero-dependency: bring your own Arrow
|
|
87
|
+
(`tableFromIPC(...)`) and hand the `Table` in; the input is duck-typed against
|
|
88
|
+
a small structural surface (`ArrowTableLike` / `ArrowVectorLike` / …, all
|
|
89
|
+
exported). Ingest is the zero-copy path — every `Float64` column's backing
|
|
90
|
+
`Float64Array` is adopted as-is (`Float32`/int columns convert; int64 value
|
|
91
|
+
columns recombine BigInt-free), and the schema is derived from the Arrow
|
|
92
|
+
fields. The time key is converted **BigInt-free**: Arrow's idiomatic int64
|
|
93
|
+
timestamps are recombined from their two int32 halves rather than
|
|
94
|
+
`Number(bigint)` per row — measured **~11× faster** on the time column (0.6ms
|
|
95
|
+
vs 6.8ms at 500k rows; `scripts/perf-from-arrow.mjs`). Options: `time` (key
|
|
96
|
+
column, default the `'time'` field), `timeUnit` (default read from the Arrow
|
|
97
|
+
Arrow type family — a `Timestamp`'s raw-unit int64 is scaled by its
|
|
98
|
+
`TimeUnit`; `Date32`/`Date64` arrive already normalized to epoch-ms and pass
|
|
99
|
+
through; overridable), `columns` (subset, in order), `name`, `sort`. Numeric
|
|
100
|
+
**and string** columns are supported — string columns (Arrow
|
|
101
|
+
`Utf8`) become dict-encoded `StringColumn`s; any other Arrow type
|
|
102
|
+
(list/struct) throws, naming it. A null time key throws; numeric nulls map to
|
|
103
|
+
`NaN` and string nulls to missing.
|
|
104
|
+
- **core:** **`TimeSeries.fromColumns` / `ValueSeries.fromColumns` now accept
|
|
105
|
+
`string` value columns** (previously numeric-only), packed to dict-encoded
|
|
106
|
+
`StringColumn`s (`null`/`undefined` → missing) — the shared columnar-ingress
|
|
107
|
+
engine now dispatches on the schema kind. Other value kinds (`boolean`,
|
|
108
|
+
arrays) still throw.
|
|
109
|
+
- **charts:** **`<ScatterChart decimate>` — dense scatter plots now decimate**
|
|
110
|
+
(PND-MARKDEC scatter half — the last un-decimated mark type). **Default
|
|
111
|
+
`true`.** When the marks are **uniform** (fixed size + colour, no data-driven
|
|
112
|
+
`radius`/`color`), **opaque**, and denser than the pixel grid, overlapping
|
|
113
|
+
marks collapse to one representative per **mark-radius cell** via a 2D
|
|
114
|
+
pixel-**occupancy** sweep. Scatter has no fill, so a line/bar's per-column
|
|
115
|
+
`[min, max]` envelope would erase interior points — the occupancy grid keeps
|
|
116
|
+
one mark per occupied cell instead, which is **visually lossless** for uniform
|
|
117
|
+
opaque marks at that density (same-cell marks overlap). Interaction (hover /
|
|
118
|
+
click / tracker) still reads **every source point**; the per-point selection
|
|
119
|
+
ring + labels are suppressed only on the decimated (dense) path. A
|
|
120
|
+
**translucent** fill (density-encoded — overlap _should_ build up) or a
|
|
121
|
+
data-driven size/colour keeps the full draw. `decimate={false}` draws every
|
|
122
|
+
mark; `{ threshold }` tunes the trigger. The occupancy sweep uses the affine
|
|
123
|
+
fast path for the per-point pixel mapping. Measured (SciChart-suite
|
|
124
|
+
point-update, real browser): **100k 18 → 73 fps (4×)**, and the ladder now
|
|
125
|
+
runs to **10M** points (previously dead by 1M). `drawScatter` now returns
|
|
126
|
+
`LayerDrawStats` (visible via `onDrawStats`).
|
|
127
|
+
|
|
128
|
+
## [0.51.0] — 2026-07-22
|
|
129
|
+
|
|
130
|
+
### Changed
|
|
131
|
+
|
|
132
|
+
- **charts:** **`trackerPosition` is now a _followed_ position, not a hard pin —
|
|
133
|
+
enabling cross-chart cursor sync.** A live local hover wins over
|
|
134
|
+
`trackerPosition`, so the chart under the pointer shows its own cursor while
|
|
135
|
+
any chart without a local pointer follows the controlled time (mapped through
|
|
136
|
+
its own `xScale`, so it's correct across different zooms). This makes
|
|
137
|
+
**multi-chart dashboard cursor sync** fall out of the plain props: give every
|
|
138
|
+
`<ChartContainer>` the same `trackerPosition={sharedTime}` and set `sharedTime`
|
|
139
|
+
from each one's `onTrackerChanged` (clear it to `null` on the group's
|
|
140
|
+
`onPointerLeave`) — no "which chart is active" bookkeeping. **Behaviour
|
|
141
|
+
change:** previously a numeric `trackerPosition` overrode local hover, and
|
|
142
|
+
`trackerPosition={null}` force-hid the cursor; now `null` and `undefined` are
|
|
143
|
+
equivalent ("no controlled position") and a hovered chart always tracks its
|
|
144
|
+
pointer. To force a chart to never show a cursor, use `cursor="none"`. See the
|
|
145
|
+
"Synced cursors across charts" story. No type change (`number | null`).
|
|
146
|
+
- **charts:** **Line / area draw is ~3× faster on stroke-bound frames**
|
|
147
|
+
(PND-AFFINE / PND-GRADX; 2026-07 external-bench profile). When the curve is
|
|
148
|
+
linear and both scales are affine (every y axis is `scaleLinear`; x is
|
|
149
|
+
`scaleLinear` / `scaleTime` / the gap-free default time axis), `drawLine` and
|
|
150
|
+
`drawArea` now map points with an inline `k·v + b` over the typed arrays
|
|
151
|
+
instead of a per-point d3-scale closure + d3-shape generator — a **visually
|
|
152
|
+
identical** draw (guarded by the decimation pixel-identity and per-layer
|
|
153
|
+
visual-regression e2e). A real-gap trading-time axis, or a non-linear curve,
|
|
154
|
+
transparently keeps the exact d3 path. Measured on a JS-only micro-bench
|
|
155
|
+
(`scripts/perf-affine.mjs`): line 1M 60.4 → 19.0 ms (3.2×), area 1M 132 →
|
|
156
|
+
38 ms (3.5×). Separately, the area fill gradient's full-series value extent is
|
|
157
|
+
now memoized per column buffer, so a y-zoom / pan repaint no longer re-walks
|
|
158
|
+
the whole series to find the gradient span. No API change.
|
|
159
|
+
- **charts:** **A y-zoom / y-autorange repaint no longer re-decimates line /
|
|
160
|
+
area layers** (PND-DECKEY; same 2026-07 profile, finding 3). The M4
|
|
161
|
+
decimation output is a pure function of the source data, x-domain, device
|
|
162
|
+
width, threshold, and session breaks — it never reads the y-scale — so it is
|
|
163
|
+
identical across every y-only frame. `drawLine` / `drawArea` now memoize the
|
|
164
|
+
cull+decimate result per source series (one entry, keyed on the x-scale
|
|
165
|
+
object + width + threshold + breaks), so a y-zoom / live y-autorange frame
|
|
166
|
+
reuses the prior polyline instead of re-binning O(N) points; a pan / x-zoom
|
|
167
|
+
mints a fresh x-scale and correctly recomputes. Measured
|
|
168
|
+
(`scripts/perf-deckey.mjs`): the ~5 ms/frame decimation walk at 1M points is
|
|
169
|
+
eliminated on every y-only frame. No API change.
|
|
170
|
+
|
|
171
|
+
### Added
|
|
172
|
+
|
|
173
|
+
- **charts:** **`<BarChart decimate>` — dense column charts now decimate**
|
|
174
|
+
(PND-MARKDEC; 2026-07 profile, finding 4 — "column dead by 5M"). **Default
|
|
175
|
+
`true`**: once the visible **single-series** bars are denser than ~2 per device
|
|
176
|
+
pixel (each slot < ~1px), they're drawn as one per-column **envelope** rect —
|
|
177
|
+
the exact painted union `[min(value, baseline), max(value, baseline)]` of each
|
|
178
|
+
pixel column — instead of every bar, so a 100k–5M-bar column chart stays
|
|
179
|
+
interactive. **Visually lossless** at that density (a perf knob, not a style);
|
|
180
|
+
interaction still reads the source bars (`barAt`), and the per-bar
|
|
181
|
+
selection/hover highlight is suppressed only when decimated (a <1px bar's ring
|
|
182
|
+
isn't visible anyway). `decimate={false}` draws every bar; `{ threshold }`
|
|
183
|
+
tunes the samples-per-pixel factor. No-op for a stacked / multi-group
|
|
184
|
+
histogram (the low-count categorical path). `drawBars` now returns
|
|
185
|
+
`LayerDrawStats` (visible via `onDrawStats`). Measured
|
|
186
|
+
(`scripts/perf-markdec.mjs`, JS-only): the bar draw at 5M points drops
|
|
187
|
+
485 → 26 ms (18.9×), 100k drops 9.7 → 0.9 ms (10.7×) — with the larger
|
|
188
|
+
rasterization win on top, browser-side.
|
|
189
|
+
- **charts:** **`panZoom` is now a three-way mode + a `bounds` extent.**
|
|
190
|
+
`<ChartContainer panZoom>` takes `'none'` / `'pan'` / `'panZoom'` (drag-only
|
|
191
|
+
vs. drag+wheel), with the old boolean kept as shorthand (`true` ⇒ `'panZoom'`,
|
|
192
|
+
`false` ⇒ `'none'`) — so existing charts are unchanged. A new `bounds`
|
|
193
|
+
(`[min, max]`) prop fences pan/zoom to an **outer** extent (panning into an
|
|
194
|
+
edge stops there keeping its span; zoom-out is capped at the whole span), the
|
|
195
|
+
companion
|
|
196
|
+
to the existing `minDuration` zoom-in floor — together they pin the reachable
|
|
197
|
+
window between an inner and outer bound. On a trading-time axis `bounds`
|
|
198
|
+
clamps in wall-clock ms. Purely additive; no type narrowing.
|
|
199
|
+
- **charts:** **Draw-cost + decimation observability** — `<ChartContainer
|
|
200
|
+
onDrawStats>` (PND-DECOBS; dashboard A/B friction, 2026-07-21). Fires a
|
|
201
|
+
`DrawStatsFrame` once per row-canvas repaint (keyed by an opaque `rowKey` for
|
|
202
|
+
multi-row attribution), one `LayerDrawInfo` per layer carrying its `as`,
|
|
203
|
+
measured `drawMs`, and — for a decimating layer (line / area / band / candle /
|
|
204
|
+
box) — `sourceCount` / `drawnCount` / `decimated`.
|
|
205
|
+
Compare `drawnCount` to `sourceCount` to see whether M4 engaged; read `drawMs`
|
|
206
|
+
for per-layer render cost. **Zero-overhead when unused** — the render loop
|
|
207
|
+
skips per-layer timing entirely unless a consumer subscribes. New exports:
|
|
208
|
+
`DrawStatsFrame`, `LayerDrawInfo`.
|
|
209
|
+
|
|
210
|
+
### Fixed
|
|
211
|
+
|
|
212
|
+
- **charts:** hovering no longer repaints the row data canvas on every cursor
|
|
213
|
+
mousemove. The container frame minted a fresh `timeRange` array identity per
|
|
214
|
+
rebuild (and the frame rebuilds per cursor move), which the Layers draw
|
|
215
|
+
callback — depending on `container.timeRange` — read as a domain change:
|
|
216
|
+
each hover frame re-fired the canvas draw effect, including per-layer M4
|
|
217
|
+
re-decimation (measured 105 repaints per 122 mousemove events; with
|
|
218
|
+
`decimate` off, hover fell to ~10 fps). The tuple is now identity-stable on
|
|
219
|
+
its endpoints, restoring the SVG-overlay cursor contract (0 repaints, full
|
|
220
|
+
frame rate while hovering). Found running uPlot's bench protocol against
|
|
221
|
+
pond-charts; guarded by a new hover-sweep perf invariant in
|
|
222
|
+
`e2e/perf-invariants.spec.ts`.
|
|
223
|
+
- **charts:** hovering no longer re-renders cursor-independent components
|
|
224
|
+
(both `YAxis`, `Bar`/`Box`). The cursor position was a `ContainerFrame`
|
|
225
|
+
field, so every mousemove re-identified the whole (~50-field) frame and
|
|
226
|
+
re-rendered **all** its context consumers — even ones that never read the
|
|
227
|
+
cursor. The per-move cursor state (`cursorX`/`cursorY`/`cursorRowKey`) now
|
|
228
|
+
lives in a dedicated `CursorContext`; the frame stays identity-stable across
|
|
229
|
+
a hover, so only the genuine cursor consumers (the `Layers` overlay,
|
|
230
|
+
`XAxis` crosshair pill, `Legend` values) re-render. Measured: 4 → 2 React
|
|
231
|
+
commits per mousemove, ~25% less hover script time on the uPlot-bench
|
|
232
|
+
workload (the win scales with axis/row count). No API change — the split
|
|
233
|
+
types are internal (`PND-HOVCTX`, follow-up to the repaint fix above).
|
|
234
|
+
- **charts:** corrected the `@pond-ts/charts` package-header doc comment, which
|
|
235
|
+
described a "chunked Path2D cache" render stage that was explored and
|
|
236
|
+
**deferred**, never built (it doesn't help the pan case, which re-decimates
|
|
237
|
+
every frame). The stale comment had misled a consumer's perf investigation.
|
|
238
|
+
|
|
53
239
|
## [0.50.0] — 2026-07-21
|
|
54
240
|
|
|
55
241
|
### Added
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Column } from '../columnar/index.js';
|
|
2
|
-
import
|
|
2
|
+
import { Float64Column } from '../columnar/index.js';
|
|
3
|
+
import type { AggregateMap, AggregateOutputMap, AggregateReducer, RollingAlignment, ScalarKind, SeriesSchema } from '../schema/index.js';
|
|
3
4
|
/**
|
|
4
5
|
* Normalised column spec used by both batch and live aggregation paths.
|
|
5
6
|
*
|
|
@@ -38,4 +39,32 @@ export declare function tryAggregateColumnarTimeKeyed<B extends {
|
|
|
38
39
|
begin(): number;
|
|
39
40
|
end(): number;
|
|
40
41
|
}>(begins: Float64Array, getColumn: (name: string) => Column | undefined, buckets: ReadonlyArray<B>, columns: ReadonlyArray<AggregateColumnSpec>): Array<ReadonlyArray<unknown>> | null;
|
|
42
|
+
/**
|
|
43
|
+
* **Columnar fast path for count-window `rolling()`** — the N-bar window
|
|
44
|
+
* financial studies compose on (SMA / Bollinger / rolling stats). When every
|
|
45
|
+
* mapped column is a built-in reducer producing a `'number'` output from a
|
|
46
|
+
* **packed `Float64Column`** source, the window sweep feeds the shared
|
|
47
|
+
* incremental rolling states (`rollingStateFor` — the same add / remove /
|
|
48
|
+
* snapshot arithmetic and non-finite skip policy as the generic path) values
|
|
49
|
+
* read straight off the typed buffers, and writes each snapshot into a typed
|
|
50
|
+
* result column. That removes the generic sweep's per-row costs: the
|
|
51
|
+
* `snapshotWindow` result-array allocation, the boxed accumulator rows, the
|
|
52
|
+
* polymorphic `col.read(i)` per add/remove, and the post-pass
|
|
53
|
+
* `assertColumnValuesMatchKind` + re-pack over boxed values (each written
|
|
54
|
+
* value is finite-asserted inline instead, same rejection class + message).
|
|
55
|
+
*
|
|
56
|
+
* Returns `null` — caller takes the generic sweep — when any column doesn't
|
|
57
|
+
* qualify: a custom-function reducer, a non-`'number'` output kind
|
|
58
|
+
* (`unique` / `samples` / `keep` over a non-number source / an explicit
|
|
59
|
+
* `kind` override), or a non-numeric / chunked / missing source column.
|
|
60
|
+
* All-or-nothing per call, matching {@link tryAggregateColumnarTimeKeyed}.
|
|
61
|
+
*
|
|
62
|
+
* Window shape replicates the generic count sweep exactly: rows are the unit
|
|
63
|
+
* (`count` is a bar count — no equal-key grouping), `lo` / `hi` are monotonic
|
|
64
|
+
* in the row index for every alignment (each row enters and leaves the window
|
|
65
|
+
* once, amortized O(1)), a centered even `count` biases one row toward the
|
|
66
|
+
* leading side, and a row whose window holds fewer than `minSamples` rows
|
|
67
|
+
* emits missing cells without consulting the reducer states.
|
|
68
|
+
*/
|
|
69
|
+
export declare function tryRollingCountColumnarNumeric(getColumn: (name: string) => Column | undefined, rowCount: number, columns: ReadonlyArray<AggregateColumnSpec>, count: number, alignment: RollingAlignment, minSamples: number): Float64Column[] | null;
|
|
41
70
|
//# sourceMappingURL=aggregate-columns.d.ts.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { resolveReducer } from '../reducers/index.js';
|
|
1
|
+
import { resolveReducer, rollingStateFor } from '../reducers/index.js';
|
|
2
|
+
import { Float64Column, bitmapByteCount, validityFromBits, } from '../columnar/index.js';
|
|
3
|
+
import { ValidationError } from '../core/errors.js';
|
|
2
4
|
/**
|
|
3
5
|
* @internal — discriminator between an `AggregateOutputSpec` (`{ from,
|
|
4
6
|
* using, kind? }`) and a bare reducer string/function passed in an
|
|
@@ -159,4 +161,137 @@ export function tryAggregateColumnarTimeKeyed(begins, getColumn, buckets, column
|
|
|
159
161
|
}
|
|
160
162
|
return rows;
|
|
161
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* **Columnar fast path for count-window `rolling()`** — the N-bar window
|
|
166
|
+
* financial studies compose on (SMA / Bollinger / rolling stats). When every
|
|
167
|
+
* mapped column is a built-in reducer producing a `'number'` output from a
|
|
168
|
+
* **packed `Float64Column`** source, the window sweep feeds the shared
|
|
169
|
+
* incremental rolling states (`rollingStateFor` — the same add / remove /
|
|
170
|
+
* snapshot arithmetic and non-finite skip policy as the generic path) values
|
|
171
|
+
* read straight off the typed buffers, and writes each snapshot into a typed
|
|
172
|
+
* result column. That removes the generic sweep's per-row costs: the
|
|
173
|
+
* `snapshotWindow` result-array allocation, the boxed accumulator rows, the
|
|
174
|
+
* polymorphic `col.read(i)` per add/remove, and the post-pass
|
|
175
|
+
* `assertColumnValuesMatchKind` + re-pack over boxed values (each written
|
|
176
|
+
* value is finite-asserted inline instead, same rejection class + message).
|
|
177
|
+
*
|
|
178
|
+
* Returns `null` — caller takes the generic sweep — when any column doesn't
|
|
179
|
+
* qualify: a custom-function reducer, a non-`'number'` output kind
|
|
180
|
+
* (`unique` / `samples` / `keep` over a non-number source / an explicit
|
|
181
|
+
* `kind` override), or a non-numeric / chunked / missing source column.
|
|
182
|
+
* All-or-nothing per call, matching {@link tryAggregateColumnarTimeKeyed}.
|
|
183
|
+
*
|
|
184
|
+
* Window shape replicates the generic count sweep exactly: rows are the unit
|
|
185
|
+
* (`count` is a bar count — no equal-key grouping), `lo` / `hi` are monotonic
|
|
186
|
+
* in the row index for every alignment (each row enters and leaves the window
|
|
187
|
+
* once, amortized O(1)), a centered even `count` biases one row toward the
|
|
188
|
+
* leading side, and a row whose window holds fewer than `minSamples` rows
|
|
189
|
+
* emits missing cells without consulting the reducer states.
|
|
190
|
+
*/
|
|
191
|
+
export function tryRollingCountColumnarNumeric(getColumn, rowCount, columns, count, alignment, minSamples) {
|
|
192
|
+
const specCount = columns.length;
|
|
193
|
+
const sources = [];
|
|
194
|
+
for (const spec of columns) {
|
|
195
|
+
if (typeof spec.reducer !== 'string')
|
|
196
|
+
return null; // custom function
|
|
197
|
+
if (spec.kind !== 'number')
|
|
198
|
+
return null; // non-numeric output kind
|
|
199
|
+
const source = getColumn(spec.source);
|
|
200
|
+
if (source === undefined)
|
|
201
|
+
return null; // missing source
|
|
202
|
+
if (source.kind !== 'number' || source.storage !== 'packed') {
|
|
203
|
+
return null; // non-numeric / chunked numeric source
|
|
204
|
+
}
|
|
205
|
+
sources.push(source);
|
|
206
|
+
}
|
|
207
|
+
// An all-finite fully-defined source feeds its reducer state the raw
|
|
208
|
+
// buffer values, so the `rollingStateFor` non-finite skip wrapper is
|
|
209
|
+
// provably an identity there — resolve the bare built-in state instead
|
|
210
|
+
// and drop one call layer from every add / remove / snapshot. Any other
|
|
211
|
+
// source keeps the wrapped state (same values, same skip policy).
|
|
212
|
+
const states = columns.map((spec, c) => {
|
|
213
|
+
const source = sources[c];
|
|
214
|
+
return source.allFinite && source.validity === undefined
|
|
215
|
+
? resolveReducer(spec.reducer).rollingState()
|
|
216
|
+
: rollingStateFor(spec.reducer);
|
|
217
|
+
});
|
|
218
|
+
const srcValues = sources.map((col) => col._values);
|
|
219
|
+
const srcValidity = sources.map((col) => col.validity);
|
|
220
|
+
const outValues = columns.map(() => new Float64Array(rowCount));
|
|
221
|
+
const outBits = columns.map(() => new Uint8Array(bitmapByteCount(rowCount)));
|
|
222
|
+
const outDefined = new Array(specCount).fill(0);
|
|
223
|
+
// Feed the states exactly what the generic sweep's `col.read(index)`
|
|
224
|
+
// yields: `undefined` for a missing cell, the raw (possibly non-finite)
|
|
225
|
+
// number otherwise — the non-finite skip stays inside the shared state
|
|
226
|
+
// wrapper so the contributor set can't drift between the two paths.
|
|
227
|
+
const addAt = (index) => {
|
|
228
|
+
for (let c = 0; c < specCount; c += 1) {
|
|
229
|
+
const validity = srcValidity[c];
|
|
230
|
+
const value = validity === undefined || validity.isDefined(index)
|
|
231
|
+
? srcValues[c][index]
|
|
232
|
+
: undefined;
|
|
233
|
+
states[c].add(index, value);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const removeAt = (index) => {
|
|
237
|
+
for (let c = 0; c < specCount; c += 1) {
|
|
238
|
+
const validity = srcValidity[c];
|
|
239
|
+
const value = validity === undefined || validity.isDefined(index)
|
|
240
|
+
? srcValues[c][index]
|
|
241
|
+
: undefined;
|
|
242
|
+
states[c].remove(index, value);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const leftSpan = Math.floor((count - 1) / 2);
|
|
246
|
+
const rightSpan = count - 1 - leftSpan;
|
|
247
|
+
let windowStart = 0;
|
|
248
|
+
let windowEnd = 0;
|
|
249
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
250
|
+
let lo;
|
|
251
|
+
let hi;
|
|
252
|
+
if (alignment === 'trailing') {
|
|
253
|
+
lo = index - count + 1 < 0 ? 0 : index - count + 1;
|
|
254
|
+
hi = index;
|
|
255
|
+
}
|
|
256
|
+
else if (alignment === 'leading') {
|
|
257
|
+
lo = index;
|
|
258
|
+
hi = index + count - 1 < rowCount ? index + count - 1 : rowCount - 1;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
lo = index - leftSpan < 0 ? 0 : index - leftSpan;
|
|
262
|
+
hi = index + rightSpan < rowCount ? index + rightSpan : rowCount - 1;
|
|
263
|
+
}
|
|
264
|
+
while (windowEnd <= hi) {
|
|
265
|
+
addAt(windowEnd);
|
|
266
|
+
windowEnd += 1;
|
|
267
|
+
}
|
|
268
|
+
while (windowStart < lo) {
|
|
269
|
+
removeAt(windowStart);
|
|
270
|
+
windowStart += 1;
|
|
271
|
+
}
|
|
272
|
+
if (windowEnd - windowStart < minSamples)
|
|
273
|
+
continue; // missing row
|
|
274
|
+
for (let c = 0; c < specCount; c += 1) {
|
|
275
|
+
const v = states[c].snapshot();
|
|
276
|
+
if (v === undefined)
|
|
277
|
+
continue; // missing cell (e.g. all-missing window)
|
|
278
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
279
|
+
// Same rejection class + message as the generic path's post-pass
|
|
280
|
+
// `assertColumnValuesMatchKind` (e.g. a `sum` overflow to Infinity)
|
|
281
|
+
// — checked at write time since there is no post-pass here.
|
|
282
|
+
throw new ValidationError(`rolling column '${columns[c].output}': result ${String(v)} is not a valid 'number' value`);
|
|
283
|
+
}
|
|
284
|
+
outValues[c][index] = v;
|
|
285
|
+
outBits[c][index >> 3] |= 1 << (index & 7);
|
|
286
|
+
outDefined[c] = outDefined[c] + 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return columns.map((_, c) => {
|
|
290
|
+
const validity = outDefined[c] === rowCount
|
|
291
|
+
? undefined
|
|
292
|
+
: validityFromBits(outBits[c], rowCount);
|
|
293
|
+
// Every written cell was finite-asserted above → `allFinite`.
|
|
294
|
+
return new Float64Column(outValues[c], rowCount, validity, true);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
162
297
|
//# sourceMappingURL=aggregate-columns.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { SeriesSchema } from '../../schema/index.js';
|
|
2
|
+
import type { RawColumns } from './ingest-columns.js';
|
|
3
|
+
/**
|
|
4
|
+
* Structural view of an Apache Arrow `Vector`. pond does **not** depend on
|
|
5
|
+
* `apache-arrow` — the caller brings their own Arrow (`tableFromIPC(...)`) and
|
|
6
|
+
* hands the decoded `Table` to {@link TimeSeries.fromArrow}. We duck-type the
|
|
7
|
+
* small slice of the vector surface we read: the length, the null count, and
|
|
8
|
+
* `toArray()` (which for a contiguous numeric column returns the backing typed
|
|
9
|
+
* array — the zero-copy handoff pond adopts).
|
|
10
|
+
*/
|
|
11
|
+
export interface ArrowVectorLike {
|
|
12
|
+
readonly length: number;
|
|
13
|
+
/** Number of null slots; `0` unlocks the fast (validity-free) path. */
|
|
14
|
+
readonly nullCount: number;
|
|
15
|
+
/**
|
|
16
|
+
* Backing values. For a **single-chunk** numeric column this is the typed
|
|
17
|
+
* array itself (`Float64Array`, `Int32Array`, `BigInt64Array`, …), adopted
|
|
18
|
+
* where possible; for a string column (Arrow `Utf8`) it is a plain `Array` of
|
|
19
|
+
* strings. Any other shape (list/struct) is rejected by
|
|
20
|
+
* {@link TimeSeries.fromArrow}. A **multi-chunk** vector (a table decoded from
|
|
21
|
+
* a multi-record-batch IPC stream) concatenates into a fresh array here — so
|
|
22
|
+
* such tables are correct but pay a copy, and the zero-copy/aliasing note
|
|
23
|
+
* does not apply to them.
|
|
24
|
+
*/
|
|
25
|
+
toArray(): unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Per-slot access, used only on the (rare) null-containing numeric slow path.
|
|
28
|
+
* Typed to match a real Arrow `Vector.get` across column kinds (a `Utf8`
|
|
29
|
+
* vector returns `string`), so a genuine `apache-arrow` `Table` satisfies
|
|
30
|
+
* this interface structurally.
|
|
31
|
+
*/
|
|
32
|
+
get(index: number): number | bigint | string | null | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Structural view of an Arrow `Field` (`schema.fields[i]`). */
|
|
35
|
+
export interface ArrowFieldLike {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/**
|
|
38
|
+
* The field's `DataType`. We read `typeId` (the Arrow `Type` enum) to detect
|
|
39
|
+
* a `Timestamp` (= 10), whose `toArray()` is raw-unit and so needs `unit`
|
|
40
|
+
* (a `TimeUnit`) to scale; every other kind is read straight off `toArray()`.
|
|
41
|
+
* Both are optional so a bare structural stand-in — and a real `DataType`,
|
|
42
|
+
* which carries far more — satisfy the shape.
|
|
43
|
+
*/
|
|
44
|
+
readonly type: {
|
|
45
|
+
readonly typeId?: number;
|
|
46
|
+
readonly unit?: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Structural view of an Arrow `Schema`. */
|
|
50
|
+
export interface ArrowSchemaLike {
|
|
51
|
+
readonly fields: ReadonlyArray<ArrowFieldLike>;
|
|
52
|
+
}
|
|
53
|
+
/** Structural view of an Arrow `Table` — the input to {@link TimeSeries.fromArrow}. */
|
|
54
|
+
export interface ArrowTableLike {
|
|
55
|
+
readonly numRows: number;
|
|
56
|
+
readonly schema: ArrowSchemaLike;
|
|
57
|
+
getChild(name: string): ArrowVectorLike | null | undefined;
|
|
58
|
+
}
|
|
59
|
+
/** Options for {@link TimeSeries.fromArrow}. */
|
|
60
|
+
export interface FromArrowOptions {
|
|
61
|
+
/** Series name. Default `'arrow'`. */
|
|
62
|
+
name?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Which Arrow column is the time key. Default: the field named `'time'`.
|
|
65
|
+
* Throws if neither `time` is given nor a `'time'` field exists.
|
|
66
|
+
*/
|
|
67
|
+
time?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Unit of the time column's raw integer values. Default: read from the Arrow
|
|
70
|
+
* `Timestamp` field's `TimeUnit` when present, otherwise `'millisecond'`.
|
|
71
|
+
* An explicit value here always wins over the field's declared unit.
|
|
72
|
+
*/
|
|
73
|
+
timeUnit?: ArrowTimeUnit;
|
|
74
|
+
/**
|
|
75
|
+
* Value columns to include, in order. Default: every non-time field. Each
|
|
76
|
+
* must be numeric (→ `Float64Column`) or a string column (→ `StringColumn`);
|
|
77
|
+
* any other Arrow type (list/struct/…) throws, naming it. Pass this to select
|
|
78
|
+
* a supported subset out of a mixed table.
|
|
79
|
+
*/
|
|
80
|
+
columns?: readonly string[];
|
|
81
|
+
/**
|
|
82
|
+
* Sort rows by time key before construction (off by default — Arrow feeds
|
|
83
|
+
* are usually already time-ordered). Forwarded to `fromColumns`; disables
|
|
84
|
+
* zero-copy adoption when set (rows are permuted into fresh buffers).
|
|
85
|
+
*/
|
|
86
|
+
sort?: boolean;
|
|
87
|
+
}
|
|
88
|
+
export type ArrowTimeUnit = 'second' | 'millisecond' | 'microsecond' | 'nanosecond';
|
|
89
|
+
/**
|
|
90
|
+
* Translate an Arrow `Table` into the `{ name, schema, columns }` triple that
|
|
91
|
+
* `TimeSeries.fromColumns` ingests. Kept separate from the static so the
|
|
92
|
+
* conversion is unit-testable without the class and so the Arrow-shaped types
|
|
93
|
+
* live in one place. The returned columns are all `Float64Array`, so
|
|
94
|
+
* `fromColumns` takes its zero-copy adoption path throughout.
|
|
95
|
+
*/
|
|
96
|
+
export declare function arrowToColumns(table: ArrowTableLike, options?: FromArrowOptions): {
|
|
97
|
+
name: string;
|
|
98
|
+
schema: SeriesSchema;
|
|
99
|
+
columns: RawColumns;
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=from-arrow.d.ts.map
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { ValidationError } from '../../core/errors.js';
|
|
2
|
+
// Arrow's `TimeUnit` enum, keyed by its wire ordinal (unit read off a
|
|
3
|
+
// `Timestamp` DataType). SECOND=0, MILLISECOND=1, MICROSECOND=2, NANOSECOND=3.
|
|
4
|
+
const TIME_UNIT_BY_ORDINAL = {
|
|
5
|
+
0: 'second',
|
|
6
|
+
1: 'millisecond',
|
|
7
|
+
2: 'microsecond',
|
|
8
|
+
3: 'nanosecond',
|
|
9
|
+
};
|
|
10
|
+
// Multiplier taking one raw unit to milliseconds (pond's `Time` key is epoch
|
|
11
|
+
// ms). Nanoseconds divide by 1e6, so sub-ms precision below the double's
|
|
12
|
+
// integer range is discarded — expected at ms resolution.
|
|
13
|
+
const MS_SCALE = {
|
|
14
|
+
second: 1000,
|
|
15
|
+
millisecond: 1,
|
|
16
|
+
microsecond: 1 / 1000,
|
|
17
|
+
nanosecond: 1 / 1_000_000,
|
|
18
|
+
};
|
|
19
|
+
// Arrow `Type` enum ordinal for `Timestamp` (a stable wire-level contract).
|
|
20
|
+
// Only `Timestamp` needs unit scaling: its `toArray()` hands back the raw int64
|
|
21
|
+
// values in the declared `TimeUnit`. Arrow's `Date32`/`Date64` `toArray()`, by
|
|
22
|
+
// contrast, is already normalized to epoch-**ms** JS numbers by the logical
|
|
23
|
+
// vector view — so a `Date` key is scale-1, NOT days/ms-from-a-DateUnit. (An
|
|
24
|
+
// earlier version read `.unit` blind and scaled Date32 as seconds; verified
|
|
25
|
+
// against real `apache-arrow` in `from-arrow.arrow.test.ts`, that was wrong.)
|
|
26
|
+
const ARROW_TYPE_TIMESTAMP = 10;
|
|
27
|
+
const TWO_POW_32 = 4294967296;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the raw-unit → milliseconds multiplier for the time key. An explicit
|
|
30
|
+
* `timeUnit` always wins. Otherwise: a `Timestamp` (`typeId` 10, or a bare
|
|
31
|
+
* structural input carrying a `unit` but no `typeId` — overwhelmingly a
|
|
32
|
+
* timestamp) is scaled by its `TimeUnit`, since its `toArray()` is raw-unit
|
|
33
|
+
* int64. Everything else — `Date` (arrow already normalizes its `toArray()` to
|
|
34
|
+
* epoch-ms), plain int/float epoch-ms columns — is taken as milliseconds
|
|
35
|
+
* (scale 1).
|
|
36
|
+
*/
|
|
37
|
+
function resolveMsScale(field, override) {
|
|
38
|
+
if (override !== undefined)
|
|
39
|
+
return MS_SCALE[override];
|
|
40
|
+
const type = field.type ?? {};
|
|
41
|
+
const typeId = typeof type.typeId === 'number' ? type.typeId : undefined;
|
|
42
|
+
const unit = typeof type.unit === 'number' ? type.unit : undefined;
|
|
43
|
+
if (typeId === ARROW_TYPE_TIMESTAMP ||
|
|
44
|
+
(typeId === undefined && unit !== undefined)) {
|
|
45
|
+
return MS_SCALE[TIME_UNIT_BY_ORDINAL[unit ?? 1] ?? 'millisecond'];
|
|
46
|
+
}
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Numeric typed-array constructors Arrow's `toArray()` can hand back. A
|
|
51
|
+
* `BigInt64Array` / `BigUint64Array` is numeric but needs the BigInt-free
|
|
52
|
+
* recombination; the rest are plain-number typed arrays.
|
|
53
|
+
*/
|
|
54
|
+
function isBigIntArray(value) {
|
|
55
|
+
return value instanceof BigInt64Array || value instanceof BigUint64Array;
|
|
56
|
+
}
|
|
57
|
+
function isNumberTypedArray(value) {
|
|
58
|
+
return (value instanceof Float64Array ||
|
|
59
|
+
value instanceof Float32Array ||
|
|
60
|
+
value instanceof Int32Array ||
|
|
61
|
+
value instanceof Int16Array ||
|
|
62
|
+
value instanceof Int8Array ||
|
|
63
|
+
value instanceof Uint32Array ||
|
|
64
|
+
value instanceof Uint16Array ||
|
|
65
|
+
value instanceof Uint8Array ||
|
|
66
|
+
value instanceof Uint8ClampedArray);
|
|
67
|
+
}
|
|
68
|
+
// Detected once: is this host little-endian? Arrow IPC is always LE, but the
|
|
69
|
+
// buffer a BigInt64Array exposes is in *host* byte order, so a big-endian host
|
|
70
|
+
// (vanishingly rare for Node, but real) needs the two int32 halves swapped.
|
|
71
|
+
const HOST_LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
72
|
+
/**
|
|
73
|
+
* Convert an int64 column to `Float64Array` **without BigInt**. We alias the
|
|
74
|
+
* buffer as `Int32Array` and recombine each pair `hi * 2^32 + (lo >>> 0)` —
|
|
75
|
+
* exact for the ±2^53 range that covers every realistic epoch timestamp
|
|
76
|
+
* (ms ≈ 1.7e12, µs ≈ 1.7e15). This is the ~30ms `Number(bigint)` ×N cost the
|
|
77
|
+
* fromArrow note called out, reclaimed: the per-element work is two array reads
|
|
78
|
+
* and a multiply-add, no BigInt boxing.
|
|
79
|
+
*
|
|
80
|
+
* Signedness follows the source: `BigUint64Array`'s high word is read unsigned
|
|
81
|
+
* (so a `Uint64` ≥ 2^63 stays positive rather than flipping negative);
|
|
82
|
+
* `BigInt64Array`'s stays signed (so pre-epoch / negative values recombine
|
|
83
|
+
* correctly). Half order follows {@link HOST_LITTLE_ENDIAN}. Values beyond
|
|
84
|
+
* ±2^53 (a huge id/count, or a nanosecond stamp) round to the nearest double —
|
|
85
|
+
* inherent to `Float64Column` storage, same as `Number(bigint)` would give.
|
|
86
|
+
*/
|
|
87
|
+
function int64ToFloat64(source, scale) {
|
|
88
|
+
const count = source.length;
|
|
89
|
+
const out = new Float64Array(count);
|
|
90
|
+
const halves = new Int32Array(source.buffer, source.byteOffset, count * 2);
|
|
91
|
+
const unsigned = source instanceof BigUint64Array;
|
|
92
|
+
const loOff = HOST_LITTLE_ENDIAN ? 0 : 1;
|
|
93
|
+
const hiOff = HOST_LITTLE_ENDIAN ? 1 : 0;
|
|
94
|
+
if (scale === 1) {
|
|
95
|
+
for (let j = 0; j < count; j += 1) {
|
|
96
|
+
const lo = halves[j * 2 + loOff] >>> 0;
|
|
97
|
+
const hiRaw = halves[j * 2 + hiOff];
|
|
98
|
+
const hi = unsigned ? hiRaw >>> 0 : hiRaw;
|
|
99
|
+
out[j] = hi * TWO_POW_32 + lo;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
for (let j = 0; j < count; j += 1) {
|
|
104
|
+
const lo = halves[j * 2 + loOff] >>> 0;
|
|
105
|
+
const hiRaw = halves[j * 2 + hiOff];
|
|
106
|
+
const hi = unsigned ? hiRaw >>> 0 : hiRaw;
|
|
107
|
+
out[j] = (hi * TWO_POW_32 + lo) * scale;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Read a numeric column carrying nulls, mapping null → `NaN` (missing). This is
|
|
114
|
+
* the slow path: per-element `get()`, and for an int64 column also the
|
|
115
|
+
* `Number(bigint)` boxing the two-int32 recombination avoids — the BigInt-free
|
|
116
|
+
* fast path is null-free-only. Acceptable given how rare a nulled int64 column
|
|
117
|
+
* is; a dense numeric column never lands here.
|
|
118
|
+
*/
|
|
119
|
+
function numericWithNulls(vector) {
|
|
120
|
+
const count = vector.length;
|
|
121
|
+
const out = new Float64Array(count);
|
|
122
|
+
for (let j = 0; j < count; j += 1) {
|
|
123
|
+
const v = vector.get(j);
|
|
124
|
+
out[j] = v == null ? NaN : Number(v);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** Copy a string array's values, mapping null/undefined → missing. */
|
|
129
|
+
function readStrings(raw) {
|
|
130
|
+
const out = new Array(raw.length);
|
|
131
|
+
for (let j = 0; j < raw.length; j += 1) {
|
|
132
|
+
const v = raw[j];
|
|
133
|
+
out[j] = v == null ? null : v;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
/** Build a `Float64Array` from a plain number array, null/undefined → `NaN`. */
|
|
138
|
+
function readNumbersFromArray(raw) {
|
|
139
|
+
const out = new Float64Array(raw.length);
|
|
140
|
+
for (let j = 0; j < raw.length; j += 1) {
|
|
141
|
+
const v = raw[j];
|
|
142
|
+
out[j] = v == null ? NaN : Number(v);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Classify a plain `Array` (what `toArray()` returns for non-typed-array
|
|
148
|
+
* columns) by its first defined element. Arrow hands back an `Array<string>`
|
|
149
|
+
* for `Utf8`/`LargeUtf8` and an `Array<number>` (epoch-ms) for `Date32`/`Date64`
|
|
150
|
+
* — the element type is the only reliable discriminator. All-null → `'number'`
|
|
151
|
+
* (an all-missing `Float64Column`); a non-string/number element → `'other'`
|
|
152
|
+
* (a list/struct vector), rejected by the caller.
|
|
153
|
+
*/
|
|
154
|
+
function classifyArray(raw) {
|
|
155
|
+
for (let j = 0; j < raw.length; j += 1) {
|
|
156
|
+
const v = raw[j];
|
|
157
|
+
if (v == null)
|
|
158
|
+
continue;
|
|
159
|
+
if (typeof v === 'number')
|
|
160
|
+
return 'number';
|
|
161
|
+
if (typeof v === 'string')
|
|
162
|
+
return 'string';
|
|
163
|
+
return 'other';
|
|
164
|
+
}
|
|
165
|
+
return 'number';
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Read one value column, inferring its pond kind from what `toArray()` hands
|
|
169
|
+
* back. Numeric fast paths: `Float64Array` adopted as-is (zero copy), other
|
|
170
|
+
* number typed arrays copy through the `Float64Array` constructor (no map fn —
|
|
171
|
+
* stays off V8's slow iterable path), int64 recombines BigInt-free; a numeric
|
|
172
|
+
* column carrying nulls takes the per-element `get()` path (null → `NaN`). A
|
|
173
|
+
* plain `Array` is classified by content: `Array<string>` (Arrow `Utf8`) →
|
|
174
|
+
* `StringColumn` downstream (dict-encoded when it pays), `Array<number>` (a
|
|
175
|
+
* normalized `Date` column, epoch-ms) → `Float64Column`. Anything else (a
|
|
176
|
+
* list/struct vector) throws, naming the column.
|
|
177
|
+
*/
|
|
178
|
+
function readColumn(vector, name) {
|
|
179
|
+
const raw = vector.toArray();
|
|
180
|
+
if (isNumberTypedArray(raw)) {
|
|
181
|
+
if (vector.nullCount > 0)
|
|
182
|
+
return { kind: 'number', values: numericWithNulls(vector) };
|
|
183
|
+
return {
|
|
184
|
+
kind: 'number',
|
|
185
|
+
values: raw instanceof Float64Array ? raw : new Float64Array(raw),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (isBigIntArray(raw)) {
|
|
189
|
+
if (vector.nullCount > 0)
|
|
190
|
+
return { kind: 'number', values: numericWithNulls(vector) };
|
|
191
|
+
return { kind: 'number', values: int64ToFloat64(raw, 1) };
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(raw)) {
|
|
194
|
+
const kind = classifyArray(raw);
|
|
195
|
+
if (kind === 'string')
|
|
196
|
+
return { kind: 'string', values: readStrings(raw) };
|
|
197
|
+
if (kind === 'number')
|
|
198
|
+
return { kind: 'number', values: readNumbersFromArray(raw) };
|
|
199
|
+
}
|
|
200
|
+
throw new ValidationError(`fromArrow: value column '${name}' is neither numeric nor string — ` +
|
|
201
|
+
`fromArrow supports 'number' and 'string' columns; drop it or select a ` +
|
|
202
|
+
`supported subset via { columns: [...] }`);
|
|
203
|
+
}
|
|
204
|
+
/** Read the time column into an epoch-ms `Float64Array`, scaled by unit. */
|
|
205
|
+
function readTimeColumn(vector, name, scale) {
|
|
206
|
+
if (vector.nullCount > 0) {
|
|
207
|
+
throw new ValidationError(`fromArrow: time column '${name}' has ${vector.nullCount} null value(s) ` +
|
|
208
|
+
`— time keys must be present`);
|
|
209
|
+
}
|
|
210
|
+
const raw = vector.toArray();
|
|
211
|
+
if (isBigIntArray(raw))
|
|
212
|
+
return int64ToFloat64(raw, scale);
|
|
213
|
+
if (raw instanceof Float64Array) {
|
|
214
|
+
if (scale === 1)
|
|
215
|
+
return raw;
|
|
216
|
+
const out = new Float64Array(raw.length);
|
|
217
|
+
for (let j = 0; j < raw.length; j += 1)
|
|
218
|
+
out[j] = raw[j] * scale;
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
if (isNumberTypedArray(raw)) {
|
|
222
|
+
const out = new Float64Array(raw);
|
|
223
|
+
if (scale !== 1)
|
|
224
|
+
for (let j = 0; j < out.length; j += 1)
|
|
225
|
+
out[j] *= scale;
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
// Arrow `Date32`/`Date64` `toArray()` yields a plain `Array<number>` already
|
|
229
|
+
// normalized to epoch-ms (scale is 1 for a Date key; see resolveMsScale).
|
|
230
|
+
if (Array.isArray(raw) && classifyArray(raw) === 'number') {
|
|
231
|
+
const out = readNumbersFromArray(raw);
|
|
232
|
+
if (scale !== 1)
|
|
233
|
+
for (let j = 0; j < out.length; j += 1)
|
|
234
|
+
out[j] *= scale;
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
throw new ValidationError(`fromArrow: time column '${name}' is not a numeric or temporal Arrow column`);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Translate an Arrow `Table` into the `{ name, schema, columns }` triple that
|
|
241
|
+
* `TimeSeries.fromColumns` ingests. Kept separate from the static so the
|
|
242
|
+
* conversion is unit-testable without the class and so the Arrow-shaped types
|
|
243
|
+
* live in one place. The returned columns are all `Float64Array`, so
|
|
244
|
+
* `fromColumns` takes its zero-copy adoption path throughout.
|
|
245
|
+
*/
|
|
246
|
+
export function arrowToColumns(table, options = {}) {
|
|
247
|
+
const fields = table.schema?.fields;
|
|
248
|
+
if (!fields || fields.length === 0) {
|
|
249
|
+
throw new ValidationError('fromArrow: table has no columns');
|
|
250
|
+
}
|
|
251
|
+
// Resolve the time column: explicit `time`, else a field named 'time'.
|
|
252
|
+
const timeName = options.time ??
|
|
253
|
+
(fields.some((f) => f.name === 'time') ? 'time' : undefined);
|
|
254
|
+
if (timeName === undefined) {
|
|
255
|
+
throw new ValidationError("fromArrow: no time column — pass { time: '<column>' } or include a " +
|
|
256
|
+
"field named 'time'");
|
|
257
|
+
}
|
|
258
|
+
const timeField = fields.find((f) => f.name === timeName);
|
|
259
|
+
if (timeField === undefined) {
|
|
260
|
+
throw new ValidationError(`fromArrow: time column '${timeName}' not found in the table schema`);
|
|
261
|
+
}
|
|
262
|
+
const timeVector = table.getChild(timeName);
|
|
263
|
+
if (timeVector == null) {
|
|
264
|
+
throw new ValidationError(`fromArrow: time column '${timeName}' has no vector`);
|
|
265
|
+
}
|
|
266
|
+
// Time unit → ms scale, gated on the Arrow type family (see resolveMsScale).
|
|
267
|
+
const scale = resolveMsScale(timeField, options.timeUnit);
|
|
268
|
+
// Value column selection: explicit `columns` (in order), else every non-time
|
|
269
|
+
// field. Every selected column must be numeric (checked in readValueColumn).
|
|
270
|
+
const valueNames = options.columns ??
|
|
271
|
+
fields.map((f) => f.name).filter((name) => name !== timeName);
|
|
272
|
+
const columns = {
|
|
273
|
+
[timeName]: readTimeColumn(timeVector, timeName, scale),
|
|
274
|
+
};
|
|
275
|
+
const schema = [
|
|
276
|
+
{ name: timeName, kind: 'time' },
|
|
277
|
+
];
|
|
278
|
+
for (const name of valueNames) {
|
|
279
|
+
if (name === timeName) {
|
|
280
|
+
throw new ValidationError(`fromArrow: column '${name}' is the time key and can't also be a ` +
|
|
281
|
+
`value column`);
|
|
282
|
+
}
|
|
283
|
+
const vector = table.getChild(name);
|
|
284
|
+
if (vector == null) {
|
|
285
|
+
throw new ValidationError(`fromArrow: column '${name}' not found in the table`);
|
|
286
|
+
}
|
|
287
|
+
const result = readColumn(vector, name);
|
|
288
|
+
columns[name] = result.values;
|
|
289
|
+
schema.push({ name, kind: result.kind });
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
name: options.name ?? 'arrow',
|
|
293
|
+
schema: schema,
|
|
294
|
+
columns,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=from-arrow.js.map
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { ColumnarStore, type ColumnSchema, type KeyColumn } from '../../columnar/index.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The per-column raw input the `fromColumns` doors accept. A `'number'` column
|
|
4
|
+
* is a `number[]` (adopted-if-`Float64Array`); a `'string'` column is a
|
|
5
|
+
* `string[]` (`null`/`undefined` → missing). The kind is taken from the
|
|
6
|
+
* matching schema entry — the engine dispatches on it.
|
|
7
|
+
*/
|
|
8
|
+
export type RawColumns = Record<string, ReadonlyArray<number | null | undefined> | Float64Array | ReadonlyArray<string | null | undefined>>;
|
|
4
9
|
/**
|
|
5
10
|
* The shared columnar-ingress engine behind `TimeSeries.fromColumns` and
|
|
6
11
|
* `ValueSeries.fromColumns`. Both doors are the same machine — normalize the
|
|
7
12
|
* key column (adopt a `Float64Array` zero-copy, convert a `number[]`),
|
|
8
13
|
* optionally sort by key (stable permutation, disables adoption), enforce the
|
|
9
|
-
* non-decreasing-key invariant, pack each
|
|
10
|
-
* (`null`/`undefined
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* non-decreasing-key invariant, pack each value column by its schema kind
|
|
15
|
+
* (`'number'` → `Float64Column`, `null`/`undefined`/non-finite → `NaN` gap;
|
|
16
|
+
* `'string'` → `StringColumn` via the shared dict-encode heuristic,
|
|
17
|
+
* `null`/`undefined` → missing) — and differ only in the key column they mint
|
|
18
|
+
* (`TimeKeyColumn` vs `ValueKeyColumn`) and the words their errors use. `op`
|
|
19
|
+
* prefixes every message (so a throw names the door the caller went through)
|
|
20
|
+
* and `keyNoun` names the key in the out-of-order error
|
|
21
|
+
* (`timestamps` / `axis values`).
|
|
15
22
|
*
|
|
16
23
|
* `makeKey` runs **before** the ordering scan, matching the original inline
|
|
17
24
|
* order of checks: a non-finite key fails in the key-column constructor first,
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { ColumnarStore, Float64Column, validityFromPredicate, } from '../../columnar/index.js';
|
|
1
|
+
import { ColumnarStore, Float64Column, stringColumnFromArray, validityFromPredicate, } from '../../columnar/index.js';
|
|
2
2
|
import { ValidationError } from '../../core/errors.js';
|
|
3
3
|
/**
|
|
4
4
|
* The shared columnar-ingress engine behind `TimeSeries.fromColumns` and
|
|
5
5
|
* `ValueSeries.fromColumns`. Both doors are the same machine — normalize the
|
|
6
6
|
* key column (adopt a `Float64Array` zero-copy, convert a `number[]`),
|
|
7
7
|
* optionally sort by key (stable permutation, disables adoption), enforce the
|
|
8
|
-
* non-decreasing-key invariant, pack each
|
|
9
|
-
* (`null`/`undefined
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* non-decreasing-key invariant, pack each value column by its schema kind
|
|
9
|
+
* (`'number'` → `Float64Column`, `null`/`undefined`/non-finite → `NaN` gap;
|
|
10
|
+
* `'string'` → `StringColumn` via the shared dict-encode heuristic,
|
|
11
|
+
* `null`/`undefined` → missing) — and differ only in the key column they mint
|
|
12
|
+
* (`TimeKeyColumn` vs `ValueKeyColumn`) and the words their errors use. `op`
|
|
13
|
+
* prefixes every message (so a throw names the door the caller went through)
|
|
14
|
+
* and `keyNoun` names the key in the out-of-order error
|
|
15
|
+
* (`timestamps` / `axis values`).
|
|
14
16
|
*
|
|
15
17
|
* `makeKey` runs **before** the ordering scan, matching the original inline
|
|
16
18
|
* order of checks: a non-finite key fails in the key-column constructor first,
|
|
@@ -85,12 +87,14 @@ export function ingestColumnsToStore(input) {
|
|
|
85
87
|
`pass { sort: true } or pre-sort the columns`);
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
|
-
// Value columns — packed directly (missing-aware) from the arrays
|
|
90
|
+
// Value columns — packed directly (missing-aware) from the arrays,
|
|
91
|
+
// dispatched by the schema kind.
|
|
89
92
|
const columnMap = new Map();
|
|
90
93
|
for (let i = 1; i < schema.length; i += 1) {
|
|
91
94
|
const def = schema[i];
|
|
92
|
-
if (def.kind !== 'number') {
|
|
93
|
-
throw new ValidationError(`${op}:
|
|
95
|
+
if (def.kind !== 'number' && def.kind !== 'string') {
|
|
96
|
+
throw new ValidationError(`${op}: supports 'number' and 'string' value columns; column ` +
|
|
97
|
+
`'${def.name}' is '${def.kind}'`);
|
|
94
98
|
}
|
|
95
99
|
const raw = columns[def.name];
|
|
96
100
|
if (raw === undefined) {
|
|
@@ -99,6 +103,36 @@ export function ingestColumnsToStore(input) {
|
|
|
99
103
|
if (raw.length !== count) {
|
|
100
104
|
throw new ValidationError(`${op}: column '${def.name}' length ${raw.length} does not match key length ${count}`);
|
|
101
105
|
}
|
|
106
|
+
if (def.kind === 'string') {
|
|
107
|
+
// String column → StringColumn (dict-encoded when it pays; see
|
|
108
|
+
// `stringColumnFromArray`). `null`/`undefined` are missing. When sorting,
|
|
109
|
+
// reorder into a fresh array through the key permutation first — strings
|
|
110
|
+
// are heap objects, so there's no zero-copy story to preserve anyway.
|
|
111
|
+
//
|
|
112
|
+
// The `columns` input type isn't correlated per-column with the schema
|
|
113
|
+
// kind (one `RawColumns` union covers both), so a numeric typed array can
|
|
114
|
+
// reach a `'string'` column at the type level. Reject that clear mismatch
|
|
115
|
+
// loudly rather than stringifying numbers — the caller crossed the schema
|
|
116
|
+
// wires. (A plain `number[]` handed to a string column is still trusted;
|
|
117
|
+
// per-cell kind-checking isn't worth the hot-path cost.)
|
|
118
|
+
if (ArrayBuffer.isView(raw)) {
|
|
119
|
+
throw new ValidationError(`${op}: string column '${def.name}' must be a string[] — got a ` +
|
|
120
|
+
`typed array (a numeric buffer can't back a string column)`);
|
|
121
|
+
}
|
|
122
|
+
const rawStrings = raw;
|
|
123
|
+
let source;
|
|
124
|
+
if (order !== null) {
|
|
125
|
+
const reordered = new Array(count);
|
|
126
|
+
for (let j = 0; j < count; j += 1)
|
|
127
|
+
reordered[j] = rawStrings[order[j]];
|
|
128
|
+
source = reordered;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
source = rawStrings;
|
|
132
|
+
}
|
|
133
|
+
columnMap.set(def.name, stringColumnFromArray(source));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
102
136
|
// Normalize to a Float64Array either way — adopt if already typed (the
|
|
103
137
|
// fast path a protobuf / fixed-point decoder hits, zero-copy), else
|
|
104
138
|
// convert (`null`/`undefined` -> `NaN`) — then apply ONE validity rule
|
|
@@ -111,30 +145,31 @@ export function ingestColumnsToStore(input) {
|
|
|
111
145
|
// array type decoded it.
|
|
112
146
|
// Manual loop, not `Float64Array.from(arr, mapFn)` — see the key-column
|
|
113
147
|
// comment above; the cost applies identically here.
|
|
148
|
+
const numeric = raw;
|
|
114
149
|
let values;
|
|
115
150
|
if (order !== null) {
|
|
116
151
|
// Sorting: reorder into a fresh buffer through the key permutation (no
|
|
117
152
|
// zero-copy adoption — the rows are being moved). Same missing rule
|
|
118
153
|
// (`null`/`undefined` → NaN) applied while remapping.
|
|
119
154
|
values = new Float64Array(count);
|
|
120
|
-
if (
|
|
155
|
+
if (numeric instanceof Float64Array) {
|
|
121
156
|
for (let j = 0; j < count; j += 1)
|
|
122
|
-
values[j] =
|
|
157
|
+
values[j] = numeric[order[j]];
|
|
123
158
|
}
|
|
124
159
|
else {
|
|
125
160
|
for (let j = 0; j < count; j += 1) {
|
|
126
|
-
const v =
|
|
161
|
+
const v = numeric[order[j]];
|
|
127
162
|
values[j] = v == null ? NaN : v;
|
|
128
163
|
}
|
|
129
164
|
}
|
|
130
165
|
}
|
|
131
|
-
else if (
|
|
132
|
-
values =
|
|
166
|
+
else if (numeric instanceof Float64Array) {
|
|
167
|
+
values = numeric;
|
|
133
168
|
}
|
|
134
169
|
else {
|
|
135
170
|
values = new Float64Array(count);
|
|
136
171
|
for (let j = 0; j < count; j += 1) {
|
|
137
|
-
const v =
|
|
172
|
+
const v = numeric[j];
|
|
138
173
|
values[j] = v == null ? NaN : v;
|
|
139
174
|
}
|
|
140
175
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateSchema, AppendColumn, OptionalNumberColumn, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap, ValidatedAggregateMap } from '../schema/index.js';
|
|
2
2
|
import type { RenameSchema, RollingAlignment, RollingSchema, ColumnValue, DedupeKeep, DiffSchema, FillMapping, FillStrategy, ScalarKind, ScalarValue, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesJsonInput, TimeSeriesInput, TimeRangeKeyedSchema, ValueColumnKindForName, ValueColumnNameForSchema, ValueColumnsForSchema, ValueKeyedSchema } from '../schema/index.js';
|
|
3
3
|
import { type ScanStep } from './operators/scan.js';
|
|
4
|
+
import { type ArrowTableLike, type FromArrowOptions } from './operators/from-arrow.js';
|
|
4
5
|
import { ValueSeries } from './value-series.js';
|
|
5
6
|
import { type BinSpec } from './by-column.js';
|
|
6
7
|
import { type WindowSpec } from './rolling-by-column.js';
|
|
@@ -134,9 +135,10 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
134
135
|
* — clamp the key column yourself before ingest; the library won't mutate data
|
|
135
136
|
* for you.)
|
|
136
137
|
*
|
|
137
|
-
* **
|
|
138
|
-
*
|
|
139
|
-
*
|
|
138
|
+
* **Value columns:** `number` (→ `Float64Column`; `Float64Array` adopted) and
|
|
139
|
+
* `string` (→ `StringColumn`, dict-encoded when it pays; `null`/`undefined`
|
|
140
|
+
* missing). Other key kinds (`interval` / `timeRange`) and other value kinds
|
|
141
|
+
* (`boolean` / array) throw for now — extend as consumers need.
|
|
140
142
|
*
|
|
141
143
|
* @throws ValidationError on a missing column, a length mismatch, an
|
|
142
144
|
* unsupported kind, or an out-of-order (decreasing) timestamp when `sort`
|
|
@@ -147,7 +149,7 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
147
149
|
static fromColumns<S extends SeriesSchema>(input: {
|
|
148
150
|
name: string;
|
|
149
151
|
schema: S;
|
|
150
|
-
columns: Record<string, ReadonlyArray<number | null | undefined> | Float64Array
|
|
152
|
+
columns: Record<string, ReadonlyArray<number | null | undefined> | Float64Array | ReadonlyArray<string | null | undefined>>;
|
|
151
153
|
/**
|
|
152
154
|
* Sort the rows by key before construction (off by default), for a columnar
|
|
153
155
|
* payload whose rows aren't guaranteed ordered — the counterpart of
|
|
@@ -156,6 +158,43 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
156
158
|
*/
|
|
157
159
|
sort?: boolean;
|
|
158
160
|
}): TimeSeries<S>;
|
|
161
|
+
/**
|
|
162
|
+
* Example: `TimeSeries.fromArrow(tableFromIPC(bytes), { time: 'ts' })`.
|
|
163
|
+
*
|
|
164
|
+
* Build a time series from a decoded Apache Arrow `Table`. pond does **not**
|
|
165
|
+
* depend on `apache-arrow` — bring your own (`tableFromIPC(...)` /
|
|
166
|
+
* `tableFromArrays(...)`) and hand the `Table` here; the input is duck-typed
|
|
167
|
+
* against the small {@link ArrowTableLike} slice we read. Ingest is the
|
|
168
|
+
* zero-copy path: every **single-chunk** `Float64` column's backing
|
|
169
|
+
* `Float64Array` is adopted as-is, and the time key is converted
|
|
170
|
+
* **BigInt-free** — Arrow's idiomatic int64 timestamps are recombined from
|
|
171
|
+
* their two int32 halves rather than `Number(bigint)` per row (which costs
|
|
172
|
+
* ~30ms at 500k rows). A table decoded from a multi-record-batch IPC stream
|
|
173
|
+
* has multi-chunk columns, whose `toArray()` concatenates into a fresh buffer
|
|
174
|
+
* — still correct, but a copy rather than an adopt.
|
|
175
|
+
*
|
|
176
|
+
* Column handling:
|
|
177
|
+
* - **Time key** — named by `time` (default: a field named `'time'`).
|
|
178
|
+
* Timestamp/int/float/Date sources accepted and normalized to epoch ms: a
|
|
179
|
+
* `Timestamp`'s raw-unit int64 is scaled by its `TimeUnit`; a `Date32`/
|
|
180
|
+
* `Date64` already arrives as epoch-ms via Arrow's `toArray()` and passes
|
|
181
|
+
* through; a plain int/float is taken as ms. `timeUnit` overrides.
|
|
182
|
+
* - **Value columns** — every non-time field by default, or the subset named
|
|
183
|
+
* by `columns` (in order). Numeric columns (`Float32`/int convert to
|
|
184
|
+
* `Float64Array`; int64 recombines BigInt-free; nulls → `NaN`) and string
|
|
185
|
+
* columns (Arrow `Utf8` → `StringColumn`, dict-encoded when it pays; nulls →
|
|
186
|
+
* missing) are supported. Any other Arrow type (list/struct/…) throws,
|
|
187
|
+
* naming it. A null in the time key throws.
|
|
188
|
+
*
|
|
189
|
+
* The rows must be time-ordered (as `fromColumns` requires); pass
|
|
190
|
+
* `{ sort: true }` for an unordered table (disables zero-copy adoption).
|
|
191
|
+
*
|
|
192
|
+
* **Trust contract on the type parameter:** the runtime schema is derived
|
|
193
|
+
* from the Arrow fields. Supplying `S` (`fromArrow<MySchema>(...)`) is a
|
|
194
|
+
* downstream-typing convenience taken on trust — pond does not verify the
|
|
195
|
+
* Arrow fields match `S`, exactly as `fromEvents` trusts its schema.
|
|
196
|
+
*/
|
|
197
|
+
static fromArrow<S extends SeriesSchema = SeriesSchema>(table: ArrowTableLike, options?: FromArrowOptions): TimeSeries<S>;
|
|
159
198
|
/**
|
|
160
199
|
* Example: `TimeSeries.fromEvents(events, { schema, name })`.
|
|
161
200
|
* Builds a typed series from an array of `Event` instances. The events
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { isAggregateOutputSpec, normalizeAggregateColumns, tryAggregateColumnarTimeKeyed, } from './aggregate-columns.js';
|
|
1
|
+
import { isAggregateOutputSpec, normalizeAggregateColumns, tryAggregateColumnarTimeKeyed, tryRollingCountColumnarNumeric, } from './aggregate-columns.js';
|
|
2
2
|
import { cumulativeOp, } from './operators/cumulative.js';
|
|
3
3
|
import { scanOp } from './operators/scan.js';
|
|
4
4
|
import { byValueOp } from './operators/by-value.js';
|
|
5
5
|
import { ingestColumnsToStore } from './operators/ingest-columns.js';
|
|
6
|
+
import { arrowToColumns, } from './operators/from-arrow.js';
|
|
6
7
|
import { ValueSeries } from './value-series.js';
|
|
7
8
|
import { diffRateOp } from './operators/diff-rate.js';
|
|
8
9
|
import { fillOp } from './operators/fill.js';
|
|
@@ -19,7 +20,7 @@ import { TimeRange } from '../core/time-range.js';
|
|
|
19
20
|
import { compareEventKeys } from '../core/temporal.js';
|
|
20
21
|
import { PartitionedTimeSeries } from './partitioned-time-series.js';
|
|
21
22
|
import { Sequence } from '../sequence/sequence.js';
|
|
22
|
-
import { ColumnarStore, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, float64ColumnFromArray, stringColumnFromArray, withColumnAppended, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from '../columnar/index.js';
|
|
23
|
+
import { ColumnarStore, Float64Column, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, bitmapByteCount, float64ColumnFromArray, stringColumnFromArray, validityFromBits, withColumnAppended, withColumnReplaced, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from '../columnar/index.js';
|
|
23
24
|
import { ValidationError } from '../core/errors.js';
|
|
24
25
|
import { SeriesStore } from '../live/series-store.js';
|
|
25
26
|
import { parseDuration } from '../core/duration.js';
|
|
@@ -539,9 +540,10 @@ export class TimeSeries {
|
|
|
539
540
|
* — clamp the key column yourself before ingest; the library won't mutate data
|
|
540
541
|
* for you.)
|
|
541
542
|
*
|
|
542
|
-
* **
|
|
543
|
-
*
|
|
544
|
-
*
|
|
543
|
+
* **Value columns:** `number` (→ `Float64Column`; `Float64Array` adopted) and
|
|
544
|
+
* `string` (→ `StringColumn`, dict-encoded when it pays; `null`/`undefined`
|
|
545
|
+
* missing). Other key kinds (`interval` / `timeRange`) and other value kinds
|
|
546
|
+
* (`boolean` / array) throw for now — extend as consumers need.
|
|
545
547
|
*
|
|
546
548
|
* @throws ValidationError on a missing column, a length mismatch, an
|
|
547
549
|
* unsupported kind, or an out-of-order (decreasing) timestamp when `sort`
|
|
@@ -572,6 +574,51 @@ export class TimeSeries {
|
|
|
572
574
|
});
|
|
573
575
|
return TimeSeries.#fromTrustedStore(name, schema, store);
|
|
574
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Example: `TimeSeries.fromArrow(tableFromIPC(bytes), { time: 'ts' })`.
|
|
579
|
+
*
|
|
580
|
+
* Build a time series from a decoded Apache Arrow `Table`. pond does **not**
|
|
581
|
+
* depend on `apache-arrow` — bring your own (`tableFromIPC(...)` /
|
|
582
|
+
* `tableFromArrays(...)`) and hand the `Table` here; the input is duck-typed
|
|
583
|
+
* against the small {@link ArrowTableLike} slice we read. Ingest is the
|
|
584
|
+
* zero-copy path: every **single-chunk** `Float64` column's backing
|
|
585
|
+
* `Float64Array` is adopted as-is, and the time key is converted
|
|
586
|
+
* **BigInt-free** — Arrow's idiomatic int64 timestamps are recombined from
|
|
587
|
+
* their two int32 halves rather than `Number(bigint)` per row (which costs
|
|
588
|
+
* ~30ms at 500k rows). A table decoded from a multi-record-batch IPC stream
|
|
589
|
+
* has multi-chunk columns, whose `toArray()` concatenates into a fresh buffer
|
|
590
|
+
* — still correct, but a copy rather than an adopt.
|
|
591
|
+
*
|
|
592
|
+
* Column handling:
|
|
593
|
+
* - **Time key** — named by `time` (default: a field named `'time'`).
|
|
594
|
+
* Timestamp/int/float/Date sources accepted and normalized to epoch ms: a
|
|
595
|
+
* `Timestamp`'s raw-unit int64 is scaled by its `TimeUnit`; a `Date32`/
|
|
596
|
+
* `Date64` already arrives as epoch-ms via Arrow's `toArray()` and passes
|
|
597
|
+
* through; a plain int/float is taken as ms. `timeUnit` overrides.
|
|
598
|
+
* - **Value columns** — every non-time field by default, or the subset named
|
|
599
|
+
* by `columns` (in order). Numeric columns (`Float32`/int convert to
|
|
600
|
+
* `Float64Array`; int64 recombines BigInt-free; nulls → `NaN`) and string
|
|
601
|
+
* columns (Arrow `Utf8` → `StringColumn`, dict-encoded when it pays; nulls →
|
|
602
|
+
* missing) are supported. Any other Arrow type (list/struct/…) throws,
|
|
603
|
+
* naming it. A null in the time key throws.
|
|
604
|
+
*
|
|
605
|
+
* The rows must be time-ordered (as `fromColumns` requires); pass
|
|
606
|
+
* `{ sort: true }` for an unordered table (disables zero-copy adoption).
|
|
607
|
+
*
|
|
608
|
+
* **Trust contract on the type parameter:** the runtime schema is derived
|
|
609
|
+
* from the Arrow fields. Supplying `S` (`fromArrow<MySchema>(...)`) is a
|
|
610
|
+
* downstream-typing convenience taken on trust — pond does not verify the
|
|
611
|
+
* Arrow fields match `S`, exactly as `fromEvents` trusts its schema.
|
|
612
|
+
*/
|
|
613
|
+
static fromArrow(table, options = {}) {
|
|
614
|
+
const { name, schema, columns } = arrowToColumns(table, options);
|
|
615
|
+
return TimeSeries.fromColumns({
|
|
616
|
+
name,
|
|
617
|
+
schema: schema,
|
|
618
|
+
columns,
|
|
619
|
+
sort: options.sort ?? false,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
575
622
|
/**
|
|
576
623
|
* Example: `TimeSeries.fromEvents(events, { schema, name })`.
|
|
577
624
|
* Builds a typed series from an array of `Event` instances. The events
|
|
@@ -2205,6 +2252,23 @@ export class TimeSeries {
|
|
|
2205
2252
|
// materialization, no per-row `Event`, no row re-validation/re-pack.
|
|
2206
2253
|
const store = this.#store.store;
|
|
2207
2254
|
const rowCount = store.length;
|
|
2255
|
+
// Count-window numeric fast path: an all-built-in numeric mapping over
|
|
2256
|
+
// packed sources builds its typed result columns in one sweep (see
|
|
2257
|
+
// `tryRollingCountColumnarNumeric`) — none of the generic sweep's
|
|
2258
|
+
// per-row snapshot arrays, boxed accumulators, or post-pass re-pack.
|
|
2259
|
+
// `null` (a custom reducer, non-number output, chunked source) falls
|
|
2260
|
+
// through to the generic sweep below.
|
|
2261
|
+
if (countWindow !== null) {
|
|
2262
|
+
const fastColumns = tryRollingCountColumnarNumeric((name) => store.columns.get(name), rowCount, columnSpecs, countWindow, alignment, minSamples);
|
|
2263
|
+
if (fastColumns !== null) {
|
|
2264
|
+
const fastOutColumns = new Map();
|
|
2265
|
+
for (let c = 0; c < columnSpecs.length; c += 1) {
|
|
2266
|
+
fastOutColumns.set(columnSpecs[c].output, fastColumns[c]);
|
|
2267
|
+
}
|
|
2268
|
+
const fastStore = ColumnarStore.fromTrustedStore(resultSchema, store.keys, fastOutColumns);
|
|
2269
|
+
return TimeSeries.#fromTrustedStore(this.name, resultSchema, fastStore);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2208
2272
|
const sourceCols = columnSpecs.map((spec) => store.columns.get(spec.source));
|
|
2209
2273
|
const reducerStates = columnSpecs.map((spec) => isBuiltInAggregateReducer(spec.reducer)
|
|
2210
2274
|
? createRollingReducerState(spec.reducer)
|
|
@@ -2434,11 +2498,6 @@ export class TimeSeries {
|
|
|
2434
2498
|
const resultSchema = output === undefined
|
|
2435
2499
|
? makeSmoothSchema(this.schema, column)
|
|
2436
2500
|
: makeSmoothSchema(this.schema, column, output);
|
|
2437
|
-
const anchors = this.events.map((event) => eventAnchorTime(event.key()));
|
|
2438
|
-
const sourceValues = this.events.map((event) => {
|
|
2439
|
-
const raw = event.get(column);
|
|
2440
|
-
return typeof raw === 'number' ? raw : undefined;
|
|
2441
|
-
});
|
|
2442
2501
|
if (method === 'ema') {
|
|
2443
2502
|
// Rate parameter: `alpha` directly, or the financial `span` convention
|
|
2444
2503
|
// (α = 2/(span+1) — a `span`-period EMA), exactly one of the two.
|
|
@@ -2488,6 +2547,63 @@ export class TimeSeries {
|
|
|
2488
2547
|
!Number.isFinite(minSamples)) {
|
|
2489
2548
|
throw new TypeError('ema smoothing requires minSamples to be a non-negative integer');
|
|
2490
2549
|
}
|
|
2550
|
+
// Columnar fast path: on a packed numeric source column the EMA
|
|
2551
|
+
// recurrence runs straight off the typed buffer into a typed result
|
|
2552
|
+
// column — no event materialization, no per-row Event / frozen-tuple
|
|
2553
|
+
// allocation, and no full-series intake re-pack (the key and every
|
|
2554
|
+
// untouched column pass through by reference). Emitted values are the
|
|
2555
|
+
// exact sequence the event path emits (same reads, same arithmetic),
|
|
2556
|
+
// and each one is asserted finite so the intake rejection the event
|
|
2557
|
+
// path gets from its row re-pack is preserved. Non-packed sources
|
|
2558
|
+
// (chunked storage) fall back to the event path below.
|
|
2559
|
+
const emaSourceCol = this.#store.store.columns.get(column);
|
|
2560
|
+
if (emaSourceCol !== undefined &&
|
|
2561
|
+
emaSourceCol.kind === 'number' &&
|
|
2562
|
+
emaSourceCol.storage === 'packed') {
|
|
2563
|
+
const store = this.#store.store;
|
|
2564
|
+
const packed = emaSourceCol;
|
|
2565
|
+
const rowCount = store.length;
|
|
2566
|
+
const src = packed._values;
|
|
2567
|
+
const srcValidity = packed.validity;
|
|
2568
|
+
const out = new Float64Array(rowCount);
|
|
2569
|
+
const outBits = new Uint8Array(bitmapByteCount(rowCount));
|
|
2570
|
+
let outDefined = 0;
|
|
2571
|
+
let hasPrevious = false;
|
|
2572
|
+
let previous = 0;
|
|
2573
|
+
let seen = 0;
|
|
2574
|
+
for (let i = 0; i < rowCount; i += 1) {
|
|
2575
|
+
if (srcValidity !== undefined && !srcValidity.isDefined(i)) {
|
|
2576
|
+
continue; // missing source cell — emitted value is missing too
|
|
2577
|
+
}
|
|
2578
|
+
const raw = src[i];
|
|
2579
|
+
const smoothed = hasPrevious
|
|
2580
|
+
? alpha * raw + (1 - alpha) * previous
|
|
2581
|
+
: raw;
|
|
2582
|
+
previous = smoothed;
|
|
2583
|
+
hasPrevious = true;
|
|
2584
|
+
seen += 1;
|
|
2585
|
+
// Mask the emitted value during the length-preserving warm-up;
|
|
2586
|
+
// `previous` / `seen` advanced on the real value above.
|
|
2587
|
+
if (seen < minSamples)
|
|
2588
|
+
continue;
|
|
2589
|
+
if (!Number.isFinite(smoothed)) {
|
|
2590
|
+
throw new ValidationError(`smooth column '${output ?? column}': result ${smoothed} is not a valid 'number' value`);
|
|
2591
|
+
}
|
|
2592
|
+
out[i] = smoothed;
|
|
2593
|
+
outBits[i >> 3] |= 1 << (i & 7);
|
|
2594
|
+
outDefined += 1;
|
|
2595
|
+
}
|
|
2596
|
+
const outValidity = outDefined === rowCount
|
|
2597
|
+
? undefined
|
|
2598
|
+
: validityFromBits(outBits, rowCount);
|
|
2599
|
+
// Every defined cell was asserted finite above → `allFinite`.
|
|
2600
|
+
const outColumn = new Float64Column(out, rowCount, outValidity, true);
|
|
2601
|
+
const reshaped = output === undefined || output === column
|
|
2602
|
+
? withColumnReplaced(store, column, outColumn)
|
|
2603
|
+
: withColumnAppended(store, output, outColumn);
|
|
2604
|
+
const kept = warmup > 0 ? withRowRange(reshaped, warmup, rowCount) : reshaped;
|
|
2605
|
+
return TimeSeries.#fromTrustedStore(this.name, resultSchema, kept);
|
|
2606
|
+
}
|
|
2491
2607
|
let previous;
|
|
2492
2608
|
let seen = 0;
|
|
2493
2609
|
const resultRows = this.events.map((event) => {
|
|
@@ -2521,6 +2637,14 @@ export class TimeSeries {
|
|
|
2521
2637
|
rows: keptRows,
|
|
2522
2638
|
});
|
|
2523
2639
|
}
|
|
2640
|
+
// Anchor times + source values for the window-based methods
|
|
2641
|
+
// (movingAverage / loess). Materialized here — below the ema branch —
|
|
2642
|
+
// because ema never reads either (its fast path stays event-free).
|
|
2643
|
+
const anchors = this.events.map((event) => eventAnchorTime(event.key()));
|
|
2644
|
+
const sourceValues = this.events.map((event) => {
|
|
2645
|
+
const raw = event.get(column);
|
|
2646
|
+
return typeof raw === 'number' ? raw : undefined;
|
|
2647
|
+
});
|
|
2524
2648
|
if (method === 'loess') {
|
|
2525
2649
|
if (!('span' in options)) {
|
|
2526
2650
|
throw new TypeError('loess smoothing requires a span option');
|
|
@@ -57,10 +57,10 @@ export declare class ValueSeries<VS extends ValueSeriesSchema> {
|
|
|
57
57
|
* stable sort every unordered snapshot wants (e.g. a keyed live feed that
|
|
58
58
|
* delivers rows in update order, not axis order).
|
|
59
59
|
*
|
|
60
|
-
* **
|
|
60
|
+
* **Value columns:** `number` and `string`, matching `TimeSeries.fromColumns`.
|
|
61
61
|
*
|
|
62
62
|
* @throws ValidationError on a non-`'value'` axis kind, a missing column, a
|
|
63
|
-
* length mismatch,
|
|
63
|
+
* length mismatch, an unsupported value-column kind, or an out-of-order axis
|
|
64
64
|
* when `sort` is not set. Throws RangeError on a non-finite
|
|
65
65
|
* (`null`/`NaN`/`±Infinity`) axis cell — sorting can't make it valid — or
|
|
66
66
|
* a duplicate column name (the axis name repeated among the value columns).
|
|
@@ -68,7 +68,7 @@ export declare class ValueSeries<VS extends ValueSeriesSchema> {
|
|
|
68
68
|
static fromColumns<VS extends ValueSeriesSchema>(input: {
|
|
69
69
|
name: string;
|
|
70
70
|
schema: VS;
|
|
71
|
-
columns: Record<string, ReadonlyArray<number | null | undefined> | Float64Array
|
|
71
|
+
columns: Record<string, ReadonlyArray<number | null | undefined> | Float64Array | ReadonlyArray<string | null | undefined>>;
|
|
72
72
|
/**
|
|
73
73
|
* Sort the rows by axis value before construction (off by default), for a
|
|
74
74
|
* payload whose rows aren't guaranteed ordered. Stable; disables the
|
|
@@ -67,10 +67,10 @@ export class ValueSeries {
|
|
|
67
67
|
* stable sort every unordered snapshot wants (e.g. a keyed live feed that
|
|
68
68
|
* delivers rows in update order, not axis order).
|
|
69
69
|
*
|
|
70
|
-
* **
|
|
70
|
+
* **Value columns:** `number` and `string`, matching `TimeSeries.fromColumns`.
|
|
71
71
|
*
|
|
72
72
|
* @throws ValidationError on a non-`'value'` axis kind, a missing column, a
|
|
73
|
-
* length mismatch,
|
|
73
|
+
* length mismatch, an unsupported value-column kind, or an out-of-order axis
|
|
74
74
|
* when `sort` is not set. Throws RangeError on a non-finite
|
|
75
75
|
* (`null`/`NaN`/`±Infinity`) axis cell — sorting can't make it valid — or
|
|
76
76
|
* a duplicate column name (the axis name repeated among the value columns).
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { Time } from './core/time.js';
|
|
|
19
19
|
export { TimeRange, toTimeRange } from './core/time-range.js';
|
|
20
20
|
export { Sequence } from './sequence/sequence.js';
|
|
21
21
|
export { TimeSeries, type KeyLike } from './batch/time-series.js';
|
|
22
|
+
export type { ArrowTableLike, ArrowVectorLike, ArrowFieldLike, ArrowSchemaLike, ArrowTimeUnit, FromArrowOptions, } from './batch/operators/from-arrow.js';
|
|
22
23
|
export { ValueSeries } from './batch/value-series.js';
|
|
23
24
|
export { top } from './reducers/index.js';
|
|
24
25
|
export { ValidationError } from './core/errors.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pond-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "TypeScript-first time series primitives",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@babel/runtime": "^7.29.2",
|
|
45
45
|
"@types/node": "^20.17.0",
|
|
46
|
+
"apache-arrow": "^21.2.0",
|
|
46
47
|
"pondjs": "^0.9.0",
|
|
47
48
|
"prettier": "^3.8.3",
|
|
48
49
|
"typescript": "^5.6.3",
|