---
url: https://chartbuddy.io/embed/docs/axes/secondary.md
---
# Secondary / dual axis

Use a secondary value axis when two series need **independent scales** (e.g. revenue in dollars vs growth in %). Same units on two axes is usually harder to read; prefer one axis when you can.

Secondary sits on:

| Orientation | Secondary side |
|---|---|
| `vertical` | `right` |
| `horizontal` | **`top`** |

***

## Config shape

```js
axes: {
  right: {
    id: 'secondaryRange',
    enabled: true,
    visible: true,
    boundSeries: [1], // original series indices (0-based)
    labels: {
      preferredTickInterval: 10,
      numberFormat: {
        mode: 'custom',
        customFormat: {
          prefix: '',
          postfix: '%',
          decimalPlaces: 0,
          forceDecimals: false,
          thousandSeparator: ',',
          decimalSeparator: '.',
          signDisplay: 'auto',
          scaleDown: 0,
        },
      },
    },
    axisTitle: { visible: true, text: 'Growth' },
  },
}
```

| Field | Role |
|---|---|
| `enabled` / `visible` | Turn the secondary scale and chrome on |
| `boundSeries` | Which **original** series indices use this scale (not filtered/display order) |
| `labels.*` | Independent ticks, bounds, and formats from the primary range |
| `breaks` | Supported on secondary the same way as primary – see [Breaks](/axes/breaks) |

Series **not** listed in `boundSeries` stay on the primary range. Primary bounds **exclude** secondary-bound series so each scale fits its own data.

***

## Line charts

Dual-axis is fully supported on **line** charts:

```js
{
  chartType: 'line',
  isDataTransposed: true,
  seriesData: [
    ['', 'Q1', 'Q2', 'Q3', 'Q4'],
    ['Revenue', 120, 132, 141, 155],
    ['Margin %', 18, 19, 17, 21],
  ],
  axes: {
    left: {
      axisTitle: { visible: true, text: 'Revenue' },
      labels: {
        numberFormat: {
          mode: 'custom',
          customFormat: {
            prefix: '$',
            postfix: '',
            decimalPlaces: 0,
            forceDecimals: false,
            thousandSeparator: ',',
            decimalSeparator: '.',
            signDisplay: 'auto',
            scaleDown: 0,
          },
        },
      },
    },
    right: {
      id: 'secondaryRange',
      enabled: true,
      visible: true,
      boundSeries: [1],
      axisTitle: { visible: true, text: 'Margin %' },
      labels: {
        preferredTickInterval: 5,
        numberFormat: {
          mode: 'custom',
          customFormat: {
            prefix: '',
            postfix: '%',
            decimalPlaces: 0,
            forceDecimals: false,
            thousandSeparator: ',',
            decimalSeparator: '.',
            signDisplay: 'auto',
            scaleDown: 0,
          },
        },
      },
    },
  },
}
```

Live: [Dual-axis line sample](/samples/dual-axis-line).

Indices in `boundSeries` match **row order in `seriesData`** after the header row – series `0` is the first data row, series `1` the second, and so on.

***

## Horizontal dual axis (secondary on top)

With `orientation: 'horizontal'`, put secondary on **`top`**:

```js
{
  orientation: 'horizontal',
  chartType: 'line',
  axes: {
    bottom: { /* primaryRange – values */ },
    top: {
      id: 'secondaryRange',
      enabled: true,
      visible: true,
      boundSeries: [1],
    },
    left: { /* domain – categories */ },
  },
}
```

Give the chart enough height: domain labels on the left plus a top value axis need vertical room.

***

## Combo charts

Combo can bind series to the secondary axis the same way (`boundSeries` + `enabled` / `visible`). Pair with `combo.seriesTypes` so the secondary series renders as a line (or whatever type you need):

```js
{
  chartType: 'combo',
  isDataTransposed: true,
  seriesData: [
    ['', 'Q1', 'Q2', 'Q3', 'Q4'],
    ['Units', 40, 48, 52, 61],
    ['ASP', 12, 11, 13, 14],
  ],
  combo: {
    seriesTypes: { 1: 'line' },
  },
  axes: {
    right: {
      id: 'secondaryRange',
      enabled: true,
      visible: true,
      boundSeries: [1],
      labels: {
        numberFormat: {
          mode: 'custom',
          customFormat: {
            prefix: '$',
            postfix: '',
            decimalPlaces: 0,
            forceDecimals: false,
            thousandSeparator: ',',
            decimalSeparator: '.',
            signDisplay: 'auto',
            scaleDown: 0,
          },
        },
      },
    },
  },
}
```

Bars that stay on the primary axis still share one primary scale. Prefer exporting from the editor if you are unsure of series indices after reordering.

***

## Anti-patterns

* Two axes for the **same** unit (readers compare the wrong scale)
* Forgetting `enabled: true` **and** `visible: true`
* Wrong `boundSeries` index after inserting/reordering series rows
* Putting secondary on `right` after flipping to horizontal (it moves to `top`)

Next: [Value axis](/axes/value-axis) · [Breaks](/axes/breaks) · [By chart type](/axes/by-chart-type)
