Skip to main content

Reading and selecting values: cursors, readouts, zoom

You will learn
  • The five cursor components, and when to reach for each
  • onTrackerChanged — reading the hovered value outside the chart
  • The select-to-zoom loop: <RangeCursor onDragRelease> → range

Every chart in this track has mounted a <LineCursor /> — the synced vertical line you've been hovering since chapter 1. A cursor is a component you mount inside <ChartContainer>: mount one to get a cursor, mount none for no cursor. This chapter is the rest of that family.

Five cursors, one slot​

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

// One cursor component per mode — mount one to get that cursor.
const CURSORS = {
line: <LineCursor />,
point: <PointCursor />,
inline: <InlineCursor />,
flag: <FlagCursor />,
crosshair: <CrosshairCursor />,
} as const;
type CursorMode = keyof typeof CURSORS;
const MODES = Object.keys(CURSORS) as CursorMode[];

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}>
{CURSORS[mode]}
<ChartRow height={220}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
<LineChart series={series} column="cpu" axis="pct" />
</Layers>
</ChartRow>
</ChartContainer>
</div>
);
}
<ChartContainer>
{/* or LineCursor | PointCursor | InlineCursor | FlagCursor */}
<CrosshairCursor />
{/* rows */}
</ChartContainer>
  • <LineCursor /> — a synced vertical line across every row.
  • <PointCursor /> — a dot on the series itself, no line.
  • <InlineCursor /> — an in-chart value readout drawn near the cursor.
  • <FlagCursor /> — an in-chart readout in a flag-shaped chip.
  • <CrosshairCursor /> — a vertical and horizontal line, with axis-pill readouts on both axes. Snaps to the nearest data point by default (<CrosshairCursor snap={false} /> for a free reticle).

Mount nothing and the chart has no cursor. There's a sixth component, <RangeCursor /> — 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,
LineCursor,
} 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}
onTrackerChanged={setInfo}
>
<LineCursor />
<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 onTrackerChanged={setInfo}>
<LineCursor />
{/* 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. showTime (a boolean on the cursor component — <FlagCursor showTime />) 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​

<RangeCursor /> is a drag-to-select cursor. Give it an onDragRelease 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,
RangeCursor,
} 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}>
<RangeCursor onDragRelease={(span) => setRange(span.x)} />
<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}>
<RangeCursor onDragRelease={(span) => setRange(span.x)} />
{/* rows */}
</ChartContainer>;

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

onDragRelease hands back { x: [lo, hi] } — the container never zooms itself, your setRange closes the loop by feeding the selection's x 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: <RangeCursor> 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: <RangeCursor dragModifier="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 components cover reading a hovered value in five different visual styles; onTrackerChanged moves that same reading outside the chart; <RangeCursor> is a drag-to-select cursor, and its onDragRelease + 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.