> ## 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 = 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${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. There are two frames:

* **Panel** — a fraction of the plot rectangle, `[0, 1]` from the top-left. `{ anchorType: 'panel', x: 0.5, y: 0 }` is the top-centre. Panel anchors don't snap to data, so they hold their place as the data changes.
* **Observation** — pinned to a single data point by its `anchorValue` (the x-value) and optional `groupValue` (the series). The annotation follows that observation wherever it lands.

```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' }
```

Region-based annotations (shapes, images) take a panel *rectangle* — `{ anchorType: 'panel', x, y, width, height }` — and point/observation annotations take a single anchor. An observation anchor also accepts an `align` (which edge or corner of the observation to attach to) and an `offset` (a nudge in panel fractions or pixels).

## 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 panel 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 panel 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}
annotation.differenceArrow({
  start: { anchorValue: 'Jan', groupValue: 'North' },
  end: { anchorValue: 'Jun', groupValue: 'North' },
  label: 'relative-difference', // 'absolute-difference' | 'relative-difference' | 'proportion'
});
```

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

### Shaded region

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

```tsx theme={null}
annotation.shape({
  region: { anchorType: 'panel', x: 0, y: 0.7, width: 1, height: 0.3 },
  fillColor: '#e15759',
  fillOpacity: 0.12,
  zOrder: 'background', // beneath the geoms; 'foreground' draws on top
});
```

`zOrder` decides whether a shape or image sits behind the data (`'background'`, the usual choice for a wash of color) or in front of it (`'foreground'`).

### Text

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

<GraphEmbed story="features-annotations-freeform-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,
});
```

### 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'`) and an `opacity` in `[0, 1]`.

### 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', // 'rocket' | 'thumbs-up' | 'thumbs-down' | 'clapping-hands' | 'grinning-face' | …
  at: { anchorType: 'observation', anchorValue: 'Q4' },
});
```

## Related

* [Reference lines](/sdk-next/emphasis/reference-lines) — for marking a constant value rather than a region
* [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
