Skip to main content

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.

src/examples/learn-07-annotations.tsx
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 viz roles).
  • Annotations use the theme's dedicated annotation hue — 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:

ModePropsBehaviour
Inertselectable={false}Background context — no hover, no select, no edit; drawn at level 3.
Selectabledefault (selectable omitted)Hover-highlights, click reports via onSelectAnnotation.
Editingediting (+ 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