The annotation model
Annotations are the marks you add to a chart to call out meaning — an
incident window, a deploy, an SLA line. @pond-ts/charts has three
primitives, all rendered as <Layers> children alongside the data:
Region (a shaded x span),
Marker (a vertical line at an x), and
Baseline (a horizontal line at a
y). This page is the shared mental model; the next pages are the per-primitive
props, the editing/creating surface, and axis indicators.
Chapter 7 teaches placing them; this section is the reference. See
Learn charts, chapter 7 for the
tutorial and the Annotations/Scenarios
Storybook group for worked scenarios.
import {
Baseline,
ChartContainer,
ChartRow,
Layers,
LineChart,
Marker,
Region,
YAxis,
YAxisIndicator,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { singleHostSeries } from './lib/server-metrics';
const STEP_MS = 60_000;
export default function LearnAnnotations() {
const theme = useSiteChartTheme();
const series = singleHostSeries();
// singleHostSeries() always returns a non-empty, fixed-length series, so
// timeRange() is never undefined here.
const base = series.timeRange()!.begin();
const latest = series.at(series.length - 1)?.get('cpu');
return (
<ChartContainer range={series.timeRange()} width={560} theme={theme}>
<ChartRow height={220}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
<LineChart series={series} column="cpu" axis="pct" />
<Region
from={base + 40 * STEP_MS}
to={base + 55 * STEP_MS}
label="busy"
/>
<Marker at={base + 40 * STEP_MS} label="deploy" />
<Baseline value={0.4} axis="pct" label="target" />
{latest !== undefined && <YAxisIndicator value={latest} axis="pct" />}
</Layers>
</ChartRow>
</ChartContainer>
);
}
Two registers
A chart draws in two colour registers, and annotations live in the second:
- Data uses the series palette (the theme's
vizroles). - Annotations use the theme's dedicated
annotationhue — never a data hue. This is deliberate: a mark you added should never be mistaken for a series. Don't override an annotation's colour to match a series; that collapses the two registers.
Depth and brightness
Marks sort into three depth levels, so the one you're interacting with reads in front:
- Level 1 (front) — the selected or editing mark, brightened.
- Level 2 — normal interactive marks.
- Level 3 (back) — inert context (
selectable={false}), always drawn behind the data-facing marks.
You don't set levels directly; they follow from selected / editing /
selectable (see Editing & creating).
Three interaction modes
Every annotation is in one of three modes, set by its props:
| Mode | Props | Behaviour |
|---|---|---|
| Inert | selectable={false} | Background context — no hover, no select, no edit; drawn at level 3. |
| Selectable | default (selectable omitted) | Hover-highlights, click reports via onSelectAnnotation. |
| Editing | editing (+ onChange) | Draggable handles out; reports its new geometry as you drag. |
Pills vs. flags — the indicator law
A mark has two kinds of label:
- A near-line chip (a flag off the line) — its
label, free text. - An on-axis pill — enabled with
indicator, pinned to the axis edge like a tick.
The indicator law: an on-axis pill always shows the formatted axis
coordinate, never a custom label. A name belongs on the near-line chip; the
axis pill reads like a tick. This is why YAxisIndicator (a pure pill) has no
label prop at all — see
Axis indicators & live values.
Sharp edges
- Annotations don't snap to data samples. Dragging snaps to other marks' guidelines (alignment), not to the nearest data point — see Editing & creating.
- Selection is single. One annotation is selected at a time (as with data-mark selection).
- Colour is the annotation register. Don't repaint a mark to a series hue.
See also
- Region · Baseline · Marker — the per-primitive props.
- Editing & creating annotations — the interactive surface.
- Axis indicators & live values — pinned pills and the live path.
- Learn charts, chapter 7 — the tutorial.