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).
cursor | What 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:
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
}
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>:
| Prop | Type | Default | Purpose |
|---|---|---|---|
cursor | CursorMode | 'line' | The in-chart cursor presentation (a <ChartRow> can override per row). |
onTrackerChanged | (info | null) => void | — | Hover snapshot for an off-chart readout; null on leave. |
trackerPosition | number | null | — | Followed cursor position (epoch ms) for when this chart isn't the one hovered. A local hover wins over it. null/omit = none. |
cursorTime | boolean | false | Show the cursor's time atop the in-chart readout, formatted by timeFormat. |
crosshairSnap | boolean | true | crosshair 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) andonTrackerChanged={info => setSharedTime(info?.time ?? null)}(report); - clear it to
nullon the group'sonPointerLeaveso 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.
flagstaffs are provisional. Flags currently stack at the top of the plot rather than staffing from each point; the geometry is a later phase.crosshairvalues snap to the series, so the axis pill reads like a tick — setcrosshairSnap={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 wiringonTrackerChangedon 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 wireonTrackerChangedonly where you actually need the report.
See also
- Learn charts, chapter 6 — the tutorial that builds these up one mode at a time.
- Selection & hover — clicking a data mark (distinct from the hover readout).
- Sweeps & multi-select — dragging across a
range of marks. Note the overlap to keep straight: a
<RangeCursor>drag selects a time range (a view concern), a<MultiSelector>drag selects the marks in one. They share the drag band renderer, andsequencesnaps both. - Pan, zoom & range selection — the
regioncursor and the select-to-zoom loop. - Storybook:
Cursors/Scenarios/Synced Charts— the cross-chart sync worked example. - Storybook:
Cursors/Line,Cursors/Point,Cursors/Inline,Cursors/Flag,Cursors/Crosshair.