Skip to main content

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'.

KindWhat produces itDraw 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 transposeRowBarChart

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.00.440
82.50.397
100.00.240
120.00.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:

startendcount
90100128
100110235
110120320
120130292

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:

labelvalue
api-10.34
api-20.48
worker-10.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:

src/examples/charts-value-axis-native.tsx
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:

src/examples/gallery-histogram.tsx
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.

src/examples/charts-value-axis-dual.tsx
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.