Reading and selecting values: cursors, readouts, zoom
- The five
cursormodes, and when to reach for each onTrackerChanged— reading the hovered value outside the chart- The select-to-zoom loop:
cursor="region"→onRegionSelect→range
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
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:
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:
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.