Value axis
@pond-ts/charts draws whatever axis your data implies — it is never a prop
you set. Hand <ChartContainer> a TimeSeries and you get a time axis; hand
it a ValueSeries (from byValue
or ValueSeries.fromColumns) and
every draw layer in that row draws against a linear value axis instead —
same LineChart, ScatterChart, BoxPlot, BarChart components, no
axis-type prop anywhere.
This page is the chart-level reference: how the axis kind is inferred, how
each draw layer behaves once it's on a value axis, and how to run two label
systems over one shared scale. For the series-level foundations — what a
ValueSeries is, byValue vs byColumn, why scan builds the axis — see
the value-axis concept page and
the design RFC. For a guided first pass with
a running example, see
Learn charts, chapter 9. For the
exhaustive per-prop walk, see the Storybook groups linked throughout this
page.
Kind inference
A <ChartContainer> never takes an axis-type prop. Instead, every layer
registered under it (LineChart, AreaChart, BarChart, ScatterChart,
BoxPlot, Candlestick, …) reports its own x-axis kind — 'time',
'value', or 'category' — read straight off the series or data it was
handed. The container adopts the first kind it sees and throws if a later
layer disagrees:
ChartContainer: rows mix x-axis kinds ('time' and 'value'). A container has one shared x axis — every row must plot the same kind (all time-keyed, all value-keyed, or all category).
This is a hard error, not a warning, because there is exactly one shared
x scale per container — mixing kinds would mean mixing scales, which
@pond-ts/charts deliberately doesn't support (see
Dual x-axes below for the feature that looks
similar but isn't: relabeling, not rescaling). A container with no layers yet
defaults to 'time'.
| Kind | What produces it | Draw layers |
|---|---|---|
'time' | A TimeSeries (the default — every layer without an explicit ValueSeries or categories prop) | all chart types |
'value' | A ValueSeries — via byValue (projected) or ValueSeries.fromColumns (native) | all chart types except Candlestick |
'category' | A CategoryDatum[] array — typically from transposeRow | BarChart |
Data shapes
Three different things read as "a value axis" to a draw layer, and only one of them is actually a series. Here is what each one looks like as data, using the same field names (and roughly the same values) as this page's live examples.
ValueSeries — columnar, axis + value columns
What the smile-chain examples below hand LineChart/ScatterChart. The
first column is the axis (the key — monotonic, non-nullable); every
other column is an ordinary value column:
strike (axis) | fair |
|---|---|
| 80.0 | 0.440 |
| 82.5 | 0.397 |
| ⋯ | ⋯ |
| 100.0 | 0.240 |
| ⋯ | ⋯ |
| 120.0 | 0.376 |
A ValueSeries is not an array of row objects you can .map() over —
it's the same columnar storage as TimeSeries, just keyed by a value
instead of time. Rows are read through per-row accessors:
series.axisAt(i) for the key, series.column('fair') for the value column
(each column reads via .read(i) / .values()). byValue('strike')
produces this shape from a TimeSeries that has a strike column;
ValueSeries.fromColumns builds it directly from columnar arrays.
byColumn bins — a plain array of records
What the histogram example hands BarChart bins={...}. Each record is one
bin: start/end are the bin's edges in axis units (here: ms of
response time), and every other field is an aggregate you asked for:
start | end | count |
|---|---|---|
| ⋯ | ⋯ | ⋯ |
| 90 | 100 | 128 |
| 100 | 110 | 235 |
| 110 | 120 | 320 |
| 120 | 130 | 292 |
| ⋯ | ⋯ | ⋯ |
This is a real Array<{ start, end, ...aggregates }> — index it, .map()
it, slice it. The x axis is inferred from the start/end edges.
CategoryDatum[] — a plain array of labelled values
What transposeRow produces and BarChart categories={...} consumes. One
entry per category; label becomes the tick, order becomes the axis order:
label | value |
|---|---|
api-1 | 0.34 |
api-2 | 0.48 |
worker-1 | 0.61 |
Also a real array — hand-build it if you don't have a wide series to transpose.
Why the split: the ValueSeries stays inside the series algebra
(sliceable, cursor-able, full resolution), while bins and categories have
already left it as plain records — see
closed vs. projected-out
for the reasoning.
Two ways onto a value axis
Projected — start with a TimeSeries you already have and re-key it onto
one of its own numeric columns with byValue. This is the common case: an
elapsed-time series becomes a pace curve, a tick series becomes a vol smile
once you compute strike as a column. Chapter 9 of Learn charts walks this
version end to end with a live embed — see
Learn charts: a linear value axis.
Native — when the data was never time-keyed at all (an options chain
keyed by strike, with no timestamp on any row), skip the projection and
construct the ValueSeries directly with ValueSeries.fromColumns — the
value-axis counterpart of TimeSeries.fromColumns:
import {
ChartContainer,
ChartRow,
Layers,
LineChart,
ScatterChart,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { smileChain } from './lib/value-axis-fixtures';
/** An options chain natively keyed by strike (`ValueSeries.fromColumns`,
* never time-keyed) — `LineChart` and `ScatterChart` read it exactly like
* a `TimeSeries`, no special-casing. */
export default function ChartsValueAxisNative() {
const theme = useSiteChartTheme();
const chain = smileChain();
return (
<ChartContainer timeFormat=",.0f" width={560} theme={theme}>
<ChartRow height={220}>
<YAxis id="iv" label="implied vol" format=".1%" width={60} />
<Layers>
<LineChart series={chain} column="fair" curve="natural" />
<ScatterChart series={chain} column="fair" id="fair" />
</Layers>
</ChartRow>
</ChartContainer>
);
}
Both LineChart and ScatterChart read the same ValueSeries here with no
special-casing — every draw layer treats a value axis exactly like a time
axis once it's handed one.
Aggregating onto a value axis: byColumn
Not every value-axis chart is a ValueSeries. byColumn
bins a numeric column and returns plain bin records
({ start, end, ...aggregates }) — a value-axis histogram, not a series.
BarChart reads those bins directly via its bins prop, inferring a
'value' axis kind from the bin boundaries the same way it infers 'time'
from a time-bucketed aggregate:
import {
BarChart,
ChartContainer,
ChartRow,
Layers,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { responseTimeDistribution } from './lib/gallery-fixtures';
/** Response-time distribution: a value-axis histogram (`byColumn`, 10ms-wide
* bins) — the x axis is inferred from the bins, not declared. */
export default function GalleryHistogram({ width }: { width: number }) {
const theme = useSiteChartTheme();
const bins = responseTimeDistribution();
return (
<ChartContainer range={[0, 280]} width={width} theme={theme}>
<ChartRow height={220}>
<YAxis id="count" label="samples" min={0} pad={0.06} width={44} />
<Layers>
<BarChart bins={bins} column="count" gap={2} />
</Layers>
</ChartRow>
</ChartContainer>
);
}
Reach for byValue (above) when you want to keep the full-resolution channel
and plot or cursor it against a value; reach for byColumn when you want to
collapse it into bins. See
byValue vs byColumn
for the full distinction, including the split = scan + byColumn composition
for stateful per-bin metrics.
Category axis
transposeRow reads one row of a wide series and turns it into
CategoryDatum[] — an ordinal axis, one bar per category, inferred the same
way a value or time axis is: from what BarChart was handed, not from a
prop. Chapter 9 has the live walkthrough:
Learn charts: a category axis.
For the systematic prop-by-prop reference — high-cardinality labeling,
selection, signed values, scrubbing a transposed row live — see the
Axes/CategoryAxis
Storybook group.
Two label systems, one scale
A value (or time) axis can carry a second <XAxis> that relabels the
same shared scale into a derived unit via transform — one pixel mapping,
two tick layouts, never two scales. Declaration order stacks the strips:
before <ChartRow> renders above the plot, after renders below.
import {
ChartContainer,
ChartRow,
Layers,
LineChart,
ScatterChart,
XAxis,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { SPOT, smileChain } from './lib/value-axis-fixtures';
export default function ChartsValueAxisDual() {
const theme = useSiteChartTheme();
const chain = smileChain();
return (
<ChartContainer showAxis={false} width={560} theme={theme}>
{/* A second <XAxis> relabels the SAME shared scale into a derived
unit via `transform` — one pixel mapping, two tick layouts. Here
strike (below) and moneyness = strike / spot (above) are linearly
related, so the top strip's ticks land evenly too. */}
<XAxis
side="top"
transform={{ to: (k) => k / SPOT, from: (m) => m * SPOT }}
format=".2f"
label="Moneyness"
/>
<ChartRow height={220}>
<YAxis id="iv" label="implied vol" format=".1%" width={60} />
<Layers>
<LineChart series={chain} column="fair" curve="natural" />
<ScatterChart series={chain} column="fair" id="fair" />
</Layers>
</ChartRow>
<XAxis label="Strike" format=",.0f" />
</ChartContainer>
);
}
Here transform is the linear strike → strike / spot map, so the top
strip's ticks land evenly too — the degenerate, easiest-to-read case. A
transform doesn't have to be linear: the
Axes/DualX
Storybook group has a nonlinear std-moneyness-to-delta example, where the
derived strip's ticks compress and expand as σ-space stretches unevenly
across delta.
Interacting with a value axis
Cursors, region-select, and annotations all work identically on a value axis
— they read positions off the shared scale, and a value scale is just
another continuous scale to them. Nothing about cursor="region",
onRegionSelect, <Region>, <Marker>, or <Baseline> changes; only the
units on the x axis do. See the
Cursors/Region
and Annotations/Scenarios
Storybook stories for both on a .byValue() series.
See also
- Value axis (concept) — the series-level
model:
ValueSeries,byValue,byColumn,scan, and why the algebra stays closed. - Value axis RFC — the design rationale and shipped-status history.
- Learn charts, chapter 9 — a guided first pass across all four axis kinds (time, value, category, session-aware time) with one running example.
- Gallery — the value-axis histogram above is one of eight live cards there.