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, or null to hide) drives the same cursor without a real pointer
event.
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.
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.