A value-keyed series — the closed value-axis counterpart of
TimeSeries. Its key is a monotonic non-time axis (distance, cumulative
work, …).
In. One door projects — TimeSeries.byValue(axis), a track re-keyed
by the cumulative-distance column it already carries. The other three
construct directly, for data that is natively value-keyed and never had
a meaningful time key per row (an options chain keyed by strike, a spectrum
keyed by frequency), one per shape the data arrives in:
ValueSeries.fromJSON (row tuples / objects),
ValueSeries.fromColumns (struct-of-arrays), and
ValueSeries.fromArrow (a decoded Apache Arrow Table). All three
share one ingest engine, so the axis contract, sort, and the packing rules
are identical whichever you use.
Out. The mirror image, so a ValueSeries is never a dead end:
ValueSeries.toJSON / ValueSeries.toRows /
ValueSeries.toObjects (rows), ValueSeries.toColumns
(columnar JSON), ValueSeries.toArrow (Arrow's memory layout, no
copy). Each export pairs with the matching ingress: fromX(series.toX())
reconstructs the series — for the number / string columns the ingest
engine carries, which is every series built through a direct door. The one
exception is a boolean / array column arriving by byValue projection: it
exports on every door but no door takes it back (fromColumns refuses at
compile time, fromJSON throws at ingest, naming the column).
ValueSeries carries the ordering-based operators (read the axis, read
value columns, nearest-by-value, slice-by-value) — the part of the series
algebra that was never really about time (RFC value-axis.md §5). The
calendar/clock operators (Sequence.every, tz formatting) are deliberately
absent: a value axis has no wall-clock semantics, and the disjoint
ValueSeriesSchema makes them type-impossible here.
Minimal by design (RFC §7: adopt the type early, grow the algebra as a second
value-axis consumer earns it). Wraps the columnar store directly — a value
row is an (axis, …values) tuple, not a Time-keyed Event, so it does not
go through the time-only SeriesStore / EventKey layer.
Properties
The axis (key) column's name — e.g. 'cumDist'.
Number of rows.
Static methods
fromArrowstatic
fromArrow(table: ArrowTableLike, options: FromArrowValueOptions): ValueSeries<VS>
ValueSeries.fromArrow(tableFromIPC(bytes), { axis: 'strike' })Build a value-keyed series from a decoded Apache Arrow Table — the
counterpart of TimeSeries.fromArrow, and the door a cross-sectional
payload most often arrives at, since an options chain or a spectrum
reaching you over Arrow has no time column to key on in the first place.
pond does not depend on apache-arrow: bring your own
(tableFromIPC(...) / tableFromArrays(...)) and hand the Table here;
the input is duck-typed against the small ArrowTableLike slice we
read. Ingest is the zero-copy path — a single-chunk Float64 column's
backing Float64Array is adopted as-is, nulls and all (Arrow's validity
bitmap is bit-identical to pond's).
Column handling:
- Axis — named by
axis, which is required: unlike the time door there is no conventional field name to fall back on. Numeric columns only, taken at face value (no unit scaling — an axis has noTimeUnit); int64 recombines BigInt-free. A null in the axis throws. - Value columns — every non-axis field by default, or the subset named
by
columns(in order). The readable Arrow types are exactly thoseTimeSeries.fromArrowlists —Int,Float32/Float64,Date,Time,Timestamp,Utf8/Utf8View,Null, and aDictionaryof any of those — checked against each field's declared type, so anything else (Decimal,Float16,Bool, list/struct/…) is refused by name rather than misread.
The rows must be axis-ordered (as every door requires); pass
{ sort: true } for an unordered table (which disables the adoption).
Trust contract on the type parameter: the runtime schema is derived
from the Arrow fields. Supplying VS (fromArrow<MySchema>(...)) is a
downstream-typing convenience taken on trust — pond does not verify the
Arrow fields match VS, exactly as TimeSeries.fromArrow does.
fromColumnsstatic
fromColumns(input: ValueSeriesColumnarInput<VS> & { sort?: boolean }): ValueSeries<VS>
ValueSeries.fromColumns({ name, schema, columns })The direct columnar door into value-land — for data that is natively
value-keyed and never had a meaningful per-row time key: an options chain
keyed by strike, a spectrum keyed by frequency, a profile keyed by depth.
(Data that starts life time-keyed projects in via TimeSeries.byValue
instead; before this door existed, cross-sectional callers had to launder
their axis through a fake time column just to reach
TimeSeries.fromColumns + byValue.)
The exact TimeSeries.fromColumns contract, with the axis in place of
time — the two doors share one ingest engine. schema[0] is the
'value'-kind axis column; each columns entry is one column's
values, keyed by schema column name and aligned by index. Values may be a
plain number[] or a Float64Array; a value cell is a gap (missing)
iff it's null/undefined or non-finite — identical rule for both input
types.
Float64Array inputs are adopted, not copied (zero-copy): the
resulting series' columns alias the caller's buffers; pass a fresh buffer
if that matters. (sort disables the adoption — a reorder needs its
own buffers.)
Ordering. The axis must be defined, finite, and non-decreasing —
it becomes the index (the same contract byValue enforces with
assertMonotonicAxis), so an out-of-order axis throws by default. Pass
sort: true to sort the rows by axis value before construction — the
stable sort every unordered snapshot wants (e.g. a keyed live feed that
delivers rows in update order, not axis order).
Value columns: number and string, matching TimeSeries.fromColumns.
fromJSONstatic
fromJSON(input: ValueSeriesJsonInput<VS> & { sort?: boolean }): ValueSeries<VS>
ValueSeries.fromJSON({ name, schema, rows })The row door — the shape data arrives in from an ordinary JSON API,
a CSV parse, or anything that hands you one record per row. Rows may be
tuples ([axis, ...values], aligned with the schema) or objects
keyed by column name — each row is read on its own terms, so a payload
that mixes the two still ingests.
The value-axis counterpart of TimeSeries.fromJSON, minus the one thing a
value axis has no use for: there is no timestamp parsing and no
parse.timeZone. The axis cell must be a finite number — a value axis
has no calendar to interpret '2026-01-01' against — so a string axis is
an error naming the row rather than a silent NaN.
Strictness (this is the strict door). Every defined value cell is
checked against its declared kind, so a NaN/Infinity in a 'number'
column or a number in a 'string' column is rejected, and a column
declared required (the default) rejects a missing cell. null and
undefined both mean missing and are accepted for a
required: false column. The columnar doors are deliberately looser —
they treat a non-finite number as a gap and ignore required, because a
decoded buffer has no way to distinguish "absent" from "not a number".
Ordering. As every door: the axis must be non-decreasing, or pass
sort: true to stable-sort the rows by axis value first.
Methods
axisAt
axisAt(i: number): number
The axis value at row i. Throws if out of range.
axisValues
axisValues(): Float64Array
The axis values (the x of every row), in axis order. Zero-copy — the
returned Float64Array is the live key buffer; treat it as read-only.
column
column(name: ValueSeriesColumnName<VS>): Column | undefined
A value column by name, for direct columnar reads (.read(i), .values()).
nearestIndex
nearestIndex(value: number): number
Index of the row whose axis value is closest to value — the
value-axis cursor primitive. The axis is non-decreasing, so this is a
binary search. Returns -1 for an empty series; clamps to the first / last
row when value is outside the axis extent.
sliceByValue
sliceByValue(lo: number, hi: number): ValueSeries<VS>
The contiguous sub-series whose axis value lies in [lo, hi) — the
value-axis cull (pan / zoom on a value x). Binary-searches the bounds and
zero-copy slices the store. lo >= hi (or a range outside the extent)
yields an empty series.
toArrow
toArrow(options?: ToArrowOptions): ArrowExport
chain.toArrow()Hands this series' columns over in Apache Arrow's memory layout, with no
copy — the export counterpart of ValueSeries.fromArrow, and the
same { length, fields } shape TimeSeries.toArrow returns (the exporter
is shared; a 'value' axis simply exports as a plain float64 field
rather than a timestamp one).
pond takes no dependency on apache-arrow, so the caller assembles the
Table from the handed-over buffers with makeData / makeVector — see
TimeSeries.toArrow's doc for the adapter snippet. From there another
columnar engine (polars, DuckDB, arrow-js) is reachable without a
re-ingest.
The returned buffers are pond's live storage, not copies — the same
read-only contract column(name) and axisValues() already carry. Copy
first if the consumer mutates in place.
toColumns
toColumns(): ValueSeriesColumnarOutput<VS>
chain.toColumns()The columnar wire envelope: { name, schema, columns } with one plain
array per column (the axis included, under its own name) — what
ValueSeries.fromColumns takes back, so
ValueSeries.fromColumns(chain.toColumns()) round-trips without a cast.
The columnar counterpart of ValueSeries.toJSON: same data, one
array per column instead of one array per row. Prefer it when the consumer
is itself column-oriented, or when the payload is dense enough that C
arrays beat N×C-element rows on size and parse time. Gaps emit as null
— NaN is not JSON, and Float64Array does not stringify as an array,
so a JSON-bound columnar payload has to pay this conversion somewhere.
For a zero-copy columnar handoff in-process (no JSON, no per-cell
walk), use ValueSeries.toArrow instead.
A boolean or array-kind column (which only a byValue projection can
introduce — the direct doors take number and string) exports fine here
but cannot be ingested back; the return type reflects that, so this
round trip fails to compile rather than at runtime. The row leg is the
other way round: toJSON types its boolean cells honestly, so
fromJSON accepts the payload's shape and throws at ingest instead,
naming the column and its kind.
toJSON
toJSON(options?: { rowFormat?: 'array' }): ValueSeriesJsonOutputArray<VS>
chain.toJSON()/ chain.toJSON({ rowFormat: 'object' }).
The row-shaped wire envelope: { name, schema, rows }, exactly what
ValueSeries.fromJSON takes back, and JSON.stringify-safe
throughout (gaps emit as null, not NaN).
Defaults to tuple rows; pass { rowFormat: 'object' } for schema-keyed
object rows (larger on the wire, readable in a log). The return type
narrows on the option, so result.rows needs no cast — the overload
cascade that keeps TimeSeries.toJSON broad doesn't reach here (this
class has no other overload sets to disturb).
toJSON(options: { rowFormat: 'object' }): ValueSeriesJsonOutputObject<VS>
chain.toJSON()/ chain.toJSON({ rowFormat: 'object' }).
The row-shaped wire envelope: { name, schema, rows }, exactly what
ValueSeries.fromJSON takes back, and JSON.stringify-safe
throughout (gaps emit as null, not NaN).
Defaults to tuple rows; pass { rowFormat: 'object' } for schema-keyed
object rows (larger on the wire, readable in a log). The return type
narrows on the option, so result.rows needs no cast — the overload
cascade that keeps TimeSeries.toJSON broad doesn't reach here (this
class has no other overload sets to disturb).
toObjects
toObjects(): readonly ValueSeriesObjectRow<VS>[]
chain.toObjects()The rows as objects keyed by schema column name, in axis order — for
reading by name rather than tuple position (a table renderer, a CSV
writer, d3). A gap reads as undefined.
toRows
toRows(): readonly ValueSeriesRow<VS>[]
chain.toRows()The rows as tuples — [axis, ...values], aligned with the schema, in
axis order. A gap reads as undefined (the JS spelling); use
ValueSeries.toJSON for the wire spelling (null).
Unlike TimeSeries.toRows there is no key object to normalize: the axis
is already a plain number, so a value row is a tuple of scalars end to end.