Skip to main content

Reading and selecting values: cursors, readouts, zoom

You will learn
  • The five cursor modes, and when to reach for each
  • onTrackerChanged — reading the hovered value outside the chart
  • The select-to-zoom loop: cursor="region"onRegionSelectrange

Every chart in this track has had a cursor without you setting one — cursor defaults to 'line', the synced vertical line you've been hovering since chapter 1. This chapter is the rest of that knob.

Five modes, one prop

src/examples/learn-06-cursor-modes.tsx
import { useState } from 'react';
import {
ChartContainer,
ChartRow,
Layers,
LineChart,
YAxis,
type CursorMode,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { singleHostSeries } from './lib/server-metrics';

const MODES: readonly CursorMode[] = [
'line',
'point',
'inline',
'flag',
'crosshair',
];

export default function LearnCursorModes() {
const theme = useSiteChartTheme();
const series = singleHostSeries();
const [mode, setMode] = useState<CursorMode>('line');

return (
<div>
<div
style={{ display: 'flex', gap: 6, marginBottom: 10, flexWrap: 'wrap' }}
>
{MODES.map((m) => (
<button
key={m}
onClick={() => setMode(m)}
style={{
padding: '4px 10px',
borderRadius: 6,
border: '1px solid var(--site-surface-border)',
background:
m === mode ? 'var(--ifm-color-primary)' : 'transparent',
color: m === mode ? '#fff' : 'inherit',
cursor: 'pointer',
fontSize: 13,
}}
>
{m}
</button>
))}
</div>
<ChartContainer
range={series.timeRange()}
width={560}
theme={theme}
cursor={mode}
>
<ChartRow height={220}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
<LineChart series={series} column="cpu" axis="pct" />
</Layers>
</ChartRow>
</ChartContainer>
</div>
);
}
<ChartContainer cursor="crosshair" /* or 'line' | 'point' | 'inline' | 'flag' */>
  • line (default) — a synced vertical line across every row.
  • point — a dot on the series itself, no line.
  • inline — an in-chart value readout drawn near the cursor.
  • flag — an in-chart readout in a flag-shaped chip.
  • crosshair — a vertical and horizontal line, with axis-pill readouts on both axes. Snaps to the nearest data point by default (crosshairSnap={false} for a free reticle).

There's a sixth value, 'none', and a seventh, 'region' — that one is this chapter's payoff, below.

Reading the value outside the chart

onTrackerChanged fires with the hovered value(s) — or null when the cursor leaves — so you can drive a readout that lives anywhere on the page, not just inside the chart:

src/examples/learn-06-tracker-readout.tsx
import { useState } from 'react';
import {
ChartContainer,
ChartRow,
Layers,
LineChart,
YAxis,
type TrackerInfo,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { singleHostSeries } from './lib/server-metrics';

export default function LearnTrackerReadout() {
const theme = useSiteChartTheme();
const series = singleHostSeries();
const [info, setInfo] = useState<TrackerInfo | null>(null);

return (
<div>
<div
style={{
marginBottom: 10,
fontSize: 13,
fontFamily: 'ui-monospace, monospace',
minHeight: 20,
}}
>
{info === null
? 'hover the chart →'
: info.values.map((v) => (
<span key={v.label} style={{ color: v.color, marginRight: 16 }}>
{v.label}: {(v.value * 100).toFixed(1)}%
</span>
))}
</div>
<ChartContainer
range={series.timeRange()}
width={560}
theme={theme}
cursor="line"
onTrackerChanged={setInfo}
>
<ChartRow height={200}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
<LineChart series={series} column="cpu" axis="pct" />
</Layers>
</ChartRow>
</ChartContainer>
</div>
);
}
const [info, setInfo] = useState<TrackerInfo | null>(null);

<ChartContainer cursor="line" onTrackerChanged={setInfo}>
{/* rows */}
</ChartContainer>;

{
info === null ? 'hover the chart' : info.values.map((v) => v.value);
}

TrackerInfo carries time plus one TrackerSample (x, value, color, label) per series under the cursor — the same data every in-chart readout draws from, just handed to you instead. cursorTime (a boolean on ChartContainer) shows the cursor's time atop an in-chart flag/inline readout, if you want it there too. For programmatic control rather than mouse-driven — highlighting a specific instant from outside, say a table row hover — a controlled trackerPosition (epoch ms) drives the same cursor without a real pointer event. It's a followed position a live hover on the chart overrides, so it also powers cross-chart cursor sync (see Cursors & readouts); omit it or pass null for no controlled position.

The payoff: select-to-zoom

cursor="region" turns the cursor into a drag-to-select tool. Combine it with onRegionSelect and a bit of state, and you have the whole zoom loop in about a dozen lines:

src/examples/learn-06-zoom.tsx
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>
);
}
const fullRange = series.timeRange();
const [range, setRange] = useState(fullRange);

<ChartContainer
range={range}
cursor="region"
onRegionSelect={(r) => setRange(r)}
>
{/* rows */}
</ChartContainer>;

<button onClick={() => setRange(fullRange)}>Reset zoom</button>;

onRegionSelect hands back a neutral [lo, hi] pair — the container never zooms itself, your setRange closes the loop by feeding the selection straight back into range. A "reset" button is just setRange(fullRange). This is the same range prop every chart in this track has passed a static value to since chapter 1 — controlling it is nothing new, just a new source for the value.

Two drags that look alike

Worth separating now, because they use the same gesture and you will meet both: cursor="region" drags out a time range — a view concern, which is why it pairs with setRange. A <MultiSelector> drag looks identical but selects the data marks inside a range instead, and hands you those.

Same gesture, same drag band on screen (one renderer, so they can't drift), two different questions. This chapter is the range one. The marks one is reference-only: selection & hover and sweeps & multi-select.

One conflict to know about: regionSelectModifier="shift" claims the shift chord, so if you later want shift to mean something in a selection handler on the same chart, decide which one owns it.

Recap

Five cursor modes cover reading a hovered value in five different visual styles; onTrackerChanged moves that same reading outside the chart; cursor="region" turns the cursor into a drag-to-select tool, and onRegionSelect + a controlled range is the whole zoom loop.

Next: Marking up charts — placing a mark on a chart, rather than just reading what's there.