---
url: https://chartbuddy.io/embed/docs/configuration/number-formats.md
---
# Number formats

Every numeric surface uses the same **spec** shape. There is no separate sibling `format` bag.

```js
{
  mode: 'custom', // or 'linked'
  source: undefined, // only when mode is 'linked'
  customFormat: {
    prefix: '',
    postfix: '',
    thousandSeparator: ',',
    decimalSeparator: '.',
    decimalPlaces: 0,
    forceDecimals: true,
    signDisplay: 'auto', // 'auto' | 'always' | 'parens'
    scaleDown: 0,
  },
}
```

| Field | Role |
|---|---|
| `mode` | `'custom'` — this surface owns `customFormat`. `'linked'` — inherit another surface |
| `source` | When linked: `'primaryRangeAxis'` | `'dataLabels'` | `'totalLabels'` |
| `customFormat` | The formatting knobs (also kept as an unlink stash when linked) |
| `prefix` / `postfix` | Strings before / after the number (`postfix: '%'` on percentage charts) |
| `thousandSeparator` / `decimalSeparator` | Grouping and decimal marks |
| `decimalPlaces` | Digits after the decimal |
| `forceDecimals` | Pad to `decimalPlaces` even with trailing zeros |
| `signDisplay` | `'auto'` | `'always'` | `'parens'` |
| `scaleDown` | Scale exponent (`0` = raw; higher values shorten large magnitudes) |

**Not a number format:** category date patterns live at `axes.<side>.labels.dateFormat.format` (a string like `'MMM D, YYYY'`).

***

## 1. Axes

Value-axis tick labels. Path is always under the side that holds that axis for the current orientation.

| Surface | Path | Notes |
|---|---|---|
| Primary value axis (Y on vertical charts) | `axes.left.labels.numberFormat` | Usually the main “Y-axis” format hub |
| Primary value axis (Y on horizontal charts) | `axes.bottom.labels.numberFormat` | Same role after orientation flip |
| Secondary value axis | `axes.right` (vertical) or `axes.top` (horizontal) `.labels.numberFormat` | Dual-axis; typically `mode: 'custom'` only — see [Secondary](/axes/secondary) |
| Domain / category axis | `axes.<side>.labels.numberFormat` | Rarely used for numbers; mekko-style % domain ticks are the main case |

Link targets elsewhere refer to the primary value axis as `source: 'primaryRangeAxis'` (not the side name).

```js
axes: {
  left: {
    labels: {
      numberFormat: {
        mode: 'custom',
        customFormat: {
          prefix: '$',
          postfix: '',
          thousandSeparator: ',',
          decimalSeparator: '.',
          decimalPlaces: 0,
          forceDecimals: true,
          signDisplay: 'auto',
          scaleDown: 0,
        },
      },
    },
  },
}
```

***

## 2. Chart labels

Whole-chart label formats under `annotations`.

### Data labels

| Surface | Path | Default / notes |
|---|---|---|
| Main data-label format | `annotations.dataLabels.numberFormat` | Hub used by Absolute when Absolute is linked |
| Absolute part | `annotations.dataLabels.numberFormatAbsolute` | Defaults to `mode: 'linked', source: 'dataLabels'` |
| Percentage part | `annotations.dataLabels.numberFormatPercentage` | Defaults to custom with `postfix: '%'` |

Which part paints depends on `annotations.dataLabels.labelParts` (e.g. `['absolute']`, `['percentage']`, `['absolute','percentage']`). Pie / donut slice labels also use these Absolute / Percentage helpers.

```js
annotations: {
  dataLabels: {
    labelParts: ['absolute'],
    numberFormat: {
      mode: 'custom',
      customFormat: { /* … */ },
    },
    numberFormatAbsolute: {
      mode: 'custom', // or keep linked to 'dataLabels'
      customFormat: { /* … */ },
    },
    numberFormatPercentage: {
      mode: 'custom',
      customFormat: {
        prefix: '',
        postfix: '%',
        thousandSeparator: ',',
        decimalSeparator: '.',
        decimalPlaces: 0,
        forceDecimals: true,
        signDisplay: 'auto',
        scaleDown: 0,
      },
    },
  },
}
```

### Stack / column totals

| Surface | Path | Charts |
|---|---|---|
| Totals | `annotations.totals.numberFormat` | Stacked bar / `stackedBar100`, mekko, bar mekko, waterfall |

Not used on line or area (no stack totals). Changing the Y-axis does **not** change totals unless totals are linked to `primaryRangeAxis`.

```js
annotations: {
  totals: {
    visible: true,
    numberFormat: {
      mode: 'custom',
      customFormat: { /* … */ },
    },
  },
}
```

***

## 3. Annotation overlays

Per-object formats. Each arrow / line has its own spec (not shared chart-wide).

### Level lines

| Surface | Path |
|---|---|
| Level-line value label | `annotations.levelLines.<id>.labelNumberFormatSpec` |

Same shape as `numberFormat`. Default when unset: follow the primary Y-axis (`linked` → `primaryRangeAxis`). Lines bound to the secondary axis follow that axis’s custom format. See [Level lines](/configuration/level-lines).

```js
annotations: {
  levelLines: {
    target: {
      value: 70,
      labelNumberFormatSpec: {
        mode: 'custom',
        customFormat: { /* … */ },
      },
    },
  },
}
```

### Arrows (difference, CAGR, level)

Each entry under these maps can carry Absolute and Percentage specs:

| Kind | Map path | Absolute | Percentage |
|---|---|---|---|
| Difference | `annotations.differenceArrows.<id>` | `numberFormatAbsolute` | `numberFormatPercentage` |
| CAGR | `annotations.cagrs.<id>` | `numberFormatAbsolute` | `numberFormatPercentage` |
| Level | `annotations.levelArrows.<id>` | `numberFormatAbsolute` | `numberFormatPercentage` |

Which of Absolute / Percentage appears depends on that arrow’s `displayMode` / `labelParts`. See [Arrows](/configuration/arrows).

```js
annotations: {
  differenceArrows: {
    q1VsQ2: {
      start: 0,
      end: 1,
      displayMode: 'absoluteWithPercentage',
      numberFormatAbsolute: {
        mode: 'custom',
        customFormat: { /* … */ },
      },
      numberFormatPercentage: {
        mode: 'custom',
        customFormat: {
          prefix: '',
          postfix: '%',
          thousandSeparator: ',',
          decimalSeparator: '.',
          decimalPlaces: 1,
          forceDecimals: true,
          signDisplay: 'auto',
          scaleDown: 0,
        },
      },
    },
  },
}
```

***

## 4. Linking

Three **hubs** can be link sources: primary Y-axis (`primaryRangeAxis`), data labels (`dataLabels`), totals (`totalLabels`).

| Target | Can link to |
|---|---|
| Primary Y-axis | `dataLabels`, `totalLabels` |
| Data labels | `primaryRangeAxis`, `totalLabels` |
| Totals | `primaryRangeAxis`, `dataLabels` |
| Absolute / Percentage parts | `dataLabels`, `primaryRangeAxis`, `totalLabels` |
| Arrow Absolute / Percentage | `primaryRangeAxis`, `dataLabels`, `totalLabels` |
| Level-line labels | `primaryRangeAxis`, `dataLabels`, `totalLabels` |

Hand-authored configs should prefer `mode: 'custom'` so the format does not depend on another surface.

```js
// Totals follow the Y-axis
annotations: {
  totals: {
    numberFormat: {
      mode: 'linked',
      source: 'primaryRangeAxis',
      customFormat: { /* stash if unlinked later */ },
    },
  },
}
```

***

## 5. Quick path index

| Category | Full path |
|---|---|
| Primary / secondary / domain axis | `axes.<left\|right\|bottom\|top>.labels.numberFormat` |
| Data labels (main) | `annotations.dataLabels.numberFormat` |
| Data-label Absolute | `annotations.dataLabels.numberFormatAbsolute` |
| Data-label Percentage | `annotations.dataLabels.numberFormatPercentage` |
| Stack totals | `annotations.totals.numberFormat` |
| Level-line label | `annotations.levelLines.<id>.labelNumberFormatSpec` |
| Difference arrow Absolute / % | `annotations.differenceArrows.<id>.numberFormatAbsolute` / `numberFormatPercentage` |
| CAGR arrow Absolute / % | `annotations.cagrs.<id>.numberFormatAbsolute` / `numberFormatPercentage` |
| Level arrow Absolute / % | `annotations.levelArrows.<id>.numberFormatAbsolute` / `numberFormatPercentage` |

Percentage chart types (`stackedBar100`, `stackedArea100`, `mekko`) often set `customFormat.postfix: '%'` on the surfaces you care about (usually the primary axis and/or totals).
