---
url: https://chartbuddy.io/embed/docs/chart-types/stacked-bar.md
---
# Stacked bar

Stacked bars show composition within each category (absolute or normalised to 100%).

## Identity

| | |
|---|---|
| `chartType` | `stackedBar` · `stackedBar100` |
| Option bag | `bar` |
| `seriesData` layout | `seriesRows` – row 0 = category header; later rows = series |
| Min grid | 2 rows × 2 columns (header included) |
| Orientation | Yes – `vertical` (default) or `horizontal` |
| Sample | [Stacked bar](/samples/stacked-bar) · [Percent axis](/samples/percent-axis) |

***

## Minimal `chartData`

```js
{
  chartType: 'stackedBar', // or 'stackedBar100'
  isDataTransposed: true,
  seriesData: [
    ['', 'Q1', 'Q2', 'Q3'],
    ['Product A', 40, 48, 52],
    ['Product B', 30, 28, 35],
  ],
  title: { visible: true, text: 'Mix by quarter' },
  subtitle: { visible: false, text: '' },
}
```

***

## `seriesData`

Same `seriesRows` layout as [clustered bar](/chart-types/clustered-bar) (`isDataTransposed: true`):

```js
[
  ['', 'Q1', 'Q2', 'Q3'],
  ['Product A', 40, 48, 52],
  ['Product B', 30, 28, 35],
]
```

Shared layout rules: [chartData schema · seriesData](/api/chart-data#seriesdata-layouts).

***

## Option bag: `bar`

`stackedBar` / `stackedBar100` / `clusteredBar` all read **`cd.bar`**. Stacking vs clustering comes from `chartType`, not a different bag.

| Field | Default (new charts) | Role |
|---|---|---|
| `padding` | `0.3` | Gap between category stacks, `0–1` |
| `cornerRadius` | `0` | Corner radius (px) |
| `reverseStackOrder` | `false` | Reverse segment order within the stack |
| `seriesStrokes` / `seriesStrokeWidths` / `seriesStrokeDashArrays` | `[]` | Per-series stroke chrome |
| `series` / `overrides` | empty | Indexed style patches |

```js
{
  chartType: 'stackedBar',
  seriesData: [/* … */],
  bar: {
    padding: 0.25,
    reverseStackOrder: true,
  },
}
```

***

## Axes & scales

| Role | Vertical | Horizontal |
|---|---|---|
| Domain | `bottom` | `left` |
| Primary range | `left` | `bottom` |

### `stackedBar`

Absolute stacks. Keep **0 in range**. Format and tick the primary range side after orientation.

### `stackedBar100`

Stacks normalised to **0–100**. Defaults seed `%` postfix; prefer `preferredTickInterval` of `20` / `25` / `50` (divisors of 100).

```js
{
  chartType: 'stackedBar100',
  isDataTransposed: true,
  seriesData: [/* … */],
  axes: {
    left: {
      labels: {
        preferredTickInterval: 25,
        // % postfix usually already seeded
      },
    },
  },
}
```

Details: [Axes · by chart type](/axes/by-chart-type) · [Value axis · percentage charts](/axes/value-axis#percentage-charts) · [Percent axis sample](/samples/percent-axis).

***

## Type-specific behaviour

* Switching `stackedBar` ↔ `stackedBar100` ↔ `clusteredBar` keeps the same grid and `bar` bag.
* Totals / data labels often live under `annotations` – polish in the editor, then `getChartData()`.
* Palette: `legend.colors`.

***

## Pitfalls

* Using the `area` bag on a stacked bar (`foreign-option-bag`)
* Hand-picking a `%` tick interval that is not a divisor of 100 on `stackedBar100`
* Pinning bounds that clip the stack (or that fight 0–100 on 100% charts)
* Expecting `reverseStackOrder` to reorder categories – it reorders **segments**, not domain columns

***

## Related

* [Clustered bar](/chart-types/clustered-bar) · [Stacked area](/chart-types/stacked-area) · [Combo](/chart-types/combo)
* [chartData schema](/api/chart-data) · [Axes](/axes/)
* Samples: [Stacked bar](/samples/stacked-bar) · [Percent axis](/samples/percent-axis)
