> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Annotations

export const GraphEmbed = ({src, story, aspectRatio, borderRadius = '14px', args}) => {
  const STORYBOOK_BASE_URL = typeof window !== 'undefined' && window.location.hostname === 'localhost' ? 'http://localhost:6006' : 'https://storybook-sdk.vercel.app';
  function buildArgsString(args) {
    if (!args) return '';
    const encodeValue = value => {
      if (value === null) return '!null';
      if (value === undefined) return '!undefined';
      if (typeof value === 'boolean') return `!${value}`;
      if (typeof value === 'number') return String(value);
      return encodeURIComponent(String(value));
    };
    const properties = Object.entries(args).map(([key, value]) => `${key}:${encodeValue(value)}`).join(';');
    return `&args=${properties}`;
  }
  const resolvedSrc = story ? `${STORYBOOK_BASE_URL}/iframe.html?id=${story}&viewMode=story&embed=1&globals=mode:readonly${buildArgsString(args)}` : src;
  const resolvedAspectRatio = aspectRatio ?? (story ? '16 / 9' : '10 / 6');
  return <iframe src={resolvedSrc} loading="lazy" allowfullscreen="true" style={{
    width: '100%',
    aspectRatio: resolvedAspectRatio,
    border: 'none',
    borderRadius,
    colorScheme: 'light'
  }} />;
};

Annotations layer callouts on top of the plot — a shaded region, a labelled arrow, a note pinned to a spike. They carry information the data doesn't contain, and the `annotation` builder pipes them into the spec like any other feature.

Because they're a spec feature, annotations re-resolve on every compile: panel-anchored ones re-flow when the chart resizes, and data-anchored ones move with the observation they're pinned to.

## Anchoring

Every annotation is positioned by an *anchor* that describes a relationship to the graph, not a fixed
pixel. Anchors come in two shapes: a **point** for the kinds that sit somewhere, and a **region** for
the kinds that fill an area.

### Point anchors

`PointAnchorSpec` is a union of five `anchorType`s:

| `anchorType` | Fields | Resolves to |
| - | - | - |
| `'panel'` | `x`, `y`, `offset?` | A fraction of the plot rectangle, `[0, 1]` from the top-left. Doesn't snap to data, so it holds its place as the data changes |
| `'observation'` | `layerId?`, `anchorValue`, `groupValue?`, `crossValue?`, `align?`, `offset?` | A point on one data point's box. The annotation follows that observation wherever it lands |
| `'axis'` | `x`, `y`, `align?`, `offset?` | A pair of axis *values*, mapped through the position scales. Dropped when either fails to map — a value outside a discrete domain, a missing scale, or polar coords |
| `'selection'` | `predicate`, `align`, `offset?` | A point on the box of everything a predicate matches — the [same predicate language highlights use](/sdk-next/emphasis/highlights#predicates). Dropped when nothing matches |
| `'annotation'` | `ref`, `align?`, `offset?` | A point on another annotation's box. Dropped on a missing `ref` or a reference cycle |

```tsx theme={null}
// floats at the top-centre of the plot
{ anchorType: 'panel', x: 0.5, y: 0 }

// pins to the 'Jun' point of the 'North' series
{ anchorType: 'observation', anchorValue: 'Jun', groupValue: 'North' }

// sits at the data coordinates (Jun, 3000), wherever the scales put them
{ anchorType: 'axis', x: 'Jun', y: 3000 }

// sits above whatever the predicate matches
{ anchorType: 'selection', predicate: { variable: 'sales', gte: 2000 }, align: 'top' }
```

#### Addressing a grid cell

An observation anchor normally narrows by series: `anchorValue` names the position on the main axis, `groupValue` picks the series. On a [heatmap](/sdk-next/graph-types/heatmap) there are no series — `anchorValue` names a whole column of cells — so `crossValue` names the band on the other axis that picks one cell out of it:

```tsx theme={null}
// the Q3 × Beta cell of a tile layer
{ anchorType: 'observation', anchorValue: 'Q3', crossValue: 'Beta' }
```

`crossValue` is read only by a layer whose observations partition a grid, and ignored everywhere else. A cell anchor resolves to the centre of its tile.

### Region anchors

`RegionAnchorSpec` is a union of three:

| `anchorType` | Fields | Resolves to |
| - | - | - |
| `'panel'` | `x`, `y`, `width`, `height` | A rectangle in panel fractions, top-left origin |
| `'selection'` | `predicate`, `padding?` | The tight bounding box of everything the predicate matches, grown by `padding`. Dropped when nothing matches |
| `'annotation'` | `ref` | A copy of another annotation's box. Dropped on a missing `ref`, a cycle, or a zero-area target |

`padding` is either a number (both axes, in panel fractions) or an `AnchorOffset`. Omit it and a
selection region gets `{ x: 8, y: 8, unit: 'px' }`, so a box wrapping bars doesn't sit flush against
them.

```tsx theme={null}
annotation.shape({
  region: {
    anchorType: 'selection',
    predicate: { variable: 'quarter', oneOf: ['Q3', 'Q4'] },
  },
  zOrder: 'background',
});
```

### `align` and `offset`

`align` names **which point of a box** an anchor resolves to:

```
'center' | 'top' | 'right' | 'bottom' | 'left' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
```

Omit it on a data anchor and you get the geom-natural point — a bar's top-edge midpoint.

`offset` is a nudge applied once the target has resolved: `{ x?, y?, unit?: 'panel' | 'px' }`, with
`unit` defaulting to `'panel'`. Pixel offsets are resolved at render time, against the measured box.

### Naming the layer

On a combo chart, more than one layer can hold the same address — bars and a trend line both have a value at `'Jun'`. Name the layer you mean and pass its id as `layerId`:

```tsx theme={null}
pipe(
  createSpec({ x: 'month' }),
  geom.bar({ id: 'bars', aes: { y: 'sales' } }),
  geom.line({ aes: { y: 'trend' } }),
  annotation.pinnedNumber({ at: { layerId: 'bars', anchorValue: 'Jun' } })
);
```

Omit `layerId` and the anchor resolves against the first layer that matches. An id no layer carries drops that annotation with a warning rather than silently moving it.

Note the asymmetry in what each kind takes: `pinnedNumber` and `comment` take a bare
`ObservationAnchorSpec` — the fields above with no `anchorType` key — while `text`, `sticker` and
`arrow` take a full `PointAnchorSpec`, where `anchorType` is required.

### Ids and references

An annotation's `id` is unique across every kind, and it is what an `annotation` anchor's `ref`
points at. The first annotation to declare an id keeps it; a second one declaring the same id gets a
generated one plus an `ANNOTATION_DUPLICATE_ID` warning. Only ids you write yourself can be
referenced — a `ref` naming nothing raises `ANNOTATION_REF_NOT_FOUND` and drops the annotation that
referred to it.

### One attachment per observation

An observation carries at most one of `sticker`, `pinnedNumber`, `comment` or `image`. Adding a
second **displaces** the first — a comment replaces the pinned number on its bar rather than joining
it. `AddAnnotationCommand` reverts by putting the displaced annotation back, so one undo restores
both halves of a replacement. (An image has no observation region anchor of its own, so today it
never displaces anything.)

## The annotation kinds

Each method on `annotation` appends one callout. Multiple calls of the same kind accumulate.

| Method | What it draws |
| - | - |
| `annotation.differenceArrow()` | A labelled delta between **two observations** — reads the measured gap and labels it as an absolute, relative, or proportional change |
| `annotation.shape()` | A shaded rectangle over a region — the classic "forecast" or "recession" band |
| `annotation.arrow()` | A free-standing arrow between two point anchors, each floating or pinned |
| `annotation.text()` | A free rich-text label |
| `annotation.image()` | An image placed in a region |
| `annotation.sticker()` | A built-in emoji-like sticker pinned to a point |
| `annotation.pinnedNumber()` | A dot on one observation; its value shows in a mini view, full tooltip on hover |
| `annotation.comment()` | A dot on one observation carrying rich-text content |

### Difference arrow

A difference arrow spans two observations and labels the change between them — you choose *what* the label measures:

<GraphEmbed story="features-annotations-difference-arrows--playground" />

```tsx theme={null}
pipe(
  annotation.differenceArrow({
    id: 'growth',
    start: { anchorValue: 'Jan', groupValue: 'North' },
    end: { anchorValue: 'Jun', groupValue: 'North' },
    label: 'relative-difference', // 'absolute-difference' | 'relative-difference' | 'proportion'
    labelCrossPosition: 0.5, // where the label sits along the arrow, as a fraction of its length
  }),
  styles({
    overrides: [
      style.annotation.differenceArrow(
        { strokeWidth: 3 },
        { annotation: 'growth' }
      ),
      style.annotation.differenceArrow.label(
        { fontSize: 16, paddingInline: 8 },
        { annotation: 'growth' }
      ),
    ],
  })
);
```

Distinct from `annotation.arrow()`, which is a plain arrow between two positions you specify — it doesn't read any data.

A difference arrow's paint comes from the [stylesheet](/sdk-next/config/styling): `style.annotation.differenceArrow({
color, strokeWidth })` styles every arrow's line, `style.annotation.differenceArrow.label({ fontSize, fontWeight,
textColor, fill, stroke, strokeWidth, cornerRadius, paddingInline, paddingBlock, ... })` its label, and
`{ annotation: id }` in the options narrows an entry to one. With nothing declared the line is 2px and the label a
12px boxed number; the route around the observations scales with `strokeWidth`. An arrow whose colour no entry
names takes the series colour its two ends share, when they share one, else black (white under a dark scheme);
the label's box border takes the arrow's colour unless `stroke` is declared.

<Note>
  A difference is only meaningful when both ends measure the same thing.
  Endpoints on different bases — one on the primary y axis and one on the
  secondary, or a revenue value against a margin value in the same reshaped
  column — raise an `INCOMPARABLE_ARROW_ENDPOINTS` warning. The arrow still
  draws, so you can see what it claims.
</Note>

### Shaded region

<GraphEmbed story="features-annotations-arrow-annotations--shape-behind-bars" />

```tsx theme={null}
pipe(
  annotation.shape({
    id: 'forecast',
    region: { anchorType: 'panel', x: 0, y: 0.7, width: 1, height: 0.3 },
    zOrder: 'background', // beneath the geoms; 'foreground' draws on top
  }),
  styles({
    overrides: [
      style.annotation.shape(
        { fill: '#e15759', alpha: 0.12 },
        { annotation: 'forecast' }
      ),
    ],
  })
);
```

`zOrder` decides whether a shape or image sits behind the data (`'background'`) or in front of it
(`'foreground'`, the default). A wash of colour under the marks wants `'background'` written out, as
above.

| Field | Type | Default |
| - | - | - |
| `kind` | `'rectangle'` | `'rectangle'` |
| `zOrder` | `'background' \| 'foreground'` | `'foreground'` |

A shape's paint comes from the [stylesheet](/sdk-next/config/styling): `style.annotation.shape({ fill, fillAlpha, alpha,
stroke, strokeWidth })` styles every shape, and `{ annotation: id }` in the options narrows an entry
to one. With nothing declared a shape is a quarter-strength blue wash inside a 1px border of the same
blue at full strength; the border wears `fill` unless `stroke` says otherwise, `strokeWidth: 0`
removes it, and `fill: 'transparent'` makes a stroke-only outline.

### Text

A text annotation takes a [rich-text node](/sdk-next/config/content) and a point anchor. `width` is a fraction of the plot width; height follows the content:

<GraphEmbed story="features-annotations-arrow-annotations--text-callout" />

```tsx theme={null}
annotation.text({
  content: {
    type: 'doc',
    content: [
      {
        type: 'paragraph',
        content: [{ type: 'text', text: 'Product launch' }],
      },
    ],
  },
  at: { anchorType: 'panel', x: 0.4, y: 0.1 },
  width: 0.25,
});
```

`at` places the point of the text's own box named by `align`, which defaults to `'center'` — so the
sample above centres the block on `(0.4, 0.1)`. Pass `align: 'top-left'` to hang it from that corner
instead.

| Field | Type | Default |
| - | - | - |
| `align` | one of the nine `AnchorAlign` values | `'center'` |

A text annotation's box and base type come from the [stylesheet](/sdk-next/config/styling):
`style.annotation.text({ fill, fillAlpha, alpha, stroke, strokeWidth, cornerRadius, paddingInline, paddingBlock,
fontFamily, fontSize, fontWeight, lineHeight, textColor })` styles every text annotation, and `{ annotation: id }`
in the options narrows an entry to one. With nothing declared the words sit on no box at all, in 15px medium type;
declare a `fill` and the box appears, with a border a shade darker than it unless `stroke` says
otherwise, `fillAlpha` fades the fill and `alpha` the whole box. The content's own marks — a colour, a font, a size — paint over the base
type, so a size mark is a multiple of the stylesheet's `fontSize`.

### Arrow

`annotation.arrow()` draws a plain arrow between two point anchors. Either end can float in panel
fractions or pin to data:

```tsx theme={null}
pipe(
  annotation.arrow({
    id: 'to-june',
    start: { anchorType: 'panel', x: 0.3, y: 0.2 },
    end: { anchorType: 'observation', anchorValue: 'Jun', align: 'top' },
    endArrowheadStyle: 'line-arrow', // 'none' | 'line-arrow'
  }),
  styles({
    overrides: [
      style.annotation.arrow(
        { stroke: '#e15759', strokeWidth: 8, dashArray: [1.5, 3] },
        { annotation: 'to-june' }
      ),
    ],
  })
);
```

| Field | Type | Default |
| - | - | - |
| `startArrowheadStyle` | `'none' \| 'line-arrow'` | `'none'` |
| `endArrowheadStyle` | `'none' \| 'line-arrow'` | `'line-arrow'` |

An arrow's paint comes from the [stylesheet](/sdk-next/config/styling): `style.annotation.arrow({ stroke,
strokeWidth, dashArray })` styles every arrow, and `{ annotation: id }` in the options narrows an entry to
one. With nothing declared an arrow is a solid 4px line in black (white under a dark scheme). `strokeWidth`
is the width on a wide panel, scaled down on narrow ones, and the arrowheads scale with it. The sticker
look is the arrow's outline part, `style.annotation.arrow.outline({ stroke, strokeWidth, shadow })` — an
outline around the line and a drop shadow; the outline picks a colour that reads against the arrow unless
`stroke` is declared.

An endpoint on a data anchor with a directional `align` and no `offset` of its own is backed off 4px
from the target's edge, so an arrow pointing at a bar stops just short of touching it. Supply an
explicit `offset` to place it yourself.

### Image

Place an image in a panel region. `fit` decides how it scales inside the box — `'fill'` stretches, `'contain'` letterboxes, `'cover'` crops to fill:

<GraphEmbed story="features-annotations-image-annotations--fit" />

```tsx theme={null}
annotation.image({
  src: 'https://example.com/logo.png', // URL or data URI
  region: { anchorType: 'panel', x: 0.05, y: 0.08, width: 0.24, height: 0.5 },
  fit: 'contain', // 'fill' | 'contain' | 'cover'
});
```

Like a shape, an image also takes an optional `zOrder` (`'background'` or `'foreground'`, defaulting
to `'foreground'`). `fit` defaults to `'contain'`.

An image's paint comes from the [stylesheet](/sdk-next/config/styling): `style.annotation.image({ alpha,
cornerRadius })` styles every image, and `{ annotation: id }` in the options narrows an entry to one. With
nothing declared an image is opaque with square corners; `cornerRadius` rounds them, in pixels.

### Sticker

A sticker pins a built-in emoji-like image to a single observation. Give it a `sticker` id from the catalogue and an observation anchor — it then travels with its data point through re-sorts and filters:

<GraphEmbed story="features-annotations-sticker-annotations--multiple-on-cartesian" />

```tsx theme={null}
annotation.sticker({
  sticker: 'rocket',
  at: { anchorType: 'observation', anchorValue: 'Q4' },
});
```

The catalogue holds exactly five ids — `'rocket'`, `'thumbs-up'`, `'thumbs-down'`,
`'clapping-hands'` and `'grinning-face'`. An id outside it renders nothing.

### Pinned number and comment

Both pin a marker dot to one observation and place a mini bubble beside it: a pinned number shows the
observation's formatted value behind a swatch, a comment its text on one line, ellipsized when long.
Hovering a bubble opens the full tooltip. Both take a bare observation anchor:

<GraphEmbed story="features-annotations-callouts--styled" />

```tsx theme={null}
annotation.pinnedNumber({ at: { anchorValue: 'Q4' } });
annotation.comment({ at: { anchorValue: 'Q2' }, content: doc });
```

Their paint comes from the [stylesheet](/sdk-next/config/styling), in two parts per kind. The marker is
`style.annotation.pinnedNumber({ fill, size, stroke, strokeWidth, shadow })` — likewise
`style.annotation.comment(...)` — and the bubble is their `.label(...)` part, on the `dataLabel` words
plus `shadow`; `{ annotation: id }` in the options narrows an entry to one. A marker whose `fill` no
entry declares borrows the observation's colour, which the swatch inside a pinned-number bubble always
keeps.

## Editing annotations at runtime

Four [commands](/sdk-next/advanced/commands-and-history) cover the editing surface:

| Command | Params |
| - | - |
| `AddAnnotationCommand` | `kind`, `annotation`, `index?` |
| `RemoveAnnotationCommand` | `id` |
| `UpdateAnnotationCommand` | `kind`, `id`, `patch` |
| `MoveAnnotationCommand` | `id`, `translation` |

`MoveAnnotationCommand` is the drag path: a translation in panel fractions, clamped inside the panel
on apply. It is relative, so applying it twice moves twice, and it reverts through the inverse patch
rather than the opposite translation — clamping isn't invertible.

`AddAnnotationCommand` takes a fully resolved annotation, so callers spread the kind's defaults:
`{ ...SHAPE_DEFAULTS, region }`. Only `id` is optional, and it is settled in the constructor so a
serialized command replays to the same spec.

## Related

* [Reference lines](/sdk-next/emphasis/reference-lines) — for marking a constant value rather than a region
* [Highlights](/sdk-next/emphasis/highlights) — the predicate language `selection` anchors take
* [Content](/sdk-next/config/content) — the rich-text node shape used by text and comment annotations
* [Mappings & aesthetics](/sdk-next/concepts/mappings) — the series and x-values an observation anchor names


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.