Skip to main content

Cursors & readouts

The cursor is how a chart reports the value under the pointer. It's a per-row presentation set with the cursor prop on <ChartContainer> (a row can override the container default); the vertical time line itself is shared across every row in the container, so hovering one row moves the cursor on all of them.

This page is the complete mode + prop reference. For the guided first pass — building up from a bare line cursor to a trading-terminal crosshair — see Learn charts, chapter 6. For the per-prop visual walk, see the Cursors/* Storybook groups.

The seven modes

cursor takes one of seven exclusive CursorMode values. Every mode except none and region is an in-chart readout — it draws marks at the hovered sample; region is a selection gesture (see Pan, zoom & range selection).

cursorWhat it draws
'none'No in-chart cursor. Pair with onTrackerChanged for a fully off-chart readout.
'line'Default. The synced vertical line only, no per-series marks.
'point'A dot on each series at the cursor, no line.
'inline'Dots plus a value chip beside each series.
'flag'Dots plus value flags. (Staffed-flag geometry lands in a later phase; for now flags stack at top.)
'crosshair'A single reticle — the vertical line, a horizontal line and a dot on the nearest series — with that value pinned to its own y-axis as an on-axis pill and the cursor time pinned to the x-axis: the ChartIQ / trading-terminal readout. Values snap to the series, not raw mouse Y.
'region'Shades the bucket under the pointer and enables drag-to-select. Needs cursorSequence for bucket snapping; see the range-selection page.

The five hover-readout modes, switchable live:

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>
);
}

Off-chart readouts — onTrackerChanged

Any mode (including 'none' and 'line') can drive a readout you render yourself — a legend, a stat row, a table beside the chart. onTrackerChanged fires on hover with a snapshot of the cursor time and every series' value there, and fires with null when the pointer leaves:

onTrackerChanged?: (info: TrackerInfo | null) => void;

interface TrackerInfo {
time: number; // cursor time, epoch ms
values: readonly TrackerSample[];
}

interface TrackerSample {
x: number; // the sample's key (epoch ms, or the axis value)
value: number; // the plotted value
color: string; // the series' resolved colour
label: string; // the series' display label
}
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>
);
}

The snapshot is a derived view of your own data at the cursor — read the live value from your series when you need the current number, don't cache the last payload (on a live chart the value under a stationary cursor keeps changing).

Cursor props

All of these are on <ChartContainer>:

PropTypeDefaultPurpose
cursorCursorMode'line'The in-chart cursor presentation (a <ChartRow> can override per row).
onTrackerChanged(info | null) => voidHover snapshot for an off-chart readout; null on leave.
trackerPositionnumber | nullFollowed cursor position (epoch ms) for when this chart isn't the one hovered. A local hover wins over it. null/omit = none.
cursorTimebooleanfalseShow the cursor's time atop the in-chart readout, formatted by timeFormat.
crosshairSnapbooleantruecrosshair reticle Y-snapping. false = free reticle (yScale.invert(pointerY)), no snap.

trackerPosition is the one to reach for when the cursor should follow something other than this chart's own mouse — a playback head, a value picked in a sibling component, or another chart's cursor (below). It is a followed position, not a hard pin: a live hover on this chart always wins over it, so a chart is never stuck showing an external cursor while the user is pointing at it. The vertical line always snaps its x to the data grid so the time readout stays clean, in every mode. To force a chart to never show a cursor, use cursor="none" — not trackerPosition={null}.

Syncing cursors across charts

On a dashboard you usually want one crosshair time shared across every chart — hover the CPU chart and the memory, disk, and network charts all show a cursor at the same instant. That falls out of the two props above, with no "which chart is active" bookkeeping, because a local hover wins over trackerPosition:

  • hold the shared time in page state;
  • give every chart trackerPosition={sharedTime} (follow) and onTrackerChanged={info => setSharedTime(info?.time ?? null)} (report);
  • clear it to null on the group's onPointerLeave so the crosshair lifts off every chart when the pointer leaves them all.

The chart under the pointer favours its own hover — it's the source, and reports the time out — while every other chart has no local pointer, so it follows the shared time. Each follower maps that time through its own xScale, so the cursors line up even when the charts are at different zoom levels or x-domains.

function Dashboard({ cpu, mem }: { cpu: TimeSeries; mem: TimeSeries }) {
const [cursor, setCursor] = useState<number | null>(null);

const chart = (series: TimeSeries, id: string) => (
<ChartContainer
range={range}
width={560}
trackerPosition={cursor} // follow the shared time…
onTrackerChanged={(info) => setCursor(info?.time ?? null)} // …and report ours
>
<ChartRow height={120}>
<YAxis id={id} min={0} max={100} />
<Layers>
<LineChart series={series} column="v" axis={id} />
</Layers>
</ChartRow>
</ChartContainer>
);

// The group-level onPointerLeave lifts the crosshair off every chart at once.
return (
<div onPointerLeave={() => setCursor(null)}>
{chart(cpu, 'cpu')}
{chart(mem, 'mem')}
</div>
);
}

Because the shared cursor is plain React state, it composes with anything else that reads or writes it — a scrubber, a "jump to event" button, or an off-chart readout fed from the same onTrackerChanged (see above). Each chart's onTrackerChanged also fires while it's a follower, carrying its series' values at the shared time — handy for a combined readout row that shows every chart's value at the one cursor.

Sharp edges

  • Mouse-only. There is no keyboard or touch cursor yet — the readout follows a pointer.
  • flag staffs are provisional. Flags currently stack at the top of the plot rather than staffing from each point; the geometry is a later phase.
  • crosshair values snap to the series, so the axis pill reads like a tick — set crosshairSnap={false} only if you want the horizontal line to track the raw pointer Y instead.
  • The pill sits on the axis that measured the value, including when a side carries several axes (it lands on that axis's column, not against the plot), and it wears that axis's <YAxis color> when one is set — so with stacked scales the pill's position and ink both say which scale you are reading.
  • Sync on a trading-time axis: a follower reports the shared time back through its own xScale, which is an exact round-trip on a continuous (linear / time) axis — so wiring onTrackerChanged on every chart is a clean no-op there. On a discontinuity (trading-time) axis a shared time that lands in that chart's collapsed gap doesn't round-trip exactly, which can nudge the shared cursor to the gap seam. If you sync charts with gaps, dedupe your shared-state setter (ignore a near-equal time), or wire onTrackerChanged only where you actually need the report.

See also