---
url: https://chartbuddy.io/embed/docs/configuration/arrows.md
---
# Arrows

Difference, CAGR, and level arrows live under `annotations` as **id-keyed maps**. Each entry is one arrow.

| Kind | chartData path | What it shows |
|---|---|---|
| Difference | `annotations.differenceArrows.<id>` | Absolute and/or % delta between two columns |
| CAGR | `annotations.cagrs.<id>` | Compound annual growth between two columns |
| Level | `annotations.levelArrows.<id>` | Horizontal compare between two anchor points |

CAGR and difference use simple column indices. Level arrows need fuller `anchorPoints` geometry (`barTop` / `linePoint`, `columnIndex`, `seriesIndex`), so they are the easiest of the three to mis-wire by hand.

## Chart type support

| Kind | Stacked / clustered bar | 100% stacked | Waterfall | Combo (bars) | Line / area | Pie / scatter / mekko |
|---|---|---|---|---|---|---|
| Difference | yes | yes\* | yes | yes | no | no |
| CAGR | yes | no | yes | yes | no | no |
| Level | yes | yes | yes | yes | yes | no |

\*Engine can draw difference on 100% stacked; the editor add menu may not offer it on every type. Combo arrows attach to **bar** series only.

## Shared styling

These fields are common across the three arrow maps:

| Field | Type | Typical default | Notes |
|---|---|---|---|
| `lineColor` | string | `'#000000'` | Stroke color |
| `lineWidth` | number | `1.5` | Stroke width (px) |
| `lineStyle` | string | `'solid'` | `solid` · `dashed` · `densely-dashed` · `dotted` · `dash-dot` |
| `labelShape` | string | `'pill'` | `none` · `rounded` · `pill` · `circle` |
| `fontSize` | number | `8`–`10` | Label size (pt) |
| `chartType` | string | current type | Snapshotted when the arrow was created |

Category anchors (`start` / `end` / `columnIndex`) are **0-based column indices** in `seriesData` (after the header row / label column).

### Labels

`displayMode` (and default `labelParts`) drive the **computed** absolute / % text. Optional custom copy uses `label` on level arrows, and `text` or `label` on difference / CAGR arrows.

If you set custom text **and** leave a computed mode active, both can render (for example `+36` next to another `+36`, or `Expansion` plus a computed `+15%`). For custom text only:

```js
label: 'Expansion',
labelParts: ['label'],
```

To show custom text plus a computed part: `labelParts: ['label', 'percentage']` (or `'absolute'`).

### Layout offsets

Fields like `verticalLinePositionIndex`, `horizontalYOffset`, and CAGR’s `startYOffset` / `endYOffset` are usually set by dragging in edit mode. When hand-authoring, start with `null` / omit offsets where possible, or copy them from an exported config.

## Difference arrows (`annotations.differenceArrows`)

U-shaped connector between two columns. Label is usually a computed delta (`displayMode` / `labelParts`).

```js
annotations: {
  differenceArrows: {
    q1VsQ2: {
      start: 0,
      end: 1,
      startSeries: null, // null = bar tops / stack totals
      endSeries: null,
      displayMode: 'percentage',
      chartType: 'stackedBar',
      lineColor: '#111827',
      lineWidth: 1.5,
      lineStyle: 'solid',
      labelShape: 'pill',
    },
  },
}
```

| Field | Meaning |
|---|---|
| `start` / `end` | Column indices |
| `startSeries` / `endSeries` | Series index, or `null` for tops / totals |
| `displayMode` | `percentage` · `absolute` · `absoluteWithPercentage` · `percentageWithAbsolute` |
| `labelParts` | Composite label parts (e.g. `['percentage']`, `['label','percentage']`) |
| `label` / `text` | Optional custom text |
| `numberFormatAbsolute` / `numberFormatPercentage` | Per-arrow number specs — see [Number formats](/configuration/number-formats) |
| `horizontalYOffset` | Drag offset for the horizontal segment |

## CAGR arrows (`annotations.cagrs`)

Diagonal growth-rate arrow. Duration comes from the category span (`|end - start|`), or the series span when comparing within a column.

```js
annotations: {
  cagrs: {
    fySpan: {
      start: 0,
      end: 5,
      startSeries: null,
      endSeries: null,
      chartType: 'stackedBar',
      lineColor: '#000000',
      lineWidth: 1.5,
      lineStyle: 'solid',
      startYOffset: -8,
      endYOffset: -8,
    },
  },
}
```

| Field | Meaning |
|---|---|
| `start` / `end` | Column indices |
| `startSeries` / `endSeries` | Series index, or `null` for tops / totals |
| `text` | Optional custom label override |
| `startYOffset` / `endYOffset` | Per-endpoint vertical drag offsets |

On waterfall, CAGR uses cumulative totals (not step deltas).

## Level arrows (`annotations.levelArrows`)

Horizontal comparison between two explicit anchors (bar tops, segments, or line points).

```js
annotations: {
  levelArrows: {
    seriesCompare: {
      anchorPoints: [
        { columnIndex: 4, seriesIndex: 2, type: 'barTop' },
        { columnIndex: 5, seriesIndex: 2, type: 'barTop' },
      ],
      verticalLinePositionIndex: null, // or a snap index from the editor
      displayMode: 'percentage',
      chartType: 'stackedBar',
      lineColor: '#000000',
      lineWidth: 1.5,
      lineStyle: 'solid',
      label: '',
      labelShape: 'pill',
      fontSize: 8,
    },
  },
}
```

Custom label only (no computed delta beside it):

```js
label: '+36',
labelParts: ['label'],
```

Line-chart variant:

```js
anchorPoints: [
  { columnIndex: 0, seriesIndex: 0, type: 'linePoint' },
  { columnIndex: 4, seriesIndex: 0, type: 'linePoint' },
]
```

| Field | Meaning |
|---|---|
| `anchorPoints` | Two anchors: `columnIndex`, `seriesIndex` (or `'total'` / `null`), optional `type` |
| `verticalLinePositionIndex` | Snap index for the vertical rail / label (`null` = engine default) |
| `displayMode` / `labelParts` | What the label shows |
| `label` | Optional custom text |

`levelArrows` is not seeded in defaults; the bag appears when the first level arrow is added.

## Authoring tip

Add arrows in edit mode (or Hub MCP `manage_annotations`), then snapshot with `getChartData()` / `exportConfig()`. That picks up anchors and layout offsets without guessing snap indices.

See also [Level lines](/configuration/level-lines).
