Skip to main content

Axes

A chart has two kinds of axis, owned at two different levels. The x axis is shared by the whole <ChartContainer> — one scale every row draws against, so cursors and ranges line up across stacked plots. Each <ChartRow> owns its own y axis (or axes), so rows with different units stack cleanly.

This page is the reference for declaring and binding axes. The three axis kinds that change what the x axis means each get their own page:

  • Value axis — key by a monotonic quantity (distance, strike) instead of time.
  • Category axis — an ordinal axis, one slot per category.
  • Trading-time axis — a session-aware time axis with closed-market gaps collapsed.

Y axes

A y axis is a <YAxis> inside a <ChartRow>. A draw layer binds to it by id through its axis prop — id picks the scale, while as picks the style (they're separate channels). Declare several with distinct ids and sides for a dual-axis row; placement follows side, not JSX order, and the first-declared axis is the row's default for any layer that names none.

src/examples/learn-02-dual-axis.tsx
import {
ChartContainer,
ChartRow,
Layers,
LineChart,
YAxis,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { singleHostSeries } from './lib/server-metrics';

export default function DualAxis() {
const theme = useSiteChartTheme();
const series = singleHostSeries();

return (
<ChartContainer range={series.timeRange()} width={560} theme={theme}>
<ChartRow height={220}>
<YAxis id="pct" side="left" label="cpu" format=".0%" />
<YAxis id="ms" side="right" label="latency (ms)" format=",.0f" />
<Layers>
<LineChart series={series} column="cpu" axis="pct" as="primary" />
<LineChart
series={series}
column="latency"
axis="ms"
as="secondary"
/>
</Layers>
</ChartRow>
</ChartContainer>
);
}
<ChartRow height={200}>
<YAxis id="pct" side="left" format=".0%" />
<YAxis id="ms" side="right" format=",.0f" />
<Layers>
<LineChart series={s} column="cpu" axis="pct" />
<LineChart series={s} column="latency" axis="ms" />
</Layers>
</ChartRow>

If a row has no <YAxis> at all, the layers still draw against an implicit auto-fitting axis — you just get no gutter or ticks.

<YAxis> props

PropTypeDefaultPurpose
idstring— (required)The scale id a layer binds to via axis. First-declared = row default.
side'left' | 'right''left'Which gutter the axis sits in.
labelstringidAxis title / unit.
labelPlacement'rotated' | 'top''rotated'rotated = vertical strip on the outer edge; top = horizontal atop.
min / maxnumberauto-fitExplicit domain bounds; omit to fit the bound layers.
padnumber0Fractional headroom added each side of the resolved domain.
formatAxisFormat (d3 specifier or (v) => string)scale defaultTick-label and cursor-readout formatting. Hoist an inline function.
ticksReadonlyArray<{ at: number; label: string }>autoExplicit ticks — drives both labels and gridlines. [] draws none.
boundaryLabelsbooleantruefalse drops just the top & bottom extreme labels (gridlines stay).
widthnumber50Gutter width in CSS px.
colorstringthemeThis axis's tick + title colour (presentation-only).

format is the one prop whose inline function form must be hoisted or useCallback'd — it's the only prop the layout's structural change-detection can't value-compare, so a fresh function each render re-registers the axis.

The x axis

You don't declare the x axis to get one: <ChartContainer> renders a <TimeAxis> at the bottom automatically (showAxis defaults to true). Set showAxis={false} for a bare plot, or to place your own <XAxis> — for a top axis, a label, custom ticks, or a second (transformed) strip.

Its kind is inferred from the data, never set by a prop: a TimeSeries gives a time axis, a ValueSeries a value axis, and BarChart categories a category axis. Every layer in a container must agree on the kind — a mix throws. <TimeAxis> and <CategoryAxis> are both just <XAxis> presets; the kind follows the data regardless of which you render.

<XAxis> props

PropTypeDefaultPurpose
side'top' | 'bottom''bottom'Which edge. Declaration order stacks multiple strips.
labelstringCentred axis title.
formatAxisFormatcontainer'sTick/cursor formatting, resolved against the axis kind.
ticksReadonlyArray<{ at: number; label: string }>autoExplicit ticks in axis-value units.
transform{ to(v): number; from(u): number }Relabel the same scale into a derived unit (a second tick layout).
align'auto' | 'center' | 'right''center'Horizontal tick-label placement.
colorstringthemeTick / label / rule / title colour — the lever for a stacked dual axis.
heightnumberfit-to-contentStrip height in px.

Dual x-axes — transform

A second <XAxis> with a transform relabels the same pixel scale into a derived unit — one scale, two tick layouts, never two scales. The to / from pair are monotonic inverses and may be nonlinear:

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>
);
}

Declaration order stacks the strips (before <ChartRow> → above the plot, after → below); gridlines always follow the container's primary ticks. This is relabeling, not the axis-kind mixing that throws — the scale is unchanged.

Time formatting

ChartContainer's timeFormat is the shared formatter for both the auto axis labels and the cursor-time readout (omit it for d3's multi-scale time default). A per-instance <XAxis format> overrides just that axis's labels. Either opts the axis out of the two-row boundary-label layout.

See also