Cursors & readouts
The cursor is how a chart reports the value under the pointer. It's a
component you mount as a child of <ChartContainer> — mount one to get a
cursor, mount none for no cursor. Mounted inside a <ChartRow> instead, it
overrides the container's cursor for that row. The vertical time line itself
is shared across every row in the container, so hovering one row moves the
cursor on all of them.
<ChartContainer range={range} width={560}>
<CrosshairCursor />
<ChartRow height={200}>{/* … */}</ChartRow>
</ChartContainer>
This page is the complete component + 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 cursor components
Six components, one per presentation. Every one except <RangeCursor> is an
in-chart readout — it draws marks at the hovered sample; <RangeCursor> is
a selection gesture (see
Pan, zoom & range selection).
| Component | What it draws |
|---|---|
| (none mounted) | No in-chart cursor. Pair with onTrackerChanged for a fully off-chart readout. |
<LineCursor /> | The synced vertical line only, no per-series marks. |
<PointCursor /> | A dot on each series at the cursor, no line. |
<InlineCursor /> | Dots plus a value chip beside each series. |
<FlagCursor /> | Dots plus value flags. (Staffed-flag geometry lands in a later phase; for now flags stack at top.) |
<CrosshairCursor /> | 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. |
<RangeCursor /> | Shades the bucket under the pointer and enables drag-to-select. Takes a sequence for bucket snapping; see the range-selection page. |
The five hover-readout cursors, switchable live:
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>
);
}
Off-chart readouts — onTrackerChanged
Any cursor (or none at all) 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,
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>
);
}
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
On <ChartContainer>:
| Prop | Type | Default | Purpose |
|---|---|---|---|
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. |
On the cursor components:
| Prop | On | Default | Purpose |
|---|---|---|---|
showTime | LineCursor, PointCursor, InlineCursor, FlagCursor, CrosshairCursor | false (true on CrosshairCursor) | Show the cursor's time atop the in-chart readout (the crosshair pins it to the x axis), formatted by timeFormat. |
snap | CrosshairCursor | true | Reticle Y-snapping. false = free reticle (yScale.invert(pointerY)), no snap. |
format | CrosshairCursor | — | Format for the x-axis time pill (also shapes marker indicators and annotation auto-labels). |
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, with every cursor. To force a chart to
never show a cursor, mount no cursor component — 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
>
<LineCursor />
<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.
<FlagCursor>staffs are provisional. Flags currently stack at the top of the plot rather than staffing from each point; the geometry is a later phase.<CrosshairCursor>values snap to the series, so the axis pill reads like a tick — set<CrosshairCursor snap={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 cursor 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
<RangeCursor>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.