pond-ts 0.42.0 → 0.44.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 +164 -1
- package/dist/batch/operators/ingest-columns.d.ts +33 -0
- package/dist/batch/operators/ingest-columns.js +146 -0
- package/dist/batch/time-series.d.ts +4 -3
- package/dist/batch/time-series.js +17 -119
- package/dist/batch/value-series.d.ts +57 -1
- package/dist/batch/value-series.js +69 -3
- package/package.json +1 -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/pjm17971/pond-ts/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.44.0...HEAD
|
|
12
|
+
[0.44.0]: https://github.com/pjm17971/pond-ts/compare/v0.43.0...v0.44.0
|
|
13
|
+
[0.43.0]: https://github.com/pjm17971/pond-ts/compare/v0.42.0...v0.43.0
|
|
12
14
|
[0.42.0]: https://github.com/pjm17971/pond-ts/compare/v0.41.0...v0.42.0
|
|
13
15
|
[0.41.0]: https://github.com/pjm17971/pond-ts/compare/v0.40.0...v0.41.0
|
|
14
16
|
[0.40.0]: https://github.com/pjm17971/pond-ts/compare/v0.39.0...v0.40.0
|
|
@@ -40,6 +42,157 @@ and type-level changes; patch bumps are strictly additive.
|
|
|
40
42
|
|
|
41
43
|
## [Unreleased]
|
|
42
44
|
|
|
45
|
+
## [0.44.0] — 2026-07-11
|
|
46
|
+
|
|
47
|
+
The **value-axis charts** release: cross-sectional data (a volatility smile keyed
|
|
48
|
+
by strike) becomes a first-class charting surface. `ValueSeries.fromColumns` is
|
|
49
|
+
the direct columnar door; `<ScatterChart>` and `<BoxPlot>` join `<LineChart>` on
|
|
50
|
+
the value axis; `<BoxPlot>` gains range-only (bid→ask) marks, `offset` pairing,
|
|
51
|
+
and `capWidth`; and the region cursor works on value axes and snaps to histogram
|
|
52
|
+
bins.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **`<BoxPlot>` finished for the value axis + range-only marks** (`@pond-ts/charts`).
|
|
57
|
+
Four coordinated changes, driven by the volatility smile's per-strike bid/ask IV
|
|
58
|
+
segments (`docs/notes/vol-smile-followups-2026-07.md` §1):
|
|
59
|
+
- **Accepts a `ValueSeries`** (`series.byValue('strike')` /
|
|
60
|
+
`ValueSeries.fromColumns`) — boxes on a value axis, the same instanceof branch
|
|
61
|
+
as `<LineChart>` / `<ScatterChart>`. The box **width** now comes from neighbour
|
|
62
|
+
spacing for a **point** key (a `ValueSeries`, or a point-keyed `TimeSeries`) —
|
|
63
|
+
like bars/candles — instead of collapsing to the 1px floor; an interval-keyed
|
|
64
|
+
`TimeSeries` still uses its `[begin, end)`.
|
|
65
|
+
- **Optional `q1`/`median`/`q3`** — omit `q1`+`q3` for a **range-only** box: a
|
|
66
|
+
whisker-only `lower→upper` segment, no body (a bid→ask IV mark honestly named,
|
|
67
|
+
not a candlestick abuse). Omitting exactly one of `q1`/`q3` throws.
|
|
68
|
+
- **`offset` prop** (`<BoxPlot>` and `<ScatterChart>`) — a **pixel** shift for
|
|
69
|
+
pairing same-key marks (call/put at one strike) side by side, zoom-stable. On
|
|
70
|
+
the scatter it moves the draw **and** the click hit-test together; on the box
|
|
71
|
+
the readout hit-tests in un-shifted data space (keep the offset small).
|
|
72
|
+
- **`capWidth` prop** (`<BoxPlot>`) — the whisker end-cap width in **pixels**
|
|
73
|
+
(else half the box width). A small fixed cap keeps two `offset`-paired marks'
|
|
74
|
+
T-bars from overlapping when the value-axis slot is wide; clamped to the box
|
|
75
|
+
width, `'whisker'` shape only.
|
|
76
|
+
- **Readout labels** carry the series' `as` identity (`iv upper`, `iv median`)
|
|
77
|
+
when set, instead of bare column names — the `as ?? column` convention
|
|
78
|
+
Line/Scatter already use.
|
|
79
|
+
- **`<ScatterChart>` accepts a `ValueSeries`** (`@pond-ts/charts`) — scatter
|
|
80
|
+
marks on the value axis, the same instanceof-branched adapter as
|
|
81
|
+
`<LineChart>` (the container infers the x kind from the data). The
|
|
82
|
+
data-driven `radius` / `color` encodings work unchanged on a value axis;
|
|
83
|
+
the per-point `label` reads through a new columnar branch (a `ValueSeries`
|
|
84
|
+
has no per-row events) — IV marks keyed by strike with open-interest
|
|
85
|
+
radius is the driving composition (vol smile). New value-axis Storybook
|
|
86
|
+
fan-out (`ValueAxis` / `ValueAxisEncoded` / `ValueAxisSmile` /
|
|
87
|
+
`ValueAxisFlag`) + Linux visual baselines.
|
|
88
|
+
|
|
89
|
+
- **`ValueSeries.fromColumns({ name, schema, columns, sort? })`** (`pond-ts`) —
|
|
90
|
+
the direct columnar door into value-land, for data that is _natively_
|
|
91
|
+
value-keyed (cross-sectional): an options chain keyed by strike, a spectrum
|
|
92
|
+
keyed by frequency. Exact `TimeSeries.fromColumns` contract with the axis in
|
|
93
|
+
place of time — same polymorphic `number[]` / `Float64Array` inputs, same
|
|
94
|
+
zero-copy adoption, same stable opt-in `sort`, same gap rule — the two doors
|
|
95
|
+
share one ingest engine. Previously cross-sectional callers had to launder
|
|
96
|
+
the axis through a fake `time` column (`TimeSeries.fromColumns` +
|
|
97
|
+
`byValue`); that detour is no longer needed.
|
|
98
|
+
|
|
99
|
+
- **The region cursor snaps to a histogram's bins** (`@pond-ts/charts`). On a
|
|
100
|
+
`<BarChart>` histogram, `cursor="region"` now snaps **bar by bar** with no
|
|
101
|
+
`cursorSequence`: hovering highlights the bar under the pointer, a drag extends
|
|
102
|
+
across whole bars, and `onRegionSelect` reports the selected bin range
|
|
103
|
+
`[lo, hi]` at the bar edges. The bar layer publishes its `[begin, end)` spans
|
|
104
|
+
(a new internal `binIntervals` channel) as the region cursor's snap buckets —
|
|
105
|
+
the same machinery a `cursorSequence` drives on a time axis, so it also covers a
|
|
106
|
+
time-axis histogram. Only a **vertical** bar layer on a continuous (time /
|
|
107
|
+
value) x axis publishes bins; a horizontal chart (value on x) and a categorical
|
|
108
|
+
axis stay freeform / excluded. An explicit `cursorSequence` still takes
|
|
109
|
+
precedence. New `HistogramBins` region-cursor story.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
|
|
113
|
+
- **BREAKING (`@pond-ts/charts`): the region cursor works on a value x-axis, and
|
|
114
|
+
`onRegionSelect` reports a neutral `[lo, hi]` pair.** The drag-select callback
|
|
115
|
+
fired a `TimeRange`; it now fires `readonly [number, number]` in **axis units** —
|
|
116
|
+
epoch ms on a time axis, the axis value (strike, distance, …) on a value axis —
|
|
117
|
+
mirroring the container's polymorphic `range` input (which never takes the axis
|
|
118
|
+
_kind_ from its value). A time-axis consumer that wants a `TimeRange` builds one
|
|
119
|
+
from the pair (`new TimeRange({ start: lo, end: hi })`). The cursor itself is
|
|
120
|
+
ungated from time-only to any **continuous** x-axis (time **or** value; a
|
|
121
|
+
**category** axis stays excluded — an ordinal-slot select is a different gesture).
|
|
122
|
+
Bucket **snapping** stays time-only (a `cursorSequence` bucket is a time
|
|
123
|
+
interval), so a value axis is always **freeform** (hover line + raw-span drag).
|
|
124
|
+
|
|
125
|
+
## [0.43.0] — 2026-07-11
|
|
126
|
+
|
|
127
|
+
The **categorical x-axis** release: a first-class ordinal band scale (ticker /
|
|
128
|
+
account / expiry on x — the transpose view of a time series), plus the charts
|
|
129
|
+
**interaction** wave that landed after v0.42.0 was cut — the region cursor and
|
|
130
|
+
its drag-to-select gesture, per-bin band colour, and annotation edges that snap
|
|
131
|
+
to session boundaries.
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- **`@pond-ts/charts`: a first-class categorical x-axis.** `<BarChart
|
|
136
|
+
categories={[{ label, value }]}>` draws one bar per category on an ordinal
|
|
137
|
+
**band scale** (the transpose view's "columns on x" — ticker / account / zone
|
|
138
|
+
on x). The container infers `xKind:'category'` and builds a `scaleBand` over the
|
|
139
|
+
labels; `<CategoryAxis>` ticks once per category. Colour per category via
|
|
140
|
+
`binColors`; selection reports the category name. **Negative** category values
|
|
141
|
+
draw below the baseline (the P&L / delta case) — a single-series category bar
|
|
142
|
+
honours its sign. New exports `scaleBand` / `ScaleBand`, `CategoryAxis`,
|
|
143
|
+
`categoryStack` / `CategoryDatum`. Additive — a new x-kind alongside time /
|
|
144
|
+
value; existing charts are unchanged. First slice of the categorical-axis RFC
|
|
145
|
+
(`docs/rfcs/categorical-axis.md`, Phase 1).
|
|
146
|
+
- **`@pond-ts/charts`: `transposeRow`** — read one **row** of a wide `TimeSeries`
|
|
147
|
+
**across** into `{ label, value }[]` for `<BarChart categories>`: the schema's
|
|
148
|
+
numeric columns (a `pivotByGroup` output's per-group columns, a term
|
|
149
|
+
structure's per-expiry columns) become the categories at one instant. Pick the
|
|
150
|
+
row with `at` (`'last'` — the head/live row — by default; `'first'`, an index,
|
|
151
|
+
or `{ time }`); bound / order the set with `columns`.
|
|
152
|
+
- **`@pond-ts/charts`: stable per-column selection identity.** `SelectInfo` gains
|
|
153
|
+
an optional **`mark`** — a stable per-mark identity within a layer. A categorical
|
|
154
|
+
bar reports its **column name** as `mark`, and a controlled `selected` echo /
|
|
155
|
+
the highlight match key on `(id, mark)`, so a pinned selection survives a column
|
|
156
|
+
reorder (the slot index doesn't; the name does). Additive — `mark` is
|
|
157
|
+
`undefined` for a time / value bar (whose sample `key` is already its identity).
|
|
158
|
+
Plus a category-axis **label policy**: a dense axis thins (keeps every k-th) and
|
|
159
|
+
ellipsis-truncates its labels so they stay legible while every bar draws.
|
|
160
|
+
- **`@pond-ts/charts`: region cursor (`cursor="region"`).** A shaded **band**
|
|
161
|
+
highlights the bucket under the pointer, bucketed by a new **`cursorSequence`**
|
|
162
|
+
prop — a `Sequence` (`Sequence.every('15m')`, `Sequence.calendar('week')`)
|
|
163
|
+
realized over the view, or a `BoundedSequence` (a `TradingCalendar`'s
|
|
164
|
+
`sessionSequence()` / `barSequence()`) used as-is. The band maps through the x
|
|
165
|
+
scale, so on a trading-time axis the closed part of a bucket collapses (crops
|
|
166
|
+
to live sessions). Time-axis only (a no-op on a value axis). (#409, #413)
|
|
167
|
+
- **`@pond-ts/charts`: draggable region cursor → one-shot select.** Opt-in
|
|
168
|
+
**`onRegionSelect?: (range: TimeRange) => void`** makes the region cursor
|
|
169
|
+
draggable: the band extends bucket by bucket and fires **once** on release
|
|
170
|
+
with the selected `[start, end)` `TimeRange` (the cursor doesn't keep it —
|
|
171
|
+
typical use is to zoom the view). With **no `cursorSequence`** it degenerates
|
|
172
|
+
to a hover **line** + **freeform** drag. **`regionSelectModifier="shift"`**
|
|
173
|
+
resolves the gesture conflict with `panZoom` (plain drag pans, shift-drag
|
|
174
|
+
selects); omitted, a region-drag preempts pan. (#416)
|
|
175
|
+
- **`@pond-ts/charts`: `binColors` — per-bin colour for single-series bars.**
|
|
176
|
+
`<BarChart binColors={[...]}>` colours each bar/band segment individually (one
|
|
177
|
+
colour per bin, in order), the single-series analog of the stacked `colors`
|
|
178
|
+
prop — used by the category axis (colour per category) and any single-series
|
|
179
|
+
band chart. (#408)
|
|
180
|
+
- **`@pond-ts/charts`: annotation edges snap to session boundaries.** When a
|
|
181
|
+
`<ChartContainer>` carries a trading calendar (disjoint x axis), dragging a
|
|
182
|
+
`<Region>` edge (or creating one) snaps to the nearest **session boundary**
|
|
183
|
+
rather than raw wall-clock, so a drawn span aligns with real market sessions.
|
|
184
|
+
(#410)
|
|
185
|
+
|
|
186
|
+
### Fixed
|
|
187
|
+
|
|
188
|
+
- **`@pond-ts/charts`: region body-move no longer distorts across a session
|
|
189
|
+
boundary.** On a trading-time (discontinuous) axis, dragging a `<Region>`
|
|
190
|
+
annotation by its body now translates it rigidly in pixel space, so the box
|
|
191
|
+
keeps its width as it crosses a collapsed gap (it previously applied one
|
|
192
|
+
value-delta to both edges, which stretched the box in the different
|
|
193
|
+
rate-contexts either side of a session boundary). No-op on a continuous axis.
|
|
194
|
+
(#405)
|
|
195
|
+
|
|
43
196
|
## [0.42.0] — 2026-07-10
|
|
44
197
|
|
|
45
198
|
The **trading-calendar** release: a new `@pond-ts/financial` package (its first
|
|
@@ -82,6 +235,16 @@ collapses closed-market time (weekends, holidays, overnight, lunch breaks).
|
|
|
82
235
|
(`theme.axis.sessionDivider`), aligned with the labels.
|
|
83
236
|
- `Charts/TradingTimeAxis` stories (weekend/holiday/half-day/intraday,
|
|
84
237
|
continuous-vs-trading, daily-months, proportional-vs-uniform).
|
|
238
|
+
- **`@pond-ts/charts`: first-class histograms.** `<BarChart>` gains **stacking**
|
|
239
|
+
— a group-by dimension → stacked segments, from a wide series (`columns`), a
|
|
240
|
+
`Map<group, TimeSeries>` (the `partitionBy().aggregate().toMap()` shape), or a
|
|
241
|
+
`byColumn` `bins` array; per-group colour via `colors` or theme roles — and an
|
|
242
|
+
**`orientation`** prop (`'vertical'` default | `'horizontal'`, bars grow right
|
|
243
|
+
with the bins on a y band axis). New readers `stacksFromGroups` /
|
|
244
|
+
`stacksFromColumns` / `stacksFromBins` plus `StackedBarSeries` / `BinRecord` /
|
|
245
|
+
`Orientation` types. All data generation composes from existing operators
|
|
246
|
+
(`aggregate` / `byColumn` / `partitionBy`); no core changes. Guide: How-to
|
|
247
|
+
guides → Histograms. (#401)
|
|
85
248
|
- **`@pond-ts/charts`:** selection now has a stable series identity. `SelectInfo`
|
|
86
249
|
carries an `id`, and `BarChart` / `ScatterChart` take an optional `id` prop —
|
|
87
250
|
the series identity used for selection + hover. An `id` **gates interactivity**:
|
|
@@ -0,0 +1,33 @@
|
|
|
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>;
|
|
4
|
+
/**
|
|
5
|
+
* The shared columnar-ingress engine behind `TimeSeries.fromColumns` and
|
|
6
|
+
* `ValueSeries.fromColumns`. Both doors are the same machine — normalize the
|
|
7
|
+
* key column (adopt a `Float64Array` zero-copy, convert a `number[]`),
|
|
8
|
+
* 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`).
|
|
15
|
+
*
|
|
16
|
+
* `makeKey` runs **before** the ordering scan, matching the original inline
|
|
17
|
+
* order of checks: a non-finite key fails in the key-column constructor first,
|
|
18
|
+
* ordering second. The per-element loops are the measured hot path
|
|
19
|
+
* (`scripts/perf-from-columns.mjs`) — moved here verbatim, including the
|
|
20
|
+
* manual copy loops (see the inline notes on why not `Float64Array.from`).
|
|
21
|
+
*
|
|
22
|
+
* The caller owns what stays door-specific: the schema[0] kind gate (with its
|
|
23
|
+
* own message) and wrapping the returned store in its series type.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ingestColumnsToStore(input: {
|
|
26
|
+
op: string;
|
|
27
|
+
keyNoun: string;
|
|
28
|
+
schema: ColumnSchema;
|
|
29
|
+
columns: RawColumns;
|
|
30
|
+
sort: boolean;
|
|
31
|
+
makeKey: (begin: Float64Array, count: number) => KeyColumn;
|
|
32
|
+
}): ColumnarStore<ColumnSchema>;
|
|
33
|
+
//# sourceMappingURL=ingest-columns.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ColumnarStore, Float64Column, validityFromPredicate, } from '../../columnar/index.js';
|
|
2
|
+
import { ValidationError } from '../../core/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* The shared columnar-ingress engine behind `TimeSeries.fromColumns` and
|
|
5
|
+
* `ValueSeries.fromColumns`. Both doors are the same machine — normalize the
|
|
6
|
+
* key column (adopt a `Float64Array` zero-copy, convert a `number[]`),
|
|
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`).
|
|
14
|
+
*
|
|
15
|
+
* `makeKey` runs **before** the ordering scan, matching the original inline
|
|
16
|
+
* order of checks: a non-finite key fails in the key-column constructor first,
|
|
17
|
+
* ordering second. The per-element loops are the measured hot path
|
|
18
|
+
* (`scripts/perf-from-columns.mjs`) — moved here verbatim, including the
|
|
19
|
+
* manual copy loops (see the inline notes on why not `Float64Array.from`).
|
|
20
|
+
*
|
|
21
|
+
* The caller owns what stays door-specific: the schema[0] kind gate (with its
|
|
22
|
+
* own message) and wrapping the returned store in its series type.
|
|
23
|
+
*/
|
|
24
|
+
export function ingestColumnsToStore(input) {
|
|
25
|
+
const { op, keyNoun, schema, columns, sort, makeKey } = input;
|
|
26
|
+
const keyDef = schema[0];
|
|
27
|
+
if (keyDef === undefined) {
|
|
28
|
+
throw new ValidationError(`${op}: schema must have at least a key column`);
|
|
29
|
+
}
|
|
30
|
+
const keyRaw = columns[keyDef.name];
|
|
31
|
+
if (keyRaw === undefined) {
|
|
32
|
+
throw new ValidationError(`${op}: missing key column '${keyDef.name}'`);
|
|
33
|
+
}
|
|
34
|
+
// Key buffer; the key-column constructor asserts all finite. A manual loop,
|
|
35
|
+
// not `Float64Array.from(arr, mapFn)`: supplying a map function forces V8's
|
|
36
|
+
// generic iterable-protocol path even for a plain array, ~15-20x slower
|
|
37
|
+
// than a preallocated-buffer copy at 100k-element scale — measured, not
|
|
38
|
+
// theoretical (see the pond-columnar-ingest spike's ingest regression).
|
|
39
|
+
let rawBegin;
|
|
40
|
+
if (keyRaw instanceof Float64Array) {
|
|
41
|
+
rawBegin = keyRaw;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
rawBegin = new Float64Array(keyRaw.length);
|
|
45
|
+
for (let j = 0; j < keyRaw.length; j += 1) {
|
|
46
|
+
const v = keyRaw[j];
|
|
47
|
+
rawBegin[j] = v == null ? NaN : Number(v);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const count = rawBegin.length;
|
|
51
|
+
// `sort: true` — reorder every column by ascending key before construction.
|
|
52
|
+
// Compute the row permutation once (a stable sort of the index array; V8's
|
|
53
|
+
// Array.sort is stable, so equal keys keep input order, matching fromJSON's
|
|
54
|
+
// stable intake), then remap the key + each value column through it below.
|
|
55
|
+
// `order` stays null on the (default) trusted fast path, so no allocation /
|
|
56
|
+
// copy is paid unless asked. A non-finite key is left for the key column's
|
|
57
|
+
// constructor to reject — sorting can't make it valid.
|
|
58
|
+
let begin;
|
|
59
|
+
let order = null;
|
|
60
|
+
if (sort) {
|
|
61
|
+
const idx = Array.from({ length: count }, (_, i) => i);
|
|
62
|
+
idx.sort((a, b) => rawBegin[a] - rawBegin[b]);
|
|
63
|
+
order = Uint32Array.from(idx);
|
|
64
|
+
begin = new Float64Array(count);
|
|
65
|
+
for (let j = 0; j < count; j += 1)
|
|
66
|
+
begin[j] = rawBegin[order[j]];
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
begin = rawBegin;
|
|
70
|
+
}
|
|
71
|
+
// Throws on any non-finite key value.
|
|
72
|
+
const keys = makeKey(begin, count);
|
|
73
|
+
// Enforce the non-decreasing-key invariant that `fromJSON`'s
|
|
74
|
+
// `validateAndNormalize` guarantees. Trusted construction skips row
|
|
75
|
+
// materialization + kind re-validation, but NOT this correctness contract:
|
|
76
|
+
// bisect-based operators (crop, `atTime`, range queries) rely on it, so an
|
|
77
|
+
// unsorted columnar input must fail loudly here rather than build a silently
|
|
78
|
+
// broken series. One O(N) scan over already-finite values — negligible next
|
|
79
|
+
// to decode. (When `sort` is set the keys are now non-decreasing, so this is
|
|
80
|
+
// a cheap post-condition check rather than a rejection.)
|
|
81
|
+
for (let j = 1; j < count; j += 1) {
|
|
82
|
+
if (begin[j] < begin[j - 1]) {
|
|
83
|
+
throw new ValidationError(`${op}: key column '${keyDef.name}' is out of order at index ${j} ` +
|
|
84
|
+
`(${begin[j]} < ${begin[j - 1]}) — ${keyNoun} must be non-decreasing; ` +
|
|
85
|
+
`pass { sort: true } or pre-sort the columns`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Value columns — packed directly (missing-aware) from the arrays.
|
|
89
|
+
const columnMap = new Map();
|
|
90
|
+
for (let i = 1; i < schema.length; i += 1) {
|
|
91
|
+
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}'`);
|
|
94
|
+
}
|
|
95
|
+
const raw = columns[def.name];
|
|
96
|
+
if (raw === undefined) {
|
|
97
|
+
throw new ValidationError(`${op}: missing column '${def.name}'`);
|
|
98
|
+
}
|
|
99
|
+
if (raw.length !== count) {
|
|
100
|
+
throw new ValidationError(`${op}: column '${def.name}' length ${raw.length} does not match key length ${count}`);
|
|
101
|
+
}
|
|
102
|
+
// Normalize to a Float64Array either way — adopt if already typed (the
|
|
103
|
+
// fast path a protobuf / fixed-point decoder hits, zero-copy), else
|
|
104
|
+
// convert (`null`/`undefined` -> `NaN`) — then apply ONE validity rule
|
|
105
|
+
// to both: a cell is a gap iff it's non-finite. This must be identical
|
|
106
|
+
// regardless of input type: an earlier version used `float64ColumnFromArray`
|
|
107
|
+
// for the `number[]` branch, which treats a `NaN` *value* (as opposed to
|
|
108
|
+
// `null`) as defined-but-non-finite rather than missing, diverging from
|
|
109
|
+
// the `Float64Array` branch's `Number.isFinite` gap signal — the same
|
|
110
|
+
// wire value would silently mean different things depending on which
|
|
111
|
+
// array type decoded it.
|
|
112
|
+
// Manual loop, not `Float64Array.from(arr, mapFn)` — see the key-column
|
|
113
|
+
// comment above; the cost applies identically here.
|
|
114
|
+
let values;
|
|
115
|
+
if (order !== null) {
|
|
116
|
+
// Sorting: reorder into a fresh buffer through the key permutation (no
|
|
117
|
+
// zero-copy adoption — the rows are being moved). Same missing rule
|
|
118
|
+
// (`null`/`undefined` → NaN) applied while remapping.
|
|
119
|
+
values = new Float64Array(count);
|
|
120
|
+
if (raw instanceof Float64Array) {
|
|
121
|
+
for (let j = 0; j < count; j += 1)
|
|
122
|
+
values[j] = raw[order[j]];
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
for (let j = 0; j < count; j += 1) {
|
|
126
|
+
const v = raw[order[j]];
|
|
127
|
+
values[j] = v == null ? NaN : v;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (raw instanceof Float64Array) {
|
|
132
|
+
values = raw;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
values = new Float64Array(count);
|
|
136
|
+
for (let j = 0; j < count; j += 1) {
|
|
137
|
+
const v = raw[j];
|
|
138
|
+
values[j] = v == null ? NaN : v;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const validity = validityFromPredicate(count, (j) => Number.isFinite(values[j]));
|
|
142
|
+
columnMap.set(def.name, new Float64Column(values, count, validity, validity === undefined));
|
|
143
|
+
}
|
|
144
|
+
return ColumnarStore.fromTrustedStore(schema, keys, columnMap);
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=ingest-columns.js.map
|
|
@@ -131,9 +131,10 @@ export declare class TimeSeries<S extends SeriesSchema> {
|
|
|
131
131
|
* value columns throw for now — extend as consumers need.
|
|
132
132
|
*
|
|
133
133
|
* @throws ValidationError on a missing column, a length mismatch, an
|
|
134
|
-
* unsupported kind,
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* unsupported kind, or an out-of-order (decreasing) timestamp when `sort`
|
|
135
|
+
* is not set — keys must be non-decreasing, same as `fromJSON`. Throws
|
|
136
|
+
* RangeError on a non-finite timestamp key (from the key-column
|
|
137
|
+
* constructor) or a duplicate column name.
|
|
137
138
|
*/
|
|
138
139
|
static fromColumns<S extends SeriesSchema>(input: {
|
|
139
140
|
name: string;
|
|
@@ -2,6 +2,7 @@ import { isAggregateOutputSpec, normalizeAggregateColumns, tryAggregateColumnarT
|
|
|
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
|
+
import { ingestColumnsToStore } from './operators/ingest-columns.js';
|
|
5
6
|
import { ValueSeries } from './value-series.js';
|
|
6
7
|
import { diffRateOp } from './operators/diff-rate.js';
|
|
7
8
|
import { fillOp } from './operators/fill.js';
|
|
@@ -18,7 +19,7 @@ import { TimeRange } from '../core/time-range.js';
|
|
|
18
19
|
import { compareEventKeys } from '../core/temporal.js';
|
|
19
20
|
import { PartitionedTimeSeries } from './partitioned-time-series.js';
|
|
20
21
|
import { Sequence } from '../sequence/sequence.js';
|
|
21
|
-
import { ColumnarStore,
|
|
22
|
+
import { ColumnarStore, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, float64ColumnFromArray, stringColumnFromArray, withColumnAppended, withColumnsRenamed, withColumnsSelected, withKeyColumn, withRowRange, withRowSelection, } from '../columnar/index.js';
|
|
22
23
|
import { ValidationError } from '../core/errors.js';
|
|
23
24
|
import { SeriesStore } from '../live/series-store.js';
|
|
24
25
|
import { parseDuration } from '../core/duration.js';
|
|
@@ -530,9 +531,10 @@ export class TimeSeries {
|
|
|
530
531
|
* value columns throw for now — extend as consumers need.
|
|
531
532
|
*
|
|
532
533
|
* @throws ValidationError on a missing column, a length mismatch, an
|
|
533
|
-
* unsupported kind,
|
|
534
|
-
*
|
|
535
|
-
*
|
|
534
|
+
* unsupported kind, or an out-of-order (decreasing) timestamp when `sort`
|
|
535
|
+
* is not set — keys must be non-decreasing, same as `fromJSON`. Throws
|
|
536
|
+
* RangeError on a non-finite timestamp key (from the key-column
|
|
537
|
+
* constructor) or a duplicate column name.
|
|
536
538
|
*/
|
|
537
539
|
static fromColumns(input) {
|
|
538
540
|
const { name, schema, columns, sort = false } = input;
|
|
@@ -544,121 +546,17 @@ export class TimeSeries {
|
|
|
544
546
|
if (keyDef.kind !== 'time') {
|
|
545
547
|
throw new ValidationError(`fromColumns: v1 supports a 'time' key; schema[0] '${keyDef.name}' is '${keyDef.kind}'`);
|
|
546
548
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
rawBegin = keyRaw;
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
rawBegin = new Float64Array(keyRaw.length);
|
|
562
|
-
for (let j = 0; j < keyRaw.length; j += 1) {
|
|
563
|
-
const v = keyRaw[j];
|
|
564
|
-
rawBegin[j] = v == null ? NaN : Number(v);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
const count = rawBegin.length;
|
|
568
|
-
// `sort: true` — reorder every column by ascending key before construction.
|
|
569
|
-
// Compute the row permutation once (a stable sort of the index array; V8's
|
|
570
|
-
// Array.sort is stable, so equal keys keep input order, matching fromJSON's
|
|
571
|
-
// stable intake), then remap the key + each value column through it below.
|
|
572
|
-
// `order` stays null on the (default) trusted fast path, so no allocation /
|
|
573
|
-
// copy is paid unless asked. A non-finite key is left for `TimeKeyColumn` to
|
|
574
|
-
// reject — sorting can't make it valid.
|
|
575
|
-
let begin;
|
|
576
|
-
let order = null;
|
|
577
|
-
if (sort) {
|
|
578
|
-
const idx = Array.from({ length: count }, (_, i) => i);
|
|
579
|
-
idx.sort((a, b) => rawBegin[a] - rawBegin[b]);
|
|
580
|
-
order = Uint32Array.from(idx);
|
|
581
|
-
begin = new Float64Array(count);
|
|
582
|
-
for (let j = 0; j < count; j += 1)
|
|
583
|
-
begin[j] = rawBegin[order[j]];
|
|
584
|
-
}
|
|
585
|
-
else {
|
|
586
|
-
begin = rawBegin;
|
|
587
|
-
}
|
|
588
|
-
// Throws on any non-finite timestamp.
|
|
589
|
-
const keys = new TimeKeyColumn(begin, count);
|
|
590
|
-
// Enforce the non-decreasing-key invariant that `fromJSON`'s
|
|
591
|
-
// `validateAndNormalize` guarantees. Trusted construction skips row
|
|
592
|
-
// materialization + kind re-validation, but NOT this correctness contract:
|
|
593
|
-
// bisect-based operators (crop, `atTime`, range queries) rely on it, so an
|
|
594
|
-
// unsorted columnar input must fail loudly here rather than build a silently
|
|
595
|
-
// broken series. One O(N) scan over already-finite values — negligible next
|
|
596
|
-
// to decode. (When `sort` is set the keys are now non-decreasing, so this is
|
|
597
|
-
// a cheap post-condition check rather than a rejection.)
|
|
598
|
-
for (let j = 1; j < count; j += 1) {
|
|
599
|
-
if (begin[j] < begin[j - 1]) {
|
|
600
|
-
throw new ValidationError(`fromColumns: key column '${keyDef.name}' is out of order at index ${j} ` +
|
|
601
|
-
`(${begin[j]} < ${begin[j - 1]}) — timestamps must be non-decreasing; ` +
|
|
602
|
-
`pass { sort: true } or pre-sort the columns`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
// Value columns — packed directly (missing-aware) from the arrays.
|
|
606
|
-
const columnMap = new Map();
|
|
607
|
-
for (let i = 1; i < schema.length; i += 1) {
|
|
608
|
-
const def = schema[i];
|
|
609
|
-
if (def.kind !== 'number') {
|
|
610
|
-
throw new ValidationError(`fromColumns: v1 supports 'number' value columns; column '${def.name}' is '${def.kind}'`);
|
|
611
|
-
}
|
|
612
|
-
const raw = columns[def.name];
|
|
613
|
-
if (raw === undefined) {
|
|
614
|
-
throw new ValidationError(`fromColumns: missing column '${def.name}'`);
|
|
615
|
-
}
|
|
616
|
-
if (raw.length !== count) {
|
|
617
|
-
throw new ValidationError(`fromColumns: column '${def.name}' length ${raw.length} does not match key length ${count}`);
|
|
618
|
-
}
|
|
619
|
-
// Normalize to a Float64Array either way — adopt if already typed (the
|
|
620
|
-
// fast path a protobuf / fixed-point decoder hits, zero-copy), else
|
|
621
|
-
// convert (`null`/`undefined` -> `NaN`) — then apply ONE validity rule
|
|
622
|
-
// to both: a cell is a gap iff it's non-finite. This must be identical
|
|
623
|
-
// regardless of input type: an earlier version used `float64ColumnFromArray`
|
|
624
|
-
// for the `number[]` branch, which treats a `NaN` *value* (as opposed to
|
|
625
|
-
// `null`) as defined-but-non-finite rather than missing, diverging from
|
|
626
|
-
// the `Float64Array` branch's `Number.isFinite` gap signal — the same
|
|
627
|
-
// wire value would silently mean different things depending on which
|
|
628
|
-
// array type decoded it.
|
|
629
|
-
// Manual loop, not `Float64Array.from(arr, mapFn)` — see the key-column
|
|
630
|
-
// comment above; the cost applies identically here.
|
|
631
|
-
let values;
|
|
632
|
-
if (order !== null) {
|
|
633
|
-
// Sorting: reorder into a fresh buffer through the key permutation (no
|
|
634
|
-
// zero-copy adoption — the rows are being moved). Same missing rule
|
|
635
|
-
// (`null`/`undefined` → NaN) applied while remapping.
|
|
636
|
-
values = new Float64Array(count);
|
|
637
|
-
if (raw instanceof Float64Array) {
|
|
638
|
-
for (let j = 0; j < count; j += 1)
|
|
639
|
-
values[j] = raw[order[j]];
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
for (let j = 0; j < count; j += 1) {
|
|
643
|
-
const v = raw[order[j]];
|
|
644
|
-
values[j] = v == null ? NaN : v;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
else if (raw instanceof Float64Array) {
|
|
649
|
-
values = raw;
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
values = new Float64Array(count);
|
|
653
|
-
for (let j = 0; j < count; j += 1) {
|
|
654
|
-
const v = raw[j];
|
|
655
|
-
values[j] = v == null ? NaN : v;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
const validity = validityFromPredicate(count, (j) => Number.isFinite(values[j]));
|
|
659
|
-
columnMap.set(def.name, new Float64Column(values, count, validity, validity === undefined));
|
|
660
|
-
}
|
|
661
|
-
const store = ColumnarStore.fromTrustedStore(schema, keys, columnMap);
|
|
549
|
+
// The normalize -> sort -> key -> order-check -> pack pipeline is shared
|
|
550
|
+
// with `ValueSeries.fromColumns` -- one ingest engine, two key kinds (see
|
|
551
|
+
// `operators/ingest-columns.ts`).
|
|
552
|
+
const store = ingestColumnsToStore({
|
|
553
|
+
op: 'fromColumns',
|
|
554
|
+
keyNoun: 'timestamps',
|
|
555
|
+
schema: schema,
|
|
556
|
+
columns,
|
|
557
|
+
sort,
|
|
558
|
+
makeKey: (begin, count) => new TimeKeyColumn(begin, count),
|
|
559
|
+
});
|
|
662
560
|
return TimeSeries.#fromTrustedStore(name, schema, store);
|
|
663
561
|
}
|
|
664
562
|
/**
|
|
@@ -3,7 +3,12 @@ import type { ValueSeriesColumnName, ValueSeriesSchema } from '../schema/index.j
|
|
|
3
3
|
/**
|
|
4
4
|
* A **value-keyed series** — the closed value-axis counterpart of
|
|
5
5
|
* `TimeSeries`. Its key is a monotonic non-time axis (distance, cumulative
|
|
6
|
-
* work, …)
|
|
6
|
+
* work, …). Two doors in: **project** a `TimeSeries` onto one of its monotonic
|
|
7
|
+
* columns (`TimeSeries.byValue(axis)` — a track re-keyed by cumulative
|
|
8
|
+
* distance), or **construct directly** from columnar arrays
|
|
9
|
+
* ({@link ValueSeries.fromColumns}) when the data is natively value-keyed and
|
|
10
|
+
* never had a meaningful time key per row — cross-sectional data such as an
|
|
11
|
+
* options chain keyed by strike or a spectrum keyed by frequency.
|
|
7
12
|
*
|
|
8
13
|
* `ValueSeries` carries the **ordering-based** operators (read the axis, read
|
|
9
14
|
* value columns, nearest-by-value, slice-by-value) — the part of the series
|
|
@@ -21,6 +26,57 @@ export declare class ValueSeries<VS extends ValueSeriesSchema> {
|
|
|
21
26
|
#private;
|
|
22
27
|
readonly name: string;
|
|
23
28
|
readonly schema: VS;
|
|
29
|
+
/**
|
|
30
|
+
* Example: `ValueSeries.fromColumns({ name, schema, columns })`.
|
|
31
|
+
*
|
|
32
|
+
* The **direct columnar door** into value-land — for data that is *natively*
|
|
33
|
+
* value-keyed and never had a meaningful per-row time key: an options chain
|
|
34
|
+
* keyed by strike, a spectrum keyed by frequency, a profile keyed by depth.
|
|
35
|
+
* (Data that starts life time-keyed projects in via `TimeSeries.byValue`
|
|
36
|
+
* instead; before this door existed, cross-sectional callers had to launder
|
|
37
|
+
* their axis through a fake `time` column just to reach
|
|
38
|
+
* `TimeSeries.fromColumns` + `byValue`.)
|
|
39
|
+
*
|
|
40
|
+
* The exact `TimeSeries.fromColumns` contract, with the axis in place of
|
|
41
|
+
* time — the two doors share one ingest engine. `schema[0]` is the
|
|
42
|
+
* `'value'`-kind **axis** column; each `columns` entry is one column's
|
|
43
|
+
* values, keyed by schema column name and aligned by index. Values may be a
|
|
44
|
+
* plain `number[]` **or** a `Float64Array`; a value cell is a gap (missing)
|
|
45
|
+
* iff it's `null`/`undefined` or non-finite — identical rule for both input
|
|
46
|
+
* types.
|
|
47
|
+
*
|
|
48
|
+
* **`Float64Array` inputs are adopted, not copied** (zero-copy): the
|
|
49
|
+
* resulting series' columns alias the caller's buffers; pass a fresh buffer
|
|
50
|
+
* if that matters. (**`sort` disables the adoption** — a reorder needs its
|
|
51
|
+
* own buffers.)
|
|
52
|
+
*
|
|
53
|
+
* **Ordering.** The axis must be **defined, finite, and non-decreasing** —
|
|
54
|
+
* it becomes the index (the same contract `byValue` enforces with
|
|
55
|
+
* `assertMonotonicAxis`), so an out-of-order axis throws by default. Pass
|
|
56
|
+
* **`sort: true`** to sort the rows by axis value before construction — the
|
|
57
|
+
* stable sort every unordered snapshot wants (e.g. a keyed live feed that
|
|
58
|
+
* delivers rows in update order, not axis order).
|
|
59
|
+
*
|
|
60
|
+
* **v1 scope:** `number` value columns, matching `TimeSeries.fromColumns`.
|
|
61
|
+
*
|
|
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
|
|
64
|
+
* when `sort` is not set. Throws RangeError on a non-finite
|
|
65
|
+
* (`null`/`NaN`/`±Infinity`) axis cell — sorting can't make it valid — or
|
|
66
|
+
* a duplicate column name (the axis name repeated among the value columns).
|
|
67
|
+
*/
|
|
68
|
+
static fromColumns<VS extends ValueSeriesSchema>(input: {
|
|
69
|
+
name: string;
|
|
70
|
+
schema: VS;
|
|
71
|
+
columns: Record<string, ReadonlyArray<number | null | undefined> | Float64Array>;
|
|
72
|
+
/**
|
|
73
|
+
* Sort the rows by axis value before construction (off by default), for a
|
|
74
|
+
* payload whose rows aren't guaranteed ordered. Stable; disables the
|
|
75
|
+
* `Float64Array` zero-copy adoption (columns are reordered into fresh
|
|
76
|
+
* buffers).
|
|
77
|
+
*/
|
|
78
|
+
sort?: boolean;
|
|
79
|
+
}): ValueSeries<VS>;
|
|
24
80
|
private constructor();
|
|
25
81
|
/** Number of rows. */
|
|
26
82
|
get length(): number;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { withRowRange, } from '../columnar/index.js';
|
|
1
|
+
import { ValueKeyColumn, withRowRange, } from '../columnar/index.js';
|
|
2
|
+
import { ValidationError } from '../core/errors.js';
|
|
3
|
+
import { ingestColumnsToStore } from './operators/ingest-columns.js';
|
|
2
4
|
/**
|
|
3
5
|
* A **value-keyed series** — the closed value-axis counterpart of
|
|
4
6
|
* `TimeSeries`. Its key is a monotonic non-time axis (distance, cumulative
|
|
5
|
-
* work, …)
|
|
7
|
+
* work, …). Two doors in: **project** a `TimeSeries` onto one of its monotonic
|
|
8
|
+
* columns (`TimeSeries.byValue(axis)` — a track re-keyed by cumulative
|
|
9
|
+
* distance), or **construct directly** from columnar arrays
|
|
10
|
+
* ({@link ValueSeries.fromColumns}) when the data is natively value-keyed and
|
|
11
|
+
* never had a meaningful time key per row — cross-sectional data such as an
|
|
12
|
+
* options chain keyed by strike or a spectrum keyed by frequency.
|
|
6
13
|
*
|
|
7
14
|
* `ValueSeries` carries the **ordering-based** operators (read the axis, read
|
|
8
15
|
* value columns, nearest-by-value, slice-by-value) — the part of the series
|
|
@@ -23,11 +30,70 @@ export class ValueSeries {
|
|
|
23
30
|
/**
|
|
24
31
|
* @internal Trusted construction — `store` must be value-keyed and structurally
|
|
25
32
|
* match `schema` (the invariant `TimeSeries.byValue` / `byValueOp` establish).
|
|
26
|
-
* Not for general use; construct a `ValueSeries` via `TimeSeries.byValue
|
|
33
|
+
* Not for general use; construct a `ValueSeries` via `TimeSeries.byValue` or
|
|
34
|
+
* {@link ValueSeries.fromColumns}.
|
|
27
35
|
*/
|
|
28
36
|
static fromTrustedStore(name, schema, store) {
|
|
29
37
|
return new ValueSeries(name, schema, store);
|
|
30
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Example: `ValueSeries.fromColumns({ name, schema, columns })`.
|
|
41
|
+
*
|
|
42
|
+
* The **direct columnar door** into value-land — for data that is *natively*
|
|
43
|
+
* value-keyed and never had a meaningful per-row time key: an options chain
|
|
44
|
+
* keyed by strike, a spectrum keyed by frequency, a profile keyed by depth.
|
|
45
|
+
* (Data that starts life time-keyed projects in via `TimeSeries.byValue`
|
|
46
|
+
* instead; before this door existed, cross-sectional callers had to launder
|
|
47
|
+
* their axis through a fake `time` column just to reach
|
|
48
|
+
* `TimeSeries.fromColumns` + `byValue`.)
|
|
49
|
+
*
|
|
50
|
+
* The exact `TimeSeries.fromColumns` contract, with the axis in place of
|
|
51
|
+
* time — the two doors share one ingest engine. `schema[0]` is the
|
|
52
|
+
* `'value'`-kind **axis** column; each `columns` entry is one column's
|
|
53
|
+
* values, keyed by schema column name and aligned by index. Values may be a
|
|
54
|
+
* plain `number[]` **or** a `Float64Array`; a value cell is a gap (missing)
|
|
55
|
+
* iff it's `null`/`undefined` or non-finite — identical rule for both input
|
|
56
|
+
* types.
|
|
57
|
+
*
|
|
58
|
+
* **`Float64Array` inputs are adopted, not copied** (zero-copy): the
|
|
59
|
+
* resulting series' columns alias the caller's buffers; pass a fresh buffer
|
|
60
|
+
* if that matters. (**`sort` disables the adoption** — a reorder needs its
|
|
61
|
+
* own buffers.)
|
|
62
|
+
*
|
|
63
|
+
* **Ordering.** The axis must be **defined, finite, and non-decreasing** —
|
|
64
|
+
* it becomes the index (the same contract `byValue` enforces with
|
|
65
|
+
* `assertMonotonicAxis`), so an out-of-order axis throws by default. Pass
|
|
66
|
+
* **`sort: true`** to sort the rows by axis value before construction — the
|
|
67
|
+
* stable sort every unordered snapshot wants (e.g. a keyed live feed that
|
|
68
|
+
* delivers rows in update order, not axis order).
|
|
69
|
+
*
|
|
70
|
+
* **v1 scope:** `number` value columns, matching `TimeSeries.fromColumns`.
|
|
71
|
+
*
|
|
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
|
|
74
|
+
* when `sort` is not set. Throws RangeError on a non-finite
|
|
75
|
+
* (`null`/`NaN`/`±Infinity`) axis cell — sorting can't make it valid — or
|
|
76
|
+
* a duplicate column name (the axis name repeated among the value columns).
|
|
77
|
+
*/
|
|
78
|
+
static fromColumns(input) {
|
|
79
|
+
const { name, schema, columns, sort = false } = input;
|
|
80
|
+
const keyDef = schema[0];
|
|
81
|
+
if (keyDef === undefined) {
|
|
82
|
+
throw new ValidationError('ValueSeries.fromColumns: schema must have at least an axis column');
|
|
83
|
+
}
|
|
84
|
+
if (keyDef.kind !== 'value') {
|
|
85
|
+
throw new ValidationError(`ValueSeries.fromColumns: schema[0] '${keyDef.name}' must be the 'value'-kind axis column; got '${keyDef.kind}'`);
|
|
86
|
+
}
|
|
87
|
+
const store = ingestColumnsToStore({
|
|
88
|
+
op: 'ValueSeries.fromColumns',
|
|
89
|
+
keyNoun: 'axis values',
|
|
90
|
+
schema: schema,
|
|
91
|
+
columns,
|
|
92
|
+
sort,
|
|
93
|
+
makeKey: (begin, count) => new ValueKeyColumn(begin, count),
|
|
94
|
+
});
|
|
95
|
+
return new ValueSeries(name, schema, store);
|
|
96
|
+
}
|
|
31
97
|
constructor(name, schema, store) {
|
|
32
98
|
this.name = name;
|
|
33
99
|
this.schema = Object.freeze(schema.slice());
|