Pan, zoom & range selection
Three related mechanisms move the view range — the slice of the x axis a
chart shows. They compose but are independent: drag-to-select a range, wire
that into a controlled zoom, and/or enable free pan/zoom. All of them speak
the same neutral range contract: a [start, end] pair in axis units.
Chapter 6 teaches the select-to-zoom loop as a payoff; this page is the full
prop reference. See
Learn charts, chapter 6 for
the narrative and the Cursors/Region
Storybook group for the per-variant walk.
Range selection — cursor="region"
Setting cursor="region" and providing onRegionSelect makes the region
cursor draggable: the user drags across the plot and, on release, you get
the selected range once.
cursor="region"
cursorSequence?: Sequence | BoundedSequence; // bucket snapping (time axis only)
onRegionSelect?: (range: readonly [number, number]) => void; // [lo, hi] on release
onRegionSelectfires once, on release, with[lo, hi]— always ordered low-to-high regardless of drag direction. Providing it is what turns the region cursor draggable; it's a notification — the container never zooms itself in response.cursorSequencesnaps the shaded band to buckets (whole minutes, whole sessions) as the drag extends, and is time-axis only — a "minute" is meaningless on a value axis. With nocursorSequencethe region cursor is the degenerate free-drag case (pixel-precise, no snapping), which is what a value-axis selection uses.- Range selection works on a time or value x axis; it's excluded on a category axis.
The select-to-zoom loop
Zooming is onRegionSelect → your state → controlled range. The container
draws whatever range you give it; the selection callback proposes a new one;
you decide whether to apply it. A "reset" is just setting range back to the
full extent.
const [range, setRange] = useState(full);
<ChartContainer
range={range}
cursor="region"
onRegionSelect={(r) => setRange(r)}
>
{/* … */}
</ChartContainer>;
import { useState } from 'react';
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 LearnZoom() {
const theme = useSiteChartTheme();
const series = singleHostSeries();
// singleHostSeries() always returns a non-empty, fixed-length series, so
// timeRange() is never undefined here.
const bounds = series.timeRange()!;
const fullRange: readonly [number, number] = [bounds.begin(), bounds.end()];
const [range, setRange] = useState<readonly [number, number]>(fullRange);
const zoomed = range[0] !== fullRange[0] || range[1] !== fullRange[1];
return (
<div>
<div style={{ marginBottom: 10 }}>
<button
onClick={() => setRange(fullRange)}
disabled={!zoomed}
style={{
padding: '4px 12px',
borderRadius: 6,
border: '1px solid var(--site-surface-border)',
background: 'transparent',
cursor: zoomed ? 'pointer' : 'default',
opacity: zoomed ? 1 : 0.5,
fontSize: 13,
}}
>
← Reset zoom
</button>
<span style={{ marginLeft: 10, fontSize: 12, opacity: 0.7 }}>
drag on the chart to select a range
</span>
</div>
<ChartContainer
range={range}
width={560}
theme={theme}
cursor="region"
onRegionSelect={(r) => setRange(r)}
>
<ChartRow height={200}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
<LineChart series={series} column="cpu" axis="pct" />
</Layers>
</ChartRow>
</ChartContainer>
</div>
);
}
Because the container never zooms on its own, the same onRegionSelect can
drive anything a range means in your app — a filter, a fetch, a linked chart —
not only a zoom.
Free pan & zoom — panZoom
Separately from range-select, panZoom enables direct manipulation: drag
the plot to pan, wheel to zoom around the cursor.
| Prop | Type | Default | Purpose |
|---|---|---|---|
panZoom | boolean | false | Drag-to-pan + wheel-to-zoom. Off by default so it doesn't capture drag/scroll unless asked. |
onTimeRangeChange | ([start, end]) => void | — | Fires on pan/zoom with the new range. Wire back to range for controlled; omit for uncontrolled. |
minDuration | number | 1 | Zoom-in floor — the minimum visible duration in ms. |
range | [number, number] | TimeRange | — | The controlled view range (shared by all three mechanisms). |
Controlled vs. uncontrolled matters here. Uncontrolled (panZoom on, no
onTimeRangeChange), the container holds the view internally and seeds it from
range whenever it isn't actively holding one — but later range changes are
then ignored so they can't fight the user's pan. To drive the range externally
— or to follow a live sliding window — use controlled mode: handle
onTimeRangeChange and feed range yourself.
Sharp edges
- The container never zooms itself.
onRegionSelectandonTimeRangeChangeare notifications; a controlledrangeis the only thing that actually changes what's shown. This is deliberate — a range means different things in different apps. cursorSequenceis time-axis only. Bucket snapping needs a temporal grid; value-axis and category selections don't take one.- Uncontrolled
panZoomstops trackingrangeonce the user grabs it — if you need the view to follow live data, go controlled. - Mouse-only — pan is drag, zoom is wheel; no keyboard or touch gestures yet.
See also
- Learn charts, chapter 6 — the select-to-zoom loop taught end to end.
- Cursors & readouts — the other six cursor
modes (the readouts, as opposed to
region's selection). - Storybook:
Cursors/Region—DragToSelect,Freeform,Sessions,ValueAxisSelect,PanAndSelect.