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 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.50.0...HEAD
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 type { AggregateMap, AggregateOutputMap, AggregateReducer, ScalarKind, SeriesSchema } from '../schema/index.js';
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
- /** The per-column raw input the `fromColumns` doors accept. */
3
- export type RawColumns = Record<string, ReadonlyArray<number | null | undefined> | Float64Array>;
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 `'number'` value column
10
- * (`null`/`undefined` → `NaN`, one gap rule for both input types) — and differ
11
- * only in the key column they mint (`TimeKeyColumn` vs `ValueKeyColumn`) and
12
- * the words their errors use. `op` prefixes every message (so a throw names
13
- * the door the caller went through) and `keyNoun` names the key in the
14
- * out-of-order error (`timestamps` / `axis values`).
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 `'number'` value column
9
- * (`null`/`undefined` → `NaN`, one gap rule for both input types) — and differ
10
- * only in the key column they mint (`TimeKeyColumn` vs `ValueKeyColumn`) and
11
- * the words their errors use. `op` prefixes every message (so a throw names
12
- * the door the caller went through) and `keyNoun` names the key in the
13
- * out-of-order error (`timestamps` / `axis values`).
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}: v1 supports 'number' value columns; column '${def.name}' is '${def.kind}'`);
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 (raw instanceof Float64Array) {
155
+ if (numeric instanceof Float64Array) {
121
156
  for (let j = 0; j < count; j += 1)
122
- values[j] = raw[order[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 = raw[order[j]];
161
+ const v = numeric[order[j]];
127
162
  values[j] = v == null ? NaN : v;
128
163
  }
129
164
  }
130
165
  }
131
- else if (raw instanceof Float64Array) {
132
- values = raw;
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 = raw[j];
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
- * **v1 scope:** a `time`-kind key + `number` value columns (the market-data /
138
- * chart wire case). Other key kinds (`interval` / `timeRange`) and non-numeric
139
- * value columns throw for now extend as consumers need.
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
- * **v1 scope:** a `time`-kind key + `number` value columns (the market-data /
543
- * chart wire case). Other key kinds (`interval` / `timeRange`) and non-numeric
544
- * value columns throw for now extend as consumers need.
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
- * **v1 scope:** `number` value columns, matching `TimeSeries.fromColumns`.
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, a non-`'number'` value column, or an out-of-order axis
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
- * **v1 scope:** `number` value columns, matching `TimeSeries.fromColumns`.
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, a non-`'number'` value column, or an out-of-order axis
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.50.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",