---
url: https://chartbuddy.io/embed/docs/axes/value-axis.md
---
# Value axis

The **primary range** (and secondary range) axes are linear value scales. On a vertical chart that is usually `axes.left`; on a horizontal chart it is `axes.bottom`. Always write to the **side that holds `id: 'primaryRange'`** (or `'secondaryRange'`) after orientation remap – see [Orientation](/concepts/orientation).

```js
axes: {
  left: {
    id: 'primaryRange',
    labels: {
      preferredTickInterval: null, // auto
      customMin: null,
      customMax: null,
      numberFormat: {
        mode: 'custom',
        customFormat: {
          prefix: '',
          postfix: '',
          thousandSeparator: ',',
          decimalSeparator: '.',
          decimalPlaces: 0,
          forceDecimals: true,
          signDisplay: 'auto',
          scaleDown: 0,
        },
      },
    },
  },
}
```

Full format field reference: [Number formats](/configuration/number-formats).

***

## Two modes: interval vs bounds

Ticks and bounds are **mutually exclusive drivers**. Change one; do not fight both at once.

| | Interval / auto mode | Bounds mode |
|---|---|---|
| Trigger | Leave min/max `null`; optionally set `preferredTickInterval` | Set `customMin` and/or `customMax` |
| Bounds | Auto-round to multiples of the chosen interval | Fixed to your min/max |
| Interval | Prefers your `preferredTickInterval` when allowed | Auto-picked so ticks land on the bounds |

If you pin bounds **and** a preferred interval that cannot divide the span, the engine adjusts the interval (or ignores the preference). Chasing both by hand causes a feedback loop – pick one mode.

***

## Preferred interval

`preferredTickInterval` is a **step size** (e.g. `10`, `25`, `50`), not “number of labels.”

```js
axes: {
  left: {
    labels: {
      preferredTickInterval: 25,
    },
  },
}
```

Leave it `null` for new charts so auto sparse selection runs from the space budget:

| Space for (max labels) | Aim for |
|---|---|
| 1–5 | ~2 labels |
| 6–13 | ~3 |
| 14–20 | ~4 |
| 21+ | ~5 |

Nice candidates are values like 1, 2, 2.5, 5, 10 × powers of 10.

**Do not hand-author `customTickInterval`.** The engine writes that field when resolving ticks. Authors set `preferredTickInterval` (or leave null).

### Percentage charts

For `stackedBar100`, `stackedArea100`, and mekko value axes, allowed intervals are filtered to **divisors of 100** (1, 2, 2.5, 4, 5, 10, 20, 25, 50, 100). Prefer `20` / `25` / `50` for clean decks. Defaults already seed `postfix: '%'`.

See [Percent axis sample](/samples/percent-axis).

***

## Bounds

```js
axes: {
  left: {
    labels: {
      customMin: 0,
      customMax: 200,
    },
  },
}
```

* `null` / omit – auto from data (+ nice rounding in interval mode)
* Set one or both – bounds mode

### Zero rules

| Chart family | Guidance |
|---|---|
| Clustered / stacked bar, stacked area, waterfall | Usually keep **0 in range**. Cutting off zero exaggerates differences. |
| Line, scatter | Setting the range with `customMin` / `customMax` around the data is often right – zero is not required. |
| 100% / mekko value | Pin **0–100** (engine and defaults already bias this way). |

If `customMin` sits **above** the lowest data point (or `customMax` below the highest), bars/lines clip. Fix the bound or widen it.

### Recipe: zoomed line

```js
{
  chartType: 'line',
  isDataTransposed: true,
  seriesData: [/* … */],
  axes: {
    left: {
      labels: {
        customMin: 80,
        customMax: 120,
        preferredTickInterval: 10,
      },
    },
  },
}
```

### Recipe: currency ticks

```js
axes: {
  left: {
    labels: {
      preferredTickInterval: 50,
      numberFormat: {
        mode: 'custom',
        customFormat: {
          prefix: '$',
          postfix: '',
          thousandSeparator: ',',
          decimalSeparator: '.',
          decimalPlaces: 0,
          forceDecimals: false,
          signDisplay: 'auto',
          scaleDown: 0,
        },
      },
    },
    axisTitle: {
      visible: true,
      text: 'USD millions',
    },
  },
}
```

Live: [Currency ticks sample](/samples/currency-ticks).

***

## Horizontal charts

After `orientation: 'horizontal'`, the **primary range is on `bottom`**. Put formats, preferred intervals, bounds, and value gridlines there – not on `left` (that side is domain / categories).

```js
{
  orientation: 'horizontal',
  axes: {
    bottom: {
      labels: {
        preferredTickInterval: 20,
        numberFormat: {
          mode: 'custom',
          customFormat: {
            prefix: '',
            postfix: '%',
            decimalPlaces: 0,
            forceDecimals: false,
            thousandSeparator: ',',
            decimalSeparator: '.',
            signDisplay: 'auto',
            scaleDown: 0,
          },
        },
      },
      gridlines: { visible: true },
    },
  },
}
```

***

## Gridlines

Default for new charts: gridlines **`visible: false`**. Turn them on on the value axis side and pair overrides with a fixed `preferredTickInterval` so tick keys stay stable – [Gridlines](/configuration/gridlines).

***

## Pitfalls

* Authoring `customTickInterval` instead of `preferredTickInterval`
* Formatting `left` after flipping to horizontal (range moved to `bottom`)
* Pinning bounds that clip data
* Using dual custom min/max **and** an incompatible preferred interval
* Expecting `chartData` number format alone to change PNG/slide look – formats are on the axis labels (and linked surfaces)

Next: [Domain axis](/axes/domain) · [Secondary](/axes/secondary) · [By chart type](/axes/by-chart-type)
